Project Unknown
2021-03-15T01:13:26+09:00
project-unknown
Hatena::Blog
hatenablog://blog/8454420450077835463
CocoaPodsを使って困った点、躓いた点をまとめて見ました
hatenablog://entry/26006613517317362
2021-03-15T01:13:26+09:00
2021-03-15T01:13:26+09:00 まだまだお付き合いしないと行けないCocoaPods Carthageが台頭して来て、利用するSDKがCarthage対応していたら大抵Carthage版をinstallするのですが、まだまだCocoaPodsで管理されているSDKが多い為、iOSエンジニアだとCocoaPodsを使うことを避けては通れません。 特にGoogle製SDKを利用しようと思ったらCocoaPodsは確実に登場します。 (決して駄目と言っているんじゃないですよ、Carthageと比較するとどうしても見劣りするだけで…) そんなCocoaPodsですが、ちょくちょく躓いたり、困ったりする時があり、その都度このブログに備…
<h2>まだまだお付き合いしないと行けないCocoaPods</h2>
<p>Carthageが台頭して来て、利用するSDKがCarthage対応していたら大抵Carthage版をinstallするのですが、まだまだCocoaPodsで管理されているSDKが多い為、iOSエンジニアだとCocoaPodsを使うことを避けては通れません。<br />
特にGoogle製SDKを利用しようと思ったらCocoaPodsは確実に登場します。</p>
<p>(決して駄目と言っているんじゃないですよ、Carthageと比較するとどうしても見劣りするだけで…)</p>
<p>そんなCocoaPodsですが、ちょくちょく躓いたり、困ったりする時があり、その都度このブログに備忘録的に残しては来たのですが、</p>
<ul>
<li>記事の数が多くなってきた</li>
<li>それなりにアクセスがある = 他にも困っている人が結構いる</li>
</ul>
<p>という理由で、CocoaPodsを利用して困った・躓いたをまとめようと思います。</p>
<h2>XCodeで Class ○○ is implemented in both ○○ and One of the two will be used. Which one is undefined.のエラーが出る</h2>
<h3>事象</h3>
<p>以下のエラーが多発した時</p>
<pre class="code" data-lang="" data-unlink>Class {$クラス名} is implemented in both {$FrameworkPath} and {$AppPath}. One of the two will be used. Which one is undefined.</pre>
<h3>解決策</h3>
<p>CocoaPodsでSDKを入れた直後にこのエラーが出た場合は、おそらくPodfileのTarget指定がミスっています。<br />
必要なTargetにSDKを記載します。</p>
<p>例えば、アプリ本体にSDKを入れないと行けないのに、以下のようにEmbedded FrameworkにSDKを入れてしまった時とかは、</p>
<pre class="code" data-lang="" data-unlink>target '{$Embedded Frameworkの名前}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for {$Embedded Frameworkの名前}
# add the Firebase pod for Google Analytics
pod 'Firebase/Analytics'
pod 'Firebase/Core'
pod 'Firebase/Firestore'
pod 'Firebase/Auth'
# Optionally, include the Swift extensions if you're using Swift.
pod 'FirebaseFirestoreSwift'
# add pods for any other desired Firebase products
# https://firebase.google.com/docs/ios/setup#available-pods
target '{$Embedded Frameworkの名前}Tests' do
# Pods for testing
end
end
target '{$AppName}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
target '{$AppName}Tests' do
inherit! :search_paths
# Pods for testing
end
target '{$AppName}UITests' do
# Pods for testing
end
end</pre>
<p>以下のようにアプリ本体のTargetにSDKを入れるようにします。</p>
<pre class="code" data-lang="" data-unlink>target '{$Embedded Frameworkの名前}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for {$Embedded Frameworkの名前}
target '{$Embedded Frameworkの名前}Tests' do
# Pods for testing
end
end
target '{$AppName}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# add the Firebase pod for Google Analytics
pod 'Firebase/Analytics'
pod 'Firebase/Core'
pod 'Firebase/Firestore'
pod 'Firebase/Auth'
# Optionally, include the Swift extensions if you're using Swift.
pod 'FirebaseFirestoreSwift'
# add pods for any other desired Firebase products
# https://firebase.google.com/docs/ios/setup#available-pods
target '{$AppName}Tests' do
inherit! :search_paths
# Pods for testing
end
target '{$AppName}UITests' do
# Pods for testing
end
end</pre>
<h2>ld: framework not found {ライブラリ名}のエラーが出る</h2>
<h3>事象</h3>
<p>コンパイル時に、以下のようにライブラリが見当たらない系のエラーが出る</p>
<pre class="code" data-lang="" data-unlink>ld: framework not found {ライブラリ名}</pre>
<h3>解決策</h3>
<p>原因によって色々解決策が別れますが、pod installしなおして見た時に、以下の警告が出ていないか確認してみてください。</p>
<pre class="code" data-lang="" data-unlink>[!] The {$AppName} target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build setting defined in `Pods/Target Support Files/Pods-{$AppName}-{$AppName}UITests/Pods-{$AppName}-{$AppName}UITests.release.xcconfig'. This can lead to problems with the CocoaPods installation
- Use the `$(inherited)` flag, or
- Remove the build settings from the target.</pre>
<p>この場合は、ちゃんと警告を潰しましょう。<br />
以下は、$(inherited)をbuild settingsに設定したときの例です</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20200221/20200221212441.png" alt="f:id:project-unknown:20200221212441p:plain" title="" class="hatena-fotolife" itemprop="image"></span></p>
<p>この警告が出て、ld: framework not found {ライブラリ名}だった場合はこれで直ります。</p>
<h2>No such module XXXX 系のエラー</h2>
<h3>事象</h3>
<p>コンパイルやArchive時に以下のようにモジュールが見つからない系のエラーが出る</p>
<pre class="code" data-lang="" data-unlink>No such module 'モジュール名'</pre>
<p>CocoaPodsを使っているとにくいほどよく見るエラー</p>
<h3>解決策</h3>
<p>いろんな事象が折り混ざってますが、以下を試してみてください</p>
<h4>XCode, Mac再起動</h4>
<p>これで結構治ったりします</p>
<h4>Clean実行</h4>
<p>クリーンしてリビルドで結構治ったりします</p>
<h4>Pods入れ直し</h4>
<p>以下で一度消して</p>
<pre class="code" data-lang="" data-unlink>pod deintegrate</pre>
<p>入れ直す</p>
<pre class="code" data-lang="" data-unlink>pod install</pre>
<h4>iOSのTargetバージョンがあってるか</h4>
<p>上記でも直らない場合( 私がそうでした )、私の場合、PodFileで指定したバージョンとXCodeで設定しているiOSとのバージョンで差異があってエラーを起こしていました。
(Archiveの時しか発生していなかったので本当に難航しました)</p>
project-unknown
XCodeで Class ○○ is implemented in both ○○ and One of the two will be used. Which one is undefined.が出るときの対処法
hatenablog://entry/26006613515365643
2020-02-17T21:13:47+09:00
2020-02-21T21:10:07+09:00 XCodeで Class ○○ is implemented in both ○○ and One of the two will be used. Which one is undefined.が出るときの対処法を記載しています。
<h2>はじめに</h2>
<p>表題の通り、XCodeでアプリを動かした時に以下のエラーが多発しました。<br />
(似た人の救済になれば…なので、量が多いですが全文載せておきます)</p>
<pre class="code" data-lang="" data-unlink>Class FIRAnalyticsConnector is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0c850) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3c38). One of the two will be used. Which one is undefined.
Class FIRConnectorUtils is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0c8a0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3c88). One of the two will be used. Which one is undefined.
Class FIRAIdentifiers is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0c918) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3d00). One of the two will be used. Which one is undefined.
Class FIRAConditionalUserProperty is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0c968) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3d50). One of the two will be used. Which one is undefined.
Class FIRAConditionalUserPropertyController is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0c9b8) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3da0). One of the two will be used. Which one is undefined.
Class FIRAEvent is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0ca08) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3df0). One of the two will be used. Which one is undefined.
Class FIRAUserAttribute is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0ca58) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3e40). One of the two will be used. Which one is undefined.
Class FIRAValue is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0caa8) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3e90). One of the two will be used. Which one is undefined.
Class FIRAAdExposureReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0caf8) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3ee0). One of the two will be used. Which one is undefined.
Class FIRAMeasurement is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0cb20) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3f08). One of the two will be used. Which one is undefined.
Class FIRASessionReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0cb98) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3f80). One of the two will be used. Which one is undefined.
Class FIRAnalytics is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0cbc0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a3fa8). One of the two will be used. Which one is undefined.
Class FIRAScreenViewReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e0cc38) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4020). One of the two will be used. Which one is undefined.
Class APMAdExposureReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e102c0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4048). One of the two will be used. Which one is undefined.
Class APMAlarm is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10310) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4098). One of the two will be used. Which one is undefined.
Class APMAnalytics is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10360) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a40e8). One of the two will be used. Which one is undefined.
Class APMAudience is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e103b0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4138). One of the two will be used. Which one is undefined.
Class APMDatabase is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10400) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4188). One of the two will be used. Which one is undefined.
Class APMEnvironmentInfo is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10450) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a41d8). One of the two will be used. Which one is undefined.
Class APMIdentity is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e104a0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4228). One of the two will be used. Which one is undefined.
Class APMMeasurement is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e104f0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4278). One of the two will be used. Which one is undefined.
Class APMMonitor is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10540) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a42c8). One of the two will be used. Which one is undefined.
Class APMPersistedConfig is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10590) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4318). One of the two will be used. Which one is undefined.
Class APMRemoteConfig is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e105e0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4368). One of the two will be used. Which one is undefined.
Class APMScheduler is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10630) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a43b8). One of the two will be used. Which one is undefined.
Class APMSessionReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10680) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4408). One of the two will be used. Which one is undefined.
Class APMIdentifiers is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e106d0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4458). One of the two will be used. Which one is undefined.
Class APMSearchAdReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10720) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a44a8). One of the two will be used. Which one is undefined.
Class APMAppDelegateInterceptor is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10770) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a44f8). One of the two will be used. Which one is undefined.
Class APMAudienceComparisonValues is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e107e8) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4570). One of the two will be used. Which one is undefined.
Class APMAudienceTimestampsCache is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10810) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4598). One of the two will be used. Which one is undefined.
Class APMSequenceTimestampsCache is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10860) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a45e8). One of the two will be used. Which one is undefined.
Class APMConditionalUserProperty is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e108b0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4638). One of the two will be used. Which one is undefined.
Class APMConditionalUserPropertyController is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10900) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4688). One of the two will be used. Which one is undefined.
Class APMAppMetadata is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10950) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a46d8). One of the two will be used. Which one is undefined.
Class APMDailyCounts is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e109a0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4728). One of the two will be used. Which one is undefined.
Class APMDataTypeValidator is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e109f0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4778). One of the two will be used. Which one is undefined.
Class APMEvent is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10a40) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a47c8). One of the two will be used. Which one is undefined.
Class APMEventAggregates is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10a90) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4818). One of the two will be used. Which one is undefined.
Class APMEventFilter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10ae0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4868). One of the two will be used. Which one is undefined.
Class APMFilterResult is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10b30) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a48b8). One of the two will be used. Which one is undefined.
Class APMPropertyFilter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10b80) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4908). One of the two will be used. Which one is undefined.
Class APMRawEventData is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10bd0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4958). One of the two will be used. Which one is undefined.
Class APMUserAttribute is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10c20) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a49a8). One of the two will be used. Which one is undefined.
Class APMValue is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10c70) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a49f8). One of the two will be used. Which one is undefined.
Class APMSqliteStore is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10cc0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4a48). One of the two will be used. Which one is undefined.
Class APMASIdentifierManager is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10d10) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4a98). One of the two will be used. Which one is undefined.
Class APMInAppPurchaseProductCache is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10d60) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4ae8). One of the two will be used. Which one is undefined.
Class APMInAppPurchaseItem is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10dd8) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4b60). One of the two will be used. Which one is undefined.
Class APMInAppPurchaseTransactionReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10e00) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4b88). One of the two will be used. Which one is undefined.
Class APMProductsRequest is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10e50) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4bd8). One of the two will be used. Which one is undefined.
Class APMLifetimeValueRecorder is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10ea0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4c28). One of the two will be used. Which one is undefined.
Class APMASLLogger is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10ef0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4c78). One of the two will be used. Which one is undefined.
Class APMMonitoringSampledData is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10f40) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4cc8). One of the two will be used. Which one is undefined.
Class APMUserDefaults is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10f90) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4d18). One of the two will be used. Which one is undefined.
Class APMScreen is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e10fe0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4d68). One of the two will be used. Which one is undefined.
Class APMScreenViewReporter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11030) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4db8). One of the two will be used. Which one is undefined.
Class APMAEU is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11080) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4e08). One of the two will be used. Which one is undefined.
Class APMInfoPlistFileUtil is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e110d0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4e58). One of the two will be used. Which one is undefined.
Class APMKeychainWrapper is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11120) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4ea8). One of the two will be used. Which one is undefined.
Class APMNumericUtil is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11198) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4f20). One of the two will be used. Which one is undefined.
Class APMPBAudience is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e111c0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4f48). One of the two will be used. Which one is undefined.
Class APMPBAudienceLeafFilterResult is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11210) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4f98). One of the two will be used. Which one is undefined.
Class APMPBDynamicFilterResultTimestamp is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11260) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a4fe8). One of the two will be used. Which one is undefined.
Class APMPBEvent is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e112b0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5038). One of the two will be used. Which one is undefined.
Class APMPBEventConfig is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11300) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5088). One of the two will be used. Which one is undefined.
Class APMPBEventFilter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11350) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a50d8). One of the two will be used. Which one is undefined.
Class APMPBEventParam is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e113a0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5128). One of the two will be used. Which one is undefined.
Class APMPBFilter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e113f0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5178). One of the two will be used. Which one is undefined.
Class APMPBMeasurementBatch is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11440) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a51c8). One of the two will be used. Which one is undefined.
Class APMPBMeasurementBundle is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11490) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5218). One of the two will be used. Which one is undefined.
Class APMPBMeasurementConfig is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e114e0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5268). One of the two will be used. Which one is undefined.
Class APMPBNumberFilter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11530) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a52b8). One of the two will be used. Which one is undefined.
Class APMPBPropertyFilter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11580) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5308). One of the two will be used. Which one is undefined.
Class APMPBResultData is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e115d0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5358). One of the two will be used. Which one is undefined.
Class APMPBSequenceFilterResultTimestamp is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11620) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a53a8). One of the two will be used. Which one is undefined.
Class APMPBSetting is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11670) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a53f8). One of the two will be used. Which one is undefined.
Class APMPBStringFilter is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e116c0) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5448). One of the two will be used. Which one is undefined.
Class APMPBUserAttribute is implemented in both /Users/${UserName}/Library/Developer/Xcode/DerivedData/${AppName}/Build/Products/Debug-iphonesimulator/Core.framework/Core (0x107e11710) and /Users/${UserName}/Library/Developer/CoreSimulator/Devices/${ランダム英数}/data/Containers/Bundle/Application/${ランダム英数}/${AppName}.app/${AppName} (0x1061a5498). One of the two will be used. Which one is undefined.</pre>
<h2>原因</h2>
<p>CocoaPodsからFirebase SDKを導入した際のTargetを、本来使わない所に記載していたため、どのTargetを参照して良いのか?な状態となっていました。<br />
これの厄介な所は、アプリは普通に動いてしまうので、どこが問題なのかはログから推測になったので、単純なポカミスだったのに気づきにくい所です。</p>
<h3>もうちょっと詳しく言うと</h3>
<p>私が遭遇した際のケースでもうちょっと深堀りすると、<br />
開発しているアプリのリファクタリングの一貫で、Model部分をEmbedded Framework化して別Targetにしていました。<br />
CocoaPodsのTarget指定したのが、Project本体ではなく、このEmbedded Frameworkの方に指定していたのが問題です。</p>
<p>具体的なPodFileは以下です。</p>
<pre class="code" data-lang="" data-unlink>target '{$Embedded Frameworkの名前}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for {$Embedded Frameworkの名前}
# add the Firebase pod for Google Analytics
pod 'Firebase/Analytics'
pod 'Firebase/Core'
pod 'Firebase/Firestore'
pod 'Firebase/Auth'
# Optionally, include the Swift extensions if you're using Swift.
pod 'FirebaseFirestoreSwift'
# add pods for any other desired Firebase products
# https://firebase.google.com/docs/ios/setup#available-pods
target '{$Embedded Frameworkの名前}Tests' do
# Pods for testing
end
end
target '{$AppName}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
target '{$AppName}Tests' do
inherit! :search_paths
# Pods for testing
end
target '{$AppName}UITests' do
# Pods for testing
end
end</pre>
<p>上記のように</p>
<pre class="code" data-lang="" data-unlink>target '{$Embedded Frameworkの名前}'</pre>
<p>にFirebaseのSDKを記載してしまったのが問題です。</p>
<p>※Embedded Frameworkについては、以下に詳細を記載していますので、ご興味があれば参照ください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Fembedded-framework" title="Embedded Frameworkのメリットと導入と使い方 - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/embedded-framework">www.project-unknown.jp</a></cite></p>
<h2>解決方法</h2>
<p>原因はTarget指定ミスなので、正式な所に記載してpod installすればOKです。<br />
今回は{$AppName}のTargetに指定するのが正しいので、上述PodFileを以下のように修正します。</p>
<pre class="code" data-lang="" data-unlink>target '{$Embedded Frameworkの名前}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for {$Embedded Frameworkの名前}
target '{$Embedded Frameworkの名前}Tests' do
# Pods for testing
end
end
target '{$AppName}' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# add the Firebase pod for Google Analytics
pod 'Firebase/Analytics'
pod 'Firebase/Core'
pod 'Firebase/Firestore'
pod 'Firebase/Auth'
# Optionally, include the Swift extensions if you're using Swift.
pod 'FirebaseFirestoreSwift'
# add pods for any other desired Firebase products
# https://firebase.google.com/docs/ios/setup#available-pods
target '{$AppName}Tests' do
inherit! :search_paths
# Pods for testing
end
target '{$AppName}UITests' do
# Pods for testing
end
end</pre>
<p>最後にpod installを実行します</p>
<pre class="code" data-lang="" data-unlink>$ pod install</pre>
<p>これで問題なく動作するはずです。</p>
project-unknown
NodeのVersion up - インストーラからの更新を行う
hatenablog://entry/26006613509818368
2020-02-11T11:03:55+09:00
2020-02-11T11:04:31+09:00 はじめに FirebaseのHostingを利用しようとした際に、以下のようなエラーが発生。 $ firebase login > Firebase CLI v7.12.1 is incompatible with Node.js v7.3.0 Please upgrade Node.js to version >= 8.0.0 nodeのversionが古いとの事で、nodeのバージョンを上げる必要が出てきたため、version upを行います。 ※本稿ではインストーラからの対応を行います。 ※私の環境はMac(Catalina)ですので、操作は基本Mac上のものです。 公式からダウンロード…
<h2>はじめに</h2>
<p>FirebaseのHostingを利用しようとした際に、以下のようなエラーが発生。</p>
<pre class="code" data-lang="" data-unlink>$ firebase login
> Firebase CLI v7.12.1 is incompatible with Node.js v7.3.0 Please upgrade Node.js to version >= 8.0.0</pre>
<p>nodeのversionが古いとの事で、nodeのバージョンを上げる必要が出てきたため、version upを行います。</p>
<p>※本稿ではインストーラからの対応を行います。<br />
※私の環境はMac(Catalina)ですので、操作は基本Mac上のものです。</p>
<h2>公式からダウンロードして、インストール。</h2>
<p>以下の公式から、最新版を落とします。<br />
何を落として良いかわからない場合は推奨版を落としておきましょう。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnodejs.org%2Fja%2Fdownload%2F" title="ダウンロード | Node.js" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://nodejs.org/ja/download/">nodejs.org</a></cite></p>
<p>ダウンロードができたらインストールを行います。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20200211/20200211110038.png" alt="f:id:project-unknown:20200211110038p:plain" title="f:id:project-unknown:20200211110038p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>インストールが終わったら環境Pathを貼る旨の記載があるので、対応を行います。</p>
<h2>環境Pathを貼る</h2>
<p>bashの場合</p>
<p>bash_profileの編集</p>
<pre class="code" data-lang="" data-unlink>$ vim ~/.bash_profile</pre>
<p>以下を追記</p>
<pre class="code" data-lang="" data-unlink>export PATH=$PATH:/usr/local/bin</pre>
<p>zshの場合
zshrcを編集</p>
<pre class="code" data-lang="" data-unlink>$ vim ~/.zshrc</pre>
<p>以下を追記</p>
<pre class="code" data-lang="" data-unlink>export PATH=$PATH:/usr/local/bin</pre>
<p>bashに入り直してversion確認</p>
<pre class="code" data-lang="" data-unlink>$ node -v
> v12.15.0</pre>
<p>これで最新版を入れることができました。</p>
project-unknown
TIME HACKER Version 1.5.0 共有機能を追加しました!
hatenablog://entry/26006613490043708
2019-12-30T13:22:01+09:00
2019-12-30T13:22:01+09:00 いつもTIME HACKERをご利用いただきありがとうございます。 今回のバージョン1.5.0で以下の対応を行いました。 行動履歴の円グラフの表示単位を分表示可能となりました。 各種レポートに共有機能を追加しました。 その他に、報告をいただいていた不具合の修正を行いました。 行動履歴の円グラフの表示単位を分表示可能となりました。 レビューなどでご要望を頂いておりました、『行動履歴を時間で確認できる』機能を追加しました。 今回のアップデートで初期設定を時間表示にしているので、 これまで同様パーセント表示にするには、以下の手順をお試し下さい。 メイン画面で右下の『メニューボタン』をタップします。 …
<p>いつもTIME HACKERをご利用いただきありがとうございます。</p>
<p>今回のバージョン1.5.0で以下の対応を行いました。</p>
<ul>
<li><p>行動履歴の円グラフの表示単位を分表示可能となりました。</p></li>
<li><p>各種レポートに共有機能を追加しました。</p></li>
<li><p>その他に、報告をいただいていた不具合の修正を行いました。</p></li>
</ul>
<h1>行動履歴の円グラフの表示単位を分表示可能となりました。</h1>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191228/20191228175037.png" alt="f:id:project-unknown:20191228175037p:plain:w500" title="f:id:project-unknown:20191228175037p:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p>
<p>レビューなどでご要望を頂いておりました、『行動履歴を時間で確認できる』機能を追加しました。</p>
<p>今回のアップデートで初期設定を時間表示にしているので、
これまで同様パーセント表示にするには、以下の手順をお試し下さい。</p>
<ol>
<li>メイン画面で右下の『メニューボタン』をタップします。</li>
<li>メニュー画面でレポート設定をタップします。</li>
<li>レポート設定画面で行動履歴の円グラフ表示の単位を『パーセント』を選択して下さい。</li>
</ol>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229214659.png" alt="f:id:project-unknown:20191229214659p:plain:w400" title="f:id:project-unknown:20191229214659p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>時間表示で行動履歴を見ることで新たな気付きがあるかもしれません。
是非ご利用下さい!</p>
<h1>レポート画面で共有機能を追加しました。</h1>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229170609.png" alt="f:id:project-unknown:20191229170609p:plain:w300" title="f:id:project-unknown:20191229170609p:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p>
<p>レポート画面をTwitterやLINEで共有する機能を追加しました。
ご自身のレポート画面を共有することで、</p>
<ul>
<li>行動履歴のグラフを共有することで、自分では気付かなかった隙間時間(スキルアップの為の時間)に気付いたり、単純に行動履歴を日記の様にして楽しんだり</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229230516.png" alt="f:id:project-unknown:20191229230516p:plain:w300" title="f:id:project-unknown:20191229230516p:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p>
<ul>
<li>確保したい時間のレポートのグラフを共有することで、モチベーションが継続したり、目標達成の喜びを分かち合えたり</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229233634.png" alt="f:id:project-unknown:20191229233634p:plain:w250" title="f:id:project-unknown:20191229233634p:plain:w250" class="hatena-fotolife" style="width:250px" itemprop="image"></span></p>
<ul>
<li>各アクションのレポートのグラフを共有することで、日報の様に扱えたり</li>
</ul>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229230640.png" alt="f:id:project-unknown:20191229230640p:plain:w300" title="f:id:project-unknown:20191229230640p:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p>
<p>上記の様に、生活改善にお役に立てると思います。是非ご活用ください!</p>
<p>Twitterを例に共有方法を説明します。</p>
<p>1. 各種レポート画面の右上にある共有ボタンをタップします。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229173234.png" alt="f:id:project-unknown:20191229173234p:plain:w300" title="f:id:project-unknown:20191229173234p:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p>
<p>2. 画面下にアクティビティビューが表示されますので、Twitterを選択して下さい。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229173644.png" alt="f:id:project-unknown:20191229173644p:plain:w300" title="f:id:project-unknown:20191229173644p:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p>
<p>3. ツイート画面が表示されますので、本文を編集してツイートを行って下さい。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191229/20191229175306.png" alt="f:id:project-unknown:20191229175306p:plain:w300" title="f:id:project-unknown:20191229175306p:plain:w300" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p>
<h3>補足1:共有したくないアクションがある場合</h3>
<p>共有したくないアクションがある場合は、
対象のアクションのアクション詳細・編集画面で「非表示にする」を選択して下さい。</p>
<p>今後ともTIME HACKERを是非ご利用ください!</p>
<hr />
<p>まだTIME HACKERをお使いでなければ、是非ともこの機会にTIME HACKERをご利用ください!</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
<hr />
<p><a href="http://www.project-unknown.jp/timehacker/jp/help/index">ヘルプページ</a><br/>
<a href="https://goo.gl/forms/btmuW8EGwQ8TyzzO2">ご意見・ご要望</a><br/>
<a href="http://www.project-unknown.jp/archive/category/TIME%20HACKER">TIME HACKER関連記事</a></p>
project-unknown
iOSのSectionの高さを指定した際に、Section末尾に余計な余白が生まれる時の解決法
hatenablog://entry/26006613486251856
2019-12-18T21:59:10+09:00
2019-12-20T20:19:13+09:00 TIME HACKERの次期開発で、メニューが煩雑になってきたため、グループ化を行おうとした際に掲題の通り、Sectionで余計な余白が生まれてしまって、軽く詰まったので備忘録です。
<h2>はじめに</h2>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>の次期開発で、メニューが煩雑になってきたため、グループ化を行おうとした際に掲題の通り、Sectionで余計な余白が生まれてしまって、軽く詰まったので備忘録です。</p>
<h2>事象</h2>
<p>何も考えず、TableViewのdelegateに以下を指定します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">tableView</span>(_ tableView<span class="synSpecial">:</span> <span class="synType">UITableView</span>, heightForHeaderInSection section<span class="synSpecial">:</span> <span class="synType">Int</span>) <span class="synSpecial">-></span> <span class="synType">CGFloat</span> {
<span class="synStatement">return</span> <span class="synConstant">30</span>
}
</pre>
<p>となると、以下のようになりました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191218/20191218215145.png" alt="f:id:project-unknown:20191218215145p:plain:w400" title="f:id:project-unknown:20191218215145p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>ただ、高さは30pt指定なので、0番目が正解で、それ以外が誤りのようです。
Viewの構成を見てみないとわからないので、</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191218/20191218215339.png" alt="f:id:project-unknown:20191218215339p:plain:w400" title="f:id:project-unknown:20191218215339p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>これを使って確認します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191218/20191218215543.png" alt="f:id:project-unknown:20191218215543p:plain:w400" title="f:id:project-unknown:20191218215543p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>0番目以外に総じてSectionのFooter部分に余計な余白が生まれている事がわかります。</p>
<h2>解決方法</h2>
<p>至って簡単です。<br/>
TableViewの設定に以下を追加します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>tableView.sectionFooterHeight <span class="synIdentifier">=</span> <span class="synConstant">0.0</span>
</pre>
<p>結果は以下の通り</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20191218/20191218215720.png" alt="f:id:project-unknown:20191218215720p:plain:w400" title="f:id:project-unknown:20191218215720p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>これで解決することができました。</p>
project-unknown
KeyHolder Version 2.5.5 軽微な不具合を修正しました
hatenablog://entry/26006613449023699
2019-10-14T10:29:49+09:00
2019-10-14T10:29:49+09:00 いつもKeyHolderをご利用戴き、ありがとうございます。 今回のVersion2.5.5で一部操作でクラッシュする不具合の修正を行いました。 ご利用の皆様にはご不便をおかけして大変申し訳ありませんでした。 また、今回のVersionにて、パスワードが大量に登録された際に、アプリが重くなる事象がありましたので、合わせてパフォーマンスチューニングを行いました。 もし、他にこの不具合を直して欲しい、こんな機能をつけて欲しい等ありましたら、 AppStoreのレビュー このブログ Twitter project.unknown.cs@gmail.com にご連絡いただけますと幸いです。 全てにお答…
<p>いつも<a href="(https://itunes.apple.com/jp/app/keyholder/id975114369?l=en&mt=8:title">KeyHolder</a>をご利用戴き、ありがとうございます。</p>
<p>今回のVersion2.5.5で一部操作でクラッシュする不具合の修正を行いました。</p>
<p>ご利用の皆様にはご不便をおかけして大変申し訳ありませんでした。</p>
<p>また、今回のVersionにて、パスワードが大量に登録された際に、アプリが重くなる事象がありましたので、合わせてパフォーマンスチューニングを行いました。</p>
<p>もし、他にこの不具合を直して欲しい、こんな機能をつけて欲しい等ありましたら、</p>
<ul>
<li>AppStoreのレビュー</li>
<li>このブログ</li>
<li><a href="https://twitter.com/p_j_unknown">Twitter</a></li>
<li>project.unknown.cs@gmail.com</li>
</ul>
<p>にご連絡いただけますと幸いです。<br/>
全てにお答えすることは難しいですが、少しでも皆様の要望を実現していきます。</p>
<p>引き続き、KeyHolderのご愛用のほど宜しくお願い致します!</p>
project-unknown
TIME HACKER Version 1.4.0 バックアップ機能を追加しました!
hatenablog://entry/26006613432784020
2019-09-13T22:18:52+09:00
2019-09-13T22:42:58+09:00 いつもTIME HACKERをご利用いただきありがとうございます。
今回のバージョン1.4.0で以下の対応を行いました。
* バックアップ機能の追加を行いました!
* データを他の端末に移すことが可能になりました。
* その他に、報告をいただいていた不具合の修正を行いました。
<p>いつもTIME HACKERをご利用いただきありがとうございます。</p>
<p>今回のバージョン1.4.0で以下の対応を行いました。</p>
<ul>
<li><p>バックアップ機能の追加を行いました!</p></li>
<li><p>データを他の端末に移すことが可能になりました。</p></li>
<li><p>その他に、報告をいただいていた不具合の修正を行いました。</p></li>
</ul>
<h1>バックアップ機能の追加を行いました!</h1>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190913/20190913210259.png" alt="f:id:project-unknown:20190913210259p:plain:w500" title="f:id:project-unknown:20190913210259p:plain:w500" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p>
<p>TIME HACKERにバックアップ機能を追加しました!</p>
<p>このバックアップ機能はiCloud上にバックアップを作る手法を採用しています。</p>
<p>そのため、Apple IDにログインするだけでご利用いただけます。</p>
<p>バージョンアップを行なったら、<br>
ぜひ一度バックアップを行い、みなさまの大切な記録の保護してください。</p>
<p>詳細は<a href="http://www.project-unknown.jp/timehacker/jp/help/index#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97%E5%BE%A9%E5%85%83%E6%A9%9F%E8%83%BD">ヘルプページ</a>をご確認ください。</p>
<h1>データを他の端末に移すことが可能になりました。</h1>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190909/20190909213844.png" alt="f:id:project-unknown:20190909213844p:plain:w600" title="f:id:project-unknown:20190909213844p:plain:w600" class="hatena-fotolife" style="width:600px" itemprop="image"></span></p>
<p>バックアップ機能を応用し、データを他の端末に移すことが可能になりました。</p>
<p>大まかなやり方は以下の通りです。</p>
<ol>
<li>Apple IDにログインし利用中の端末で「iCloudにバックアップ」を行います。</li>
<li>新たに利用したい端末でバックアップしたときに利用したApple IDでサインインします。</li>
<li>TIME HACKERをインストールし、「iCloudから復元」を行います。</li>
</ol>
<p>機種変更などにご活用頂ければ幸いです。</p>
<p>詳しくは<a href="http://www.project-unknown.jp/timehacker/jp/help/index#%E3%83%90%E3%83%83%E3%82%AF%E3%82%A2%E3%83%83%E3%83%97%E5%BE%A9%E5%85%83%E6%A9%9F%E8%83%BD">ヘルプページ</a>をご覧ください。</p>
<p>今後ともTIME HACKERを是非ご利用ください!</p>
<hr />
<p>まだTIME HACKERをお使いでなければ、是非ともこの機会にTIME HACKERをご利用ください!</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
<hr />
<p><a href="http://www.project-unknown.jp/timehacker/jp/help/index">ヘルプページ</a><br/>
<a href="https://goo.gl/forms/btmuW8EGwQ8TyzzO2">ご意見・ご要望</a><br/>
<a href="http://www.project-unknown.jp/archive/category/TIME%20HACKER">TIME HACKER関連記事</a></p>
project-unknown
Embedded Frameworkのメリットと導入と使い方
hatenablog://entry/26006613412961406
2019-08-31T20:42:31+09:00
2019-09-17T12:32:18+09:00 Embedded Framework Embedded Frameworkは平たく言うと、アプリのコードを分割してFrameworkとして利用する事ができる機能です。 XCode8からの機能なので話題としては古いのですが、丁度アプリのリファクタリングを行っている際に、Embedded Frameworkを用いたので記事として記載します。 Embedded Frameworkのメリット Embedded Frameworkを導入する事で主に以下の恩恵を授かることができます。 差分コンパイルされる為、ビルドパフォーマンスが向上 よく語られるメリットです。 中規模以上のアプリですと、ビルドパフォーマ…
<h2>Embedded Framework</h2>
<p>Embedded Frameworkは平たく言うと、アプリのコードを分割してFrameworkとして利用する事ができる機能です。</p>
<p>XCode8からの機能なので話題としては古いのですが、丁度アプリのリファクタリングを行っている際に、Embedded Frameworkを用いたので記事として記載します。</p>
<h2>Embedded Frameworkのメリット</h2>
<p>Embedded Frameworkを導入する事で主に以下の恩恵を授かることができます。</p>
<p><strong>差分コンパイルされる為、ビルドパフォーマンスが向上</strong></p>
<p>よく語られるメリットです。<br/>
中規模以上のアプリですと、ビルドパフォーマンスが悪く、生産的ではありません。<br/>
劇的に…とまではいかないかもしれませんが、結構馬鹿にならないコスト軽減が見込めます。</p>
<p><strong>WidgetやAppleWatch等、App Extentions間でコードを共有する事ができる</strong></p>
<p>一応Targetを指定する事で、コード共有はできますが、Target指定の場合だと <strong>ファイルが複製され純粋に容量が増えます</strong> 。
なので、メインTarget以外でコードを共有する場合は、Embedded Frameworkをおすすめします。</p>
<p><strong>依存関係が強制される為、後々の設計がきれいになる、依存を意識した製造を行える</strong></p>
<p>製造中に気をつけることができれば、わざわざEmbedded Framework化しなくても良いのですが、気が緩むとどうしても綻びが生じ、気づいた頃にはリファクタリングに多大な時間を消費する事態を招きかねません。</p>
<p>なので、色々と人によってベストプラクティスは異なりますが、私としてはある程度まとまったモジュール単位でEmbedded Framework化してしまった方が良いと考えています。</p>
<p><strong>テスタブルなコードが書きやすい</strong></p>
<p>上記依存関係の話題に近い話ですが、やはり依存関係を意識した設計になるので、Embedded Frameworkを使わないケースと比べ、テスタブルなコード設計がやりやすい(その方向に製造をすすめることができると言いますか)です。</p>
<p>ただ、個人的にはアプリケーションの場合は、UTをCIに乗せるところにそこまで強い恩恵を感じていないため、UT事態をコアロジック部分くらいしか書かないので、強いメリットには感じていません(が、業務ロジックの場合だとそうも言っていられないので、やはりメリットですね)</p>
<h2>Embedded Frameworkの導入方法</h2>
<p>XCodeのFile -> New -> Targetから作成できます。</p>
<p>Framework & LibraryからCocoa Touch Frameworkを選択します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190831/20190831201928.png" alt="f:id:project-unknown:20190831201928p:plain" title="f:id:project-unknown:20190831201928p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>その後、必要項目を入力する事で、新規にTargetが追加されます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190831/20190831202047.png" alt="f:id:project-unknown:20190831202047p:plain" title="f:id:project-unknown:20190831202047p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<h2>Embedded Frameworkの使い方</h2>
<p>Targetが追加されたのと同時にProject Navigatorに該当グループができているので、その下にコードを追加していくだけです。</p>
<p>注意点としては、他モジュールからEmbedded Framework化されたモジュールへアクセスする場合は、publicでのアクセスになります。<br/>
(特にclass等にアクセス修飾子を指定シなかった場合はinternalになります)</p>
<h3>Embedded FrameworkでCarthageを使う</h3>
<p>Cartfieでinstallする所まではこれまでと同じなのですが、Embedded Framework内でしかCarthage経由でinstallしたライブラリを利用していなくても、メインTargetにも指定してあげる必要があります。</p>
<p>■メインTargetの設定例<br/>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190831/20190831203254.png" alt="f:id:project-unknown:20190831203254p:plain" title="f:id:project-unknown:20190831203254p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<h3>Embedded FrameworkでCocoa Podsを使う</h3>
<p>Carthageと同様に、Cocoa Podsの場合でもメインTarget、Embedded Framework共にライブラリのinstallが必要です。</p>
<p>特にこだわりが無ければabstract_targetにまとめるで良いでしょう。</p>
<pre class="code" data-lang="" data-unlink>abstract_target 'hoge' do
pod 'Alamofire'
# AlamofireとFirebaseがProjectUnknownTools Targetにinstall
target 'ProjectUnknownTools' do
pod 'Firebase/Core'
end
target 'API' do # AlamofireのみAPI Targetにinstall
end
end</pre>
<h2>Embedded Frameworkでの申請</h2>
<p>Embedded Frameworkで申請するときに、以下のようなエラーが発生するかもしれません。</p>
<pre class="code" data-lang="" data-unlink>Code Signing Error: {Framework Name} has conflicting provisioning settings. ProjectUnknownCommon is automatically signed, but code signing identity iPhone Distribution: {Team Name} has been manually specified. Set the code signing identity value to "iPhone Developer" in the build settings editor, or switch to manual signing in the project editor.</pre>
<p>この場合、Projectの設定で、</p>
<ul>
<li>Provisioning ProfileをAutomatic</li>
<li>Code Signing IdentityのDebugを iOS Developer</li>
<li>Code Signing IdentityのReleaseを iPhone Distribution</li>
</ul>
<p>上記で設定すれば問題ないはずです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190917/20190917123106.png" alt="f:id:project-unknown:20190917123106p:plain" title="f:id:project-unknown:20190917123106p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190917/20190917123053.png" alt="f:id:project-unknown:20190917123053p:plain" title="f:id:project-unknown:20190917123053p:plain" class="hatena-fotolife" itemprop="image"></span></p>
project-unknown
Type "xxx" does not confirm to protocol 'NSObjectProtocol'エラーの対処法
hatenablog://entry/26006613378946796
2019-07-28T16:51:27+09:00
2019-08-31T19:40:43+09:00 はじめに タイトル通りのエラーが出た時の対処法です。 例えば、Objective-Cのライブラリで、以下のようなプロトコルがあり、 @protocol Hogehoge <NSObject> これを宣言しようとした際、 class Fuga: Hogehoge 以下のようなエラーが出ます. Type "Fuga" does not confirm to protocol 'NSObjectProtocol' これは、Hogehoge自体がNSObjectを継承したものなので、Fugaにその定義が無いとエラーを起こしているのですが、XCodeの補助機能で、足りていない定義を追加しようとした場合、…
<h2>はじめに</h2>
<p>タイトル通りのエラーが出た時の対処法です。</p>
<p>例えば、Objective-Cのライブラリで、以下のようなプロトコルがあり、</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synType">@protocol</span> Hogehoge <span class="synIdentifier"><</span>NSObject<span class="synIdentifier">></span>
</pre>
<p>これを宣言しようとした際、</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synType">Fuga</span><span class="synSpecial">:</span> <span class="synType">Hogehoge</span>
</pre>
<p>以下のようなエラーが出ます.</p>
<pre class="code" data-lang="" data-unlink>Type "Fuga" does not confirm to protocol 'NSObjectProtocol'</pre>
<p>これは、Hogehoge自体がNSObjectを継承したものなので、Fugaにその定義が無いとエラーを起こしているのですが、XCodeの補助機能で、足りていない定義を追加しようとした場合、下記のようになってしまいます。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">func</span> <span class="synIdentifier">isEqual</span>(_ object<span class="synSpecial">:</span> <span class="synType">Any</span>?) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">var</span> <span class="synIdentifier">hash</span><span class="synSpecial">:</span> <span class="synType">Int</span>
<span class="synPreProc">var</span> <span class="synIdentifier">superclass</span><span class="synSpecial">:</span> <span class="synType">AnyClass</span>?
<span class="synPreProc">func</span> `<span class="synIdentifier">self</span>`() <span class="synSpecial">-></span> <span class="synType">Self</span> {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">perform</span>(_ aSelector<span class="synSpecial">:</span> <span class="synType">Selector</span><span class="synIdentifier">!</span>) <span class="synSpecial">-></span> <span class="synType">Unmanaged</span><span class="synSpecial"><AnyObject></span>! {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">perform</span>(_ aSelector<span class="synSpecial">:</span> <span class="synType">Selector</span><span class="synIdentifier">!</span>, with object<span class="synSpecial">:</span> <span class="synType">Any</span><span class="synIdentifier">!</span>) <span class="synSpecial">-></span> <span class="synType">Unmanaged</span><span class="synSpecial"><AnyObject></span>! {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">perform</span>(_ aSelector<span class="synSpecial">:</span> <span class="synType">Selector</span><span class="synIdentifier">!</span>, with object1<span class="synSpecial">:</span> <span class="synType">Any</span><span class="synIdentifier">!</span>, with object2<span class="synSpecial">:</span> <span class="synType">Any</span><span class="synIdentifier">!</span>) <span class="synSpecial">-></span> <span class="synType">Unmanaged</span><span class="synSpecial"><AnyObject></span>! {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">isProxy</span>() <span class="synSpecial">-></span> <span class="synType">Bool</span> {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">isKind</span>(of aClass<span class="synSpecial">:</span> <span class="synType">AnyClass</span>) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">isMember</span>(of aClass<span class="synSpecial">:</span> <span class="synType">AnyClass</span>) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">conforms</span>(to aProtocol<span class="synSpecial">:</span> <span class="synType">Protocol</span>) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">func</span> <span class="synIdentifier">responds</span>(to aSelector<span class="synSpecial">:</span> <span class="synType">Selector</span><span class="synIdentifier">!</span>) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
<span class="synIdentifier"><</span>#code#<span class="synIdentifier">></span>
}
<span class="synPreProc">var</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synType">String</span>
</pre>
<h2>解決方法</h2>
<p>これを埋めても良いのですが・・・、<br/>
単純にプロトコルを継承するだけなのと、NSObjectが使えれば良いだけなので、</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synType">Fuga</span><span class="synSpecial">:</span> <span class="synType">NSObject</span>, Hogehoge
</pre>
<p>と、NSObjectを継承しておけば解決します。</p>
<p>久々にiOS開発してここで詰まってしまった為、備忘録的な記事でした。</p>
project-unknown
TIME HACKER Version 1.3.0 新規アイコンを追加しました
hatenablog://entry/17680117127201178575
2019-06-23T08:42:45+09:00
2019-06-23T08:42:45+09:00 いつもTIME HACKERをご利用いただきありがとうございます。 今回のバージョン1.3.0で以下の対応を行いました。 ご要望のあったアイコン、並びに新規のアイコンを追加しました ご要望のあったアイコン、並びに新規のアイコンを追加しました 今回はご要望のあったアイコンに加え、Project.Unknownとしておすすめしたい新規のアイコンを追加しました。 交通系アイコンの追加 ペット系アイコンの追加 家事系アイコンの追加 また、お店のアイコンが分かりづらかったので、一部修正を行っております。 是非ご利用ください! まだTIME HACKERをお使いでなければ、是非ともこの機会にTIME HA…
<p>いつもTIME HACKERをご利用いただきありがとうございます。</p>
<p>今回のバージョン1.3.0で以下の対応を行いました。</p>
<ul>
<li>ご要望のあったアイコン、並びに新規のアイコンを追加しました</li>
</ul>
<h1>ご要望のあったアイコン、並びに新規のアイコンを追加しました</h1>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190616/20190616153117.png" alt="f:id:project-unknown:20190616153117p:plain" title="f:id:project-unknown:20190616153117p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>今回はご要望のあったアイコンに加え、Project.Unknownとしておすすめしたい新規のアイコンを追加しました。</p>
<ul>
<li>交通系アイコンの追加</li>
<li>ペット系アイコンの追加</li>
<li>家事系アイコンの追加</li>
</ul>
<p>また、お店のアイコンが分かりづらかったので、一部修正を行っております。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190616/20190616185447.png" alt="f:id:project-unknown:20190616185447p:plain" title="f:id:project-unknown:20190616185447p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>是非ご利用ください!</p>
<hr />
<p>まだTIME HACKERをお使いでなければ、是非ともこの機会にTIME HACKERをご利用ください!</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
<hr />
<p><a href="http://www.project-unknown.jp/timehacker/jp/help/index">ヘルプページ</a><br/>
<a href="https://goo.gl/forms/btmuW8EGwQ8TyzzO2">ご意見・ご要望</a><br/>
<a href="http://www.project-unknown.jp/archive/category/TIME%20HACKER">TIME HACKER関連記事</a></p>
project-unknown
あんのう〜のん!- LINEクリエイターズスタンプ
hatenablog://entry/17680117127201460302
2019-06-17T00:25:08+09:00
2019-06-17T00:37:47+09:00 ついに念願のProject.UnknownのLINEクリエイターズスタンプを無事世に送り出すことができました! Projectのマスコット担当、う〜のんのスタンプです。 使いやすい日常系、ちょっぴりクスっとシュールなもの、お相手や自分が仕事中に使えそうなものとたくさんありますので、もし気に入っていただけたなら自由に使ってもらえるととても嬉しいです^^ store.line.me
<p>ついに念願のProject.UnknownのLINEクリエイターズスタンプを無事世に送り出すことができました!<br/>
Projectのマスコット担当、う〜のんのスタンプです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190617/20190617002110.png" alt="f:id:project-unknown:20190617002110p:plain" title="f:id:project-unknown:20190617002110p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>使いやすい日常系、ちょっぴりクスっとシュールなもの、お相手や自分が仕事中に使えそうなものとたくさんありますので、もし気に入っていただけたなら自由に使ってもらえるととても嬉しいです^^</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fstore.line.me%2Fstickershop%2Fproduct%2F7884508%2Fja" title="あんのう〜のん! - LINE スタンプ | LINE STORE" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://store.line.me/stickershop/product/7884508/ja">store.line.me</a></cite></p>
project-unknown
FirebaseUIの認証を使ってドハマリした件 - Cannot read property 'length' of undefined Dismiss
hatenablog://entry/17680117127170581979
2019-05-28T10:58:07+09:00
2019-05-28T10:58:07+09:00 はじめに Project.Unknown内で利用するツールをFirebaseで行おうと考え、PJ内だけで利用したいため、折角なのでFirebase Authenticationを利用して認証されたメンバーだけ閲覧できるページを作ろうと、以下のモジュールをFirebaseUIで実装しました。 この時にドハマリしたのが、Sign inしようとしたら、以下のエラーになった件です。 Cannot read property 'length' of undefined Dismiss 処理云々はFirebaseUIにまかせているので、何故こうなったのかさっぱりわからず、丸1日消費してしまいました。 解決…
<h2>はじめに</h2>
<p>Project.Unknown内で利用するツールをFirebaseで行おうと考え、PJ内だけで利用したいため、折角なのでFirebase Authenticationを利用して認証されたメンバーだけ閲覧できるページを作ろうと、以下のモジュールをFirebaseUIで実装しました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190528/20190528105232.png" alt="f:id:project-unknown:20190528105232p:plain" title="f:id:project-unknown:20190528105232p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>この時にドハマリしたのが、Sign inしようとしたら、以下のエラーになった件です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190528/20190528105007.png" alt="f:id:project-unknown:20190528105007p:plain" title="f:id:project-unknown:20190528105007p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<pre class="code" data-lang="" data-unlink>Cannot read property 'length' of undefined Dismiss</pre>
<p>処理云々はFirebaseUIにまかせているので、何故こうなったのかさっぱりわからず、丸1日消費してしまいました。</p>
<h2>解決法</h2>
<p>いろいろ模索したのですが、firebase.jsを最新にする事で解決することができました。</p>
<p>before</p>
<pre class="code" data-lang="" data-unlink><script src="https://www.gstatic.com/firebasejs/5.9.1/firebase.js"></script></pre>
<p>after</p>
<pre class="code" data-lang="" data-unlink><script src="https://www.gstatic.com/firebasejs/6.0.2/firebase.js"></script></pre>
<p>それ以外だと、2019/5/28現在、以下のFirebaseUIを参照しています。</p>
<pre class="code" data-lang="" data-unlink><script src="https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.js"></script>
<link type="text/css" rel="stylesheet" href="https://cdn.firebase.com/libs/firebaseui/4.0.0/firebaseui.css" /></pre>
<p>また、最新のSDKの情報は以下から確認できます。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffirebase.google.com%2Fdocs%2Freference%2Fjs" title="JavaScript SDK | Firebase" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://firebase.google.com/docs/reference/js">firebase.google.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ffirebase%2Ffirebaseui-web%2Freleases" title="firebase/firebaseui-web" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/firebase/firebaseui-web/releases">github.com</a></cite></p>
project-unknown
KeyHolder データのバックアップ、機種変更について
hatenablog://entry/17680117127086603160
2019-04-28T14:09:32+09:00
2019-10-01T08:28:37+09:00 はじめに いつもKeyHolderをご利用いただきありがとうございます。 よくお問い合わせをいただきます。KeyHolderのバックアップ・機種変更時のデータの移行についての記事です。 まず、KeyHolderではデータのバックアップ・機種変更について、現在提供しておりません。 ですが、以下ご紹介する方法で、データのバックアップや機種変更時のデータの移行を行うことができます。 バックアップ, 機種変時の移行の方法 方法1 iTunes, iCloudのバックアップ・復元機能 これは、Appleが提供しているiPhoneのバックアップ・復元の機能です。 詳細なやりかたは以下の記事を参考にしてくだ…
<h2>はじめに</h2>
<p>いつもKeyHolderをご利用いただきありがとうございます。<br/>
よくお問い合わせをいただきます。KeyHolderのバックアップ・機種変更時のデータの移行についての記事です。</p>
<p>まず、KeyHolderではデータのバックアップ・機種変更について、<strong>現在提供しておりません</strong>。</p>
<p>ですが、以下ご紹介する方法で、データのバックアップや機種変更時のデータの移行を行うことができます。</p>
<h2>バックアップ, 機種変時の移行の方法</h2>
<h3>方法1 iTunes, iCloudのバックアップ・復元機能</h3>
<p>これは、Appleが提供しているiPhoneのバックアップ・復元の機能です。<br/>
詳細なやりかたは以下の記事を参考にしてください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsupport.apple.com%2Fja-jp%2FHT204184" title="iPhone、iPad、iPod touch をバックアップから復元する" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://support.apple.com/ja-jp/HT204184">support.apple.com</a></cite></p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsupport.apple.com%2Fkb%2Fph12521%3Flocale%3Dja_JP" title="iCloud: iCloudバックアップからiOSデバイスを復元またはセットアップ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://support.apple.com/kb/ph12521?locale=ja_JP">support.apple.com</a></cite></p>
<p>このやり方を行うことで、バックアップ・機種変更時のデータの移行も行う事ができます。<br/>
Apple公式の機能ですので、安全性が高いので、こちらのやり方をおすすめしております。</p>
<p><strong>注意点</strong><br/>
この機能を使ってにバックアップしたデータの復元機能を使うと、バックアップを取った時点に復元されます。</p>
<p>例えば、機種変更時にKeyHolderのデータを移行したい時に、本機能を利用すると、<strong>他のアプリやアプリのデータも一緒に移行されます</strong>。<br/>
まっさらな状態から利用したい時は、余分なデータが入り込みますので、適宜削除してください。</p>
<h3>方法2 手動で行う</h3>
<p>ものすごいださい方法で申し訳ありませんが、上記iTunes, iCloudのバックアップを行わない場合は、手動で移していただくしかありません。</p>
<h2>今後の方針</h2>
<p>バックアップ機能を提供するには、主にコストの面でご提供できておりません。<br/>
広告収益に頼っている以上、限界がある為です。<br/>
しかし、一番ニーズがある機能ですので、有料化提供などで実現するなどを検討に入れてます。
が、対応に時間が掛かることについてはご容赦ください。</p>
project-unknown
KeyHolder Version 2.5.3 一部デザイン、不具合修正並びに、ご指摘内容の対応を行いました
hatenablog://entry/17680117126997142275
2019-03-23T13:48:11+09:00
2019-09-08T12:50:04+09:00 いつもKeyHolderをご利用戴き、ありがとうございます。 今回のアップデートで、以下の対応を行いました。 アプリアイコンを刷新 Widgetから起動できない 指定時間経過していないのに、別アプリから戻って来るとパスコード画面が表示される アプリアイコンの刷新 リリースから一度も変えて来てなかったのですが、 気分を一新させる意味で、アイコンを変更しました。 現状、どのアイコンがBestなのか?を模索している段階ですので、前のほうが良かったや、他のアイコンが良いなどのご要望がありましたら是非ご連絡ください! Widgetから起動できない こちらストアのレビューで度々ご指摘されて、なかなか対応で…
<p>いつも<a href="(https://itunes.apple.com/jp/app/keyholder/id975114369?l=en&mt=8:title">KeyHolder</a>をご利用戴き、ありがとうございます。</p>
<p>今回のアップデートで、以下の対応を行いました。</p>
<ul>
<li>アプリアイコンを刷新</li>
<li>Widgetから起動できない</li>
<li>指定時間経過していないのに、別アプリから戻って来るとパスコード画面が表示される</li>
</ul>
<h2>アプリアイコンの刷新</h2>
<p>リリースから一度も変えて来てなかったのですが、<br/>
気分を一新させる意味で、アイコンを変更しました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190321/20190321180030.png" alt="f:id:project-unknown:20190321180030p:plain" title="f:id:project-unknown:20190321180030p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>現状、どのアイコンがBestなのか?を模索している段階ですので、前のほうが良かったや、他のアイコンが良いなどのご要望がありましたら是非ご連絡ください!</p>
<h2>Widgetから起動できない</h2>
<p>こちらストアのレビューで度々ご指摘されて、なかなか対応できておらず申し訳ありません。<br/>
手元での再現性を確認することができず、対応にすごく時間がかかってしまいました。<br/>
ただ、手元で再現性が確実に取れている訳では無く、不具合に対して「これが効きそうだ」の推測対応を行っている為、再現する可能性もあります。<br/>
もし、お手元で再現された場合は、詳細情報をご連絡いただけますと幸いです。</p>
<h2>指定時間経過していないのに、別アプリから戻って来るとパスコード画面が表示される</h2>
<p>こちらも度々ご指摘を頂いていたのにかかわらず、対応が遅くなり申し訳ありません。<br/>
ロック時間の計測方法を変更する事で、これまでよりもユーザ体験が良くなったかと思います。</p>
<h2>最後に</h2>
<p>もし、他にこの不具合を直して欲しい、こんな機能をつけて欲しい等ありましたら、</p>
<ul>
<li>AppStoreのレビュー</li>
<li>このブログ</li>
<li><a href="https://twitter.com/p_j_unknown">Twitter</a></li>
<li>project.unknown.cs@gmail.com</li>
</ul>
<p>にご連絡いただけますと幸いです。<br/>
全てにお答えすることは難しいですが、少しでも皆様の要望を実現していきます。</p>
<p>引き続き、KeyHolderのご愛用のほど宜しくお願い致します!</p>
project-unknown
TIME HACKER Version 1.2.0 Apple Watch対応を行いました
hatenablog://entry/17680117126990654809
2019-03-23T08:43:14+09:00
2019-03-23T08:43:14+09:00 いつもTIME HACKERをご利用いただきありがとうございます。 今回のバージョン1.2.0で以下の対応を行いました。 Apple Watchの対応 一部不具合の修正 パフォーマンス向上対応 Apple Watchの対応 トップ画面 計測中の画面 もちろん、Apple Watchで計測したデータはiPhone上に記録されますし、計測をiPhone上のアプリ、Widgetで終了する事もできます。 (逆もまたしかりです) わざわざiPhoneを開かなくても簡単に行動を記録できるようになった世界観になっております。 是非お使いくださいませ! まだTIME HACKERをお使いでなければ、是非ともこ…
<p>いつもTIME HACKERをご利用いただきありがとうございます。</p>
<p>今回のバージョン1.2.0で以下の対応を行いました。</p>
<ul>
<li>Apple Watchの対応</li>
<li>一部不具合の修正</li>
<li>パフォーマンス向上対応</li>
</ul>
<h2>Apple Watchの対応</h2>
<p>トップ画面<br/>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190309/20190309232306.png" alt="f:id:project-unknown:20190309232306p:plain" title="f:id:project-unknown:20190309232306p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>計測中の画面<br/>
<span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190309/20190309232314.png" alt="f:id:project-unknown:20190309232314p:plain" title="f:id:project-unknown:20190309232314p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>もちろん、Apple Watchで計測したデータはiPhone上に記録されますし、計測をiPhone上のアプリ、Widgetで終了する事もできます。<br/>
(逆もまたしかりです)</p>
<p>わざわざiPhoneを開かなくても簡単に行動を記録できるようになった世界観になっております。<br/>
是非お使いくださいませ!</p>
<hr />
<p>まだTIME HACKERをお使いでなければ、是非ともこの機会にTIME HACKERをご利用ください!</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
<hr />
<p><a href="http://www.project-unknown.jp/timehacker/jp/help/index">ヘルプページ</a><br/>
<a href="https://goo.gl/forms/btmuW8EGwQ8TyzzO2">ご意見・ご要望</a><br/>
<a href="http://www.project-unknown.jp/archive/category/TIME%20HACKER">TIME HACKER関連記事</a></p>
project-unknown
Apple Watch対応アプリ申請でエラーになった時の対応 - Invalid Swift Support
hatenablog://entry/17680117126996174899
2019-03-19T21:27:21+09:00
2019-08-31T19:39:48+09:00 Apple Watch対応アプリを申請しようとしたらエラーになった TIME HACKERのApple Watch対応したものを申請しようとした際に、Upload後メールで以下のエラーを受け取りました。 Dear Developer, We identified one or more issues with a recent delivery for your app, "TIME HACKER - Time Tracker". Please correct the following issues, then upload again. Invalid Swift Support - Th…
<h2>Apple Watch対応アプリを申請しようとしたらエラーになった</h2>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>のApple Watch対応したものを申請しようとした際に、Upload後メールで以下のエラーを受け取りました。</p>
<blockquote><p>Dear Developer,</p>
<p>We identified one or more issues with a recent delivery for your app, "TIME HACKER - Time Tracker". Please correct the following issues, then upload again.</p>
<p>Invalid Swift Support - The watchOS application has Swift libraries at both {Payloadに含まれているPath} . Remove all of the Swift libraries from one of the locations and resubmit your app.</p>
<p>Best regards,</p>
<p>The App Store Team</p></blockquote>
<p>今回はこの対応方法です。</p>
<h2>原因</h2>
<p>エラー内容に記載されている通り、Watch Kit にはSwiftのライブラリを梱包しては行けないもので、何も設定をいじっていない場合は大抵梱包されてるかと思われます。<br/>
(Watch Kitに含めては行けないのであって、Watch Kit Extensionのロジック部分はもちろん含めて問題ありません)</p>
<h2>対応</h2>
<p>以下のキャプチャのように、Watch Kit側の「Always Embed Swift Standard Libraries」をNoにします。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190319/20190319212405.png" alt="f:id:project-unknown:20190319212405p:plain" title="f:id:project-unknown:20190319212405p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>繰り返しますが、Watch Kit Extension側の設定は不要です。</p>
<h2>最後に</h2>
<p>宣伝となりますが、この記事を記載中はまだリリースできていませんが、<br/>
近日中に<a href="https://itunes.apple.com/jp/app/time-hacker-%E6%99%82%E9%96%93%E7%AE%A1%E7%90%86/id1381203243?mt=8">TIME HACKER</a>にApple Watch対応版をリリースする予定です。</p>
<p>まだインストールされていない方は是非お使いください!</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
project-unknown
Apple Developer Programの更新 2019年ver
hatenablog://entry/17680117126978006821
2019-02-23T16:00:42+09:00
2020-03-01T19:59:00+09:00 iOSアプリをリリースする等の場合は、Apple Developer Programの登録(有料)が必要です。 また、このProgramは年に1度更新が必要となるのに加え、1年に1度の作業のため、毎回思い出しながら手探りで更新作業を行っていたので、備忘録として更新手続きを残しておきます。 30日前にもうすぐ有効期限が切れるメールが届くので開く Apple Developer Programで更新作業を進める 30日前にもうすぐ有効期限が切れるメールが届くので開く 件名が「Your Apple Developer Program membership expires in 30 days.」で、…
<p>iOSアプリをリリースする等の場合は、Apple Developer Programの登録(有料)が必要です。</p>
<p>また、このProgramは年に1度更新が必要となるのに加え、1年に1度の作業のため、毎回思い出しながら手探りで更新作業を行っていたので、備忘録として更新手続きを残しておきます。</p>
<ul class="table-of-contents">
<li><a href="#30日前にもうすぐ有効期限が切れるメールが届くので開く">30日前にもうすぐ有効期限が切れるメールが届くので開く</a></li>
<li><a href="#Apple-Developer-Programで更新作業を進める">Apple Developer Programで更新作業を進める</a></li>
</ul>
<h2 id="30日前にもうすぐ有効期限が切れるメールが届くので開く">30日前にもうすぐ有効期限が切れるメールが届くので開く</h2>
<p>件名が「Your Apple Developer Program membership expires in 30 days.」で、下のようなメールが届きますので、「Renew now」をクリックします。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190223/20190223154023.png" alt="f:id:project-unknown:20190223154023p:plain" title="f:id:project-unknown:20190223154023p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<h2 id="Apple-Developer-Programで更新作業を進める">Apple Developer Programで更新作業を進める</h2>
<p>Apple Developer Programが表示されますので、「Renew Membership」をクリックします。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190223/20190223154628.png" alt="f:id:project-unknown:20190223154628p:plain" title="f:id:project-unknown:20190223154628p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>サインインします。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190223/20190223154726.png" alt="f:id:project-unknown:20190223154726p:plain" title="f:id:project-unknown:20190223154726p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>支払い画面が表示されますので、必要情報を入力します。<br />
金額は、2019/2/23現段階で12,744ですが、ドル換算お金額となるため、日々価格が変動します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190223/20190223155114.png" alt="f:id:project-unknown:20190223155114p:plain" title="f:id:project-unknown:20190223155114p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>購入が完了したら、以下の画面が表示されます。<br />
ここでは購入が完了しただけなので、まだ更新(アクティベート)はされていません。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20190223/20190223155721.png" alt="f:id:project-unknown:20190223155721p:plain" title="f:id:project-unknown:20190223155721p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>しばらく待つと、アクティベートに関するメールが届きます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20200301/20200301195835.png" alt="f:id:project-unknown:20200301195835p:plain" title="f:id:project-unknown:20200301195835p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>後は適宜、Provisioning profileの更新などを進めましょう。</p>
project-unknown
iOSでインジケータのようなくるくる回るアニメーションを造る - CGAffineTransform
hatenablog://entry/10257846132663879374
2018-11-03T20:18:43+09:00
2019-08-31T19:41:13+09:00 はじめに インジケータを自作する方法です。 標準で搭載されているものでも十分事足りるのですが、アプリの色を出したいときにインジケータの演出もこだわりたいですよね。 提供中のTIME HACKERにも自作のインジケータを導入した方ので、以下のキャプチャのようなものを自作しました。 今日は、その際のやりかたです。 パラパラ漫画のような作り方でも良いのですが、上記キャプチャを見てもらえばわかるように、一部だけエンドレスに回転させる程度なら、CGAffineTransformを利用します。 CGAffineTransform CGAffineTransformは、UIViewの拡大・縮小・回転などを実…
<h2>はじめに</h2>
<p>インジケータを自作する方法です。<br/>
標準で搭載されているものでも十分事足りるのですが、アプリの色を出したいときにインジケータの演出もこだわりたいですよね。</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">提供中のTIME HACKER</a>にも自作のインジケータを導入した方ので、以下のキャプチャのようなものを自作しました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20181103/20181103200007.gif" alt="f:id:project-unknown:20181103200007g:plain" title="f:id:project-unknown:20181103200007g:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>今日は、その際のやりかたです。<br/>
パラパラ漫画のような作り方でも良いのですが、上記キャプチャを見てもらえばわかるように、一部だけエンドレスに回転させる程度なら、CGAffineTransformを利用します。</p>
<h2>CGAffineTransform</h2>
<p>CGAffineTransformは、UIViewの拡大・縮小・回転などを実装するのをサポートしています。</p>
<p>これを用いて、回転するUIViewを作ります。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>
<span class="synPreProc">class</span> <span class="synType">IndicatorView</span><span class="synSpecial">:</span> <span class="synType">UIView</span> {
<span class="synType">@IBOutlet</span> weak <span class="synPreProc">var</span> <span class="synIdentifier">indicatorPin</span><span class="synSpecial">:</span> <span class="synType">UIImageView</span><span class="synIdentifier">!</span>
<span class="synPreProc">func</span> <span class="synIdentifier">start</span>() {
UIView.animate(withDuration<span class="synSpecial">:</span> <span class="synConstant">1.0</span>, delay<span class="synSpecial">:</span> <span class="synConstant">0.0</span>, options<span class="synSpecial">:</span> .curveLinear, animations<span class="synSpecial">:</span> {
<span class="synIdentifier">self</span>.indicatorPin.transform <span class="synIdentifier">=</span> CGAffineTransform(rotationAngle<span class="synSpecial">:</span> <span class="synType">CGFloat.pi</span>)
<span class="synIdentifier">self</span>.indicatorPin.transform <span class="synIdentifier">=</span> CGAffineTransform.identity
}, completion<span class="synSpecial">:</span> { completed <span class="synStatement">in</span>
<span class="synIdentifier">self</span>.start(title<span class="synSpecial">:</span> <span class="synType">title</span>)
})
}
}
</pre>
<p>indicatorPinに回転したい画像をセットし、startメソッド内の、UIView.animateメソッドを使って回転させます。</p>
<p>animationsの中身は、以下のように指定することで、1周分の回転を指定します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synIdentifier">self</span>.indicatorPin.transform <span class="synIdentifier">=</span> CGAffineTransform(rotationAngle<span class="synSpecial">:</span> <span class="synType">CGFloat.pi</span>)
<span class="synIdentifier">self</span>.indicatorPin.transform <span class="synIdentifier">=</span> CGAffineTransform.identity
</pre>
<p>最後のcompletionで自分自身を再帰呼び出し、1周回ったら次の周へ・・・とエンドレスに呼び出しています。</p>
<p>また、途中で止めたい場合は、stop用のメソッドなどを作ると良いかもです。</p>
<h2>完成形</h2>
<p>上記含めて、自作Indicatorの簡単な完成形です。<br/>
outletのところは、xibと接続してください。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synType">IndicatorView</span><span class="synSpecial">:</span> <span class="synType">UIView</span> {
<span class="synComment">/// Indicatorの動作フラグです</span>
<span class="synComment">/// - true: Indicatorの動作を停止します</span>
<span class="synComment">/// - false: Indicatorの動作を開始します</span>
<span class="synPreProc">var</span> <span class="synIdentifier">finished</span> <span class="synIdentifier">=</span> <span class="synConstant">true</span>
<span class="synType">@IBOutlet</span> weak <span class="synPreProc">var</span> <span class="synIdentifier">indicatorPin</span><span class="synSpecial">:</span> <span class="synType">UIImageView</span><span class="synIdentifier">!</span>
<span class="synStatement">override</span> <span class="synIdentifier">init</span>(frame<span class="synSpecial">:</span> <span class="synType">CGRect</span>) {
<span class="synIdentifier">super</span>.<span class="synIdentifier">init</span>(frame<span class="synSpecial">:</span> <span class="synType">frame</span>)
loadNib()
}
<span class="synStatement">required</span> <span class="synIdentifier">init</span>(coder aDecoder<span class="synSpecial">:</span> <span class="synType">NSCoder</span>) {
<span class="synIdentifier">super</span>.<span class="synIdentifier">init</span>(coder<span class="synSpecial">:</span> <span class="synType">aDecoder</span>)<span class="synIdentifier">!</span>
loadNib()
}
<span class="synComment">/// Indicatorを開始します</span>
<span class="synComment">///</span>
<span class="synPreProc">func</span> <span class="synIdentifier">start</span>() {
finished <span class="synIdentifier">=</span> <span class="synConstant">false</span>
running()
}
<span class="synComment">/// Indicator animationを実行します</span>
<span class="synPreProc">func</span> <span class="synIdentifier">running</span>() {
<span class="synStatement">if</span> finished <span class="synIdentifier">==</span> <span class="synConstant">true</span> {
<span class="synStatement">return</span>
}
UIView.animate(withDuration<span class="synSpecial">:</span> <span class="synConstant">1.0</span>, delay<span class="synSpecial">:</span> <span class="synConstant">0.0</span>, options<span class="synSpecial">:</span> .curveLinear, animations<span class="synSpecial">:</span> {
<span class="synIdentifier">self</span>.indicatorPin.transform <span class="synIdentifier">=</span> CGAffineTransform(rotationAngle<span class="synSpecial">:</span> <span class="synType">CGFloat.pi</span>)
<span class="synIdentifier">self</span>.indicatorPin.transform <span class="synIdentifier">=</span> CGAffineTransform.identity
}, completion<span class="synSpecial">:</span> { completed <span class="synStatement">in</span>
<span class="synIdentifier">self</span>.running()
})
}
<span class="synPreProc">func</span> <span class="synIdentifier">stop</span>() {
finished <span class="synIdentifier">=</span> <span class="synConstant">true</span>
}
<span class="synPreProc">private</span> <span class="synPreProc">func</span> <span class="synIdentifier">loadNib</span>(){
<span class="synPreProc">let</span> <span class="synIdentifier">view</span> <span class="synIdentifier">=</span> Bundle.main.loadNibNamed(String(describing<span class="synSpecial">:</span> <span class="synType">type</span>(of<span class="synSpecial">:</span> <span class="synType">self</span>)), owner<span class="synSpecial">:</span> <span class="synType">self</span>, options<span class="synSpecial">:</span> <span class="synType">nil</span>)?.first <span class="synStatement">as</span><span class="synIdentifier">!</span> UIView
view.frame <span class="synIdentifier">=</span> <span class="synIdentifier">self</span>.bounds
<span class="synIdentifier">self</span>.addSubview(view)
}
}
</pre>
<p>最後に、これを使いたいViewControllerのStoryboard等に設置して、以下のように呼び出すだけです。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>{IndicatorViewのインスタンス}.start()
{IndicatorViewのインスタンス}.stop()
</pre>
project-unknown
Kotlin:時計アプリを作成
hatenablog://entry/10257846132631161129
2018-10-09T01:55:15+09:00
2018-10-09T01:55:15+09:00 はじめに Kotlinで時計アプリを作ってみます。 ゴールは以下の機能を有するアプリを作ることに定めます。 シミュレータ上で日付・時間を表示する。 時間はリアルタイムで更新される。 んで、下記の内容を把握できるかなぁ〜と思っています。 関数の作り方などの基本的な内容をマスターする。 日付・時間の操作方法をマスターする。 画面のリアルタイム更新の方法をマスターする。 では、早速挑戦です! プロジェクト作成の主な設定は以下のような感じです。 基本的にデフォルトの設定になります。 設定名 設定内容 アプリ名 clock include Kotlin support チェックを入れる Activity…
<h2>はじめに</h2>
<p>Kotlinで時計アプリを作ってみます。</p>
<p>ゴールは以下の機能を有するアプリを作ることに定めます。</p>
<ul>
<li>シミュレータ上で日付・時間を表示する。</li>
<li>時間はリアルタイムで更新される。</li>
</ul>
<p>んで、下記の内容を把握できるかなぁ〜と思っています。</p>
<ul>
<li>関数の作り方などの基本的な内容をマスターする。</li>
<li>日付・時間の操作方法をマスターする。</li>
<li>画面のリアルタイム更新の方法をマスターする。</li>
</ul>
<p>では、早速挑戦です!</p>
<p>プロジェクト作成の主な設定は以下のような感じです。<br>
基本的にデフォルトの設定になります。</p>
<table>
<thead>
<tr>
<th>設定名</th>
<th>設定内容</th>
</tr>
</thead>
<tbody>
<tr>
<td>アプリ名</td>
<td> clock</td>
</tr>
<tr>
<td> include Kotlin support</td>
<td> チェックを入れる</td>
</tr>
<tr>
<td> Activity name</td>
<td> MainActivity</td>
</tr>
<tr>
<td> Layout name</td>
<td> activity_main</td>
</tr>
</tbody>
</table>
<h2>画面を設定する。</h2>
<p>画面ですが、次のファイルを開くと編集可能です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20181008/20181008184035.png" alt="f:id:project-unknown:20181008184035p:plain:w350" title="f:id:project-unknown:20181008184035p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>app>res>layout>activity_main.xmlを開きます。</p>
<p>デフォルトでtextViewが一つありますので、これのIDだけ変更しておきます。
IDは「dateView」としておきます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20181008/20181008211650.png" alt="f:id:project-unknown:20181008211650p:plain" title="f:id:project-unknown:20181008211650p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<h2>時間を取得する方法</h2>
<p>時間の取得は、
「Calendar」を利用します。</p>
<pre class="code" data-lang="" data-unlink>val calendar = Calendar.getInstance()
// 時・分・秒を設定する。
val hour = calendar.get(Calendar.HOUR)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
// dateViewのテキストを変更
val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"</pre>
<p>結果は・・・<br>
7時10分11秒と表示されました。<br>
できれば、19時と表示したいので、下記のように修正しました。</p>
<pre class="code" data-lang="" data-unlink>val calendar = Calendar.getInstance()
// 時・分・秒を設定する。
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
// dateViewのテキストを変更
val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"</pre>
<p>HOUR:午前、午後の何時(0〜11)を返す。<br>
HOUR_OF_DAY:1日の何時(0〜23)を返す。<br></p>
<p>ここまでは簡単ですかね〜</p>
<h2>リアルタイム更新</h2>
<p>リアルタイム更新が曲者です。</p>
<p>timer関数を使えばいいと思うのですが、<br></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923172732.png" alt="f:id:project-unknown:20180923172732p:plain" title="f:id:project-unknown:20180923172732p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>引数が多くて、わかりにくい!<br>
何を設定すればいいの!<br></p>
<p>調査した結果、引数の説明はこんな感じです。</p>
<table>
<thead>
<tr>
<th>引数</th>
<th>説明</th>
</tr>
</thead>
<tbody>
<tr>
<td>name</td>
<td>タイマーを実行しているスレッドに使用する名前。</td>
</tr>
<tr>
<td>daemon</td>
<td>trueの場合、スレッドはデーモンスレッドとして起動されます<br>(デーモンスレッドのみが実行されているときにVMは終了します)</td>
</tr>
<tr>
<td> initialDelay</td>
<td>タイマー作成後、指定したミリ秒後に処理を開始する。</td>
</tr>
<tr>
<td> period</td>
<td>指定したミリ秒後間隔で処理する。</td>
</tr>
<tr>
<td> action</td>
<td>定期的に実行する処理</td>
</tr>
</tbody>
</table>
<p>この中で必ず指定しないといけないのは、3つ</p>
<ul>
<li>name</li>
<li>period</li>
<li>action</li>
</ul>
<p>指定した結果は、こんな感じです。</p>
<pre class="code" data-lang="" data-unlink>// nameとperiodを指定、{}内が全部actionの指定
timer(name = "testTimer",period = 1000) {
val calendar = Calendar.getInstance()
// 時・分・秒を設定する。
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
// dateViewのテキストを変更
val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"
}</pre>
<p>これを実行した結果がこれ!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923195000.png" alt="f:id:project-unknown:20180923195000p:plain" title="f:id:project-unknown:20180923195000p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>ええ!!!!<br>
<br>
「アプリ名 keeps stopping」<br>
ビルドは成功したのに、Stopしてしまいました。<br>
原因はこいつでした。</p>
<pre class="code" data-lang="" data-unlink>val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"</pre>
<p>Androidには、<br>
<b>「画面に関する処理はメインスレッドから行わなければならない」</b><br>
という仕様があるためのようです。</p>
<p>timerのname引数の説明を読むと、<br>
「タイマーを実行しているスレッドに使用する名前」<br>
となっているため、timerメソッドを利用した段階で別スレッドを用意してしまい、その中で画面に関する処理を記載したため、停止してしまったようです。</p>
<h2>ハンドラを用意する。</h2>
<p>ハンドラはスレッド間の通信を行うことができます。<br></p>
<p>timerのスレッドで処理した内容をハンドラでメインスレッドに送信してあげます。</p>
<p>つまり、こんな感じで書き換えました。</p>
<pre class="code" data-lang="" data-unlink>// ハンドラのインスタンス作成
val hander = Handler()
timer(name = "testTimer",period = 1000) {
val calendar = Calendar.getInstance()
// 時・分・秒を設定する。
val hour = calendar.get(Calendar.HOUR_OF_DAY)
val minute = calendar.get(Calendar.MINUTE)
val second = calendar.get(Calendar.SECOND)
// ハンドラで処理したい内容をメインメソッドに送信
hander.post {
val test = findViewById<TextView>(R.id.dateView)
test.text = "${hour}時${minute}分${second}秒"
}
}</pre>
<p>ハンドラを用意して、timerスレッド内でメインスレッドで処理してほしい内容をメインメソッドに送信します。<br></p>
<p>これにより、画面に関する処理がメインメソッド上で処理され、停止することなく処理されるようになります。</p>
<p>実行結果は以下の通りです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923203644.png" alt="f:id:project-unknown:20180923203644p:plain:w350" title="f:id:project-unknown:20180923203644p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<h2>まとめ</h2>
<p>リアルタイム処理を学ぶつもりが、ハンドラを勉強することになってしまいました。<br></p>
<p>ハンドラって「イベントハンドラ」とかいろんなところで聞いていたんですが、よくわかってなかったんだってわかりました。<br></p>
project-unknown
Kotlin:Macでkotlin開発環境を構築(AndroidStudio)
hatenablog://entry/10257846132599272745
2018-10-03T01:21:31+09:00
2018-10-03T01:21:31+09:00 Kotlinで開発するため、AndroidStudioをMacにインストールしてみます! 開発環境って綺麗に構築できないことが多いですよね。 そのため、今回はエラーや失敗など、全部記録! 綺麗にいかない開発環境構築を記載します! 事前に入手した情報 KotlinでAndroidのアプリが開発できるようになった。 Android Studioで開発が可能、以上! つまり、事前知識ほぼ皆無です。(Androidアプリの開発経験もなし) Android Studioをダウンロード さっそくAndroidStadioをGoogleで検索! 次のサイトがヒットしました。 Android Studio の…
<p>Kotlinで開発するため、AndroidStudioをMacにインストールしてみます!<br></p>
<p>開発環境って綺麗に構築できないことが多いですよね。<br>
そのため、今回はエラーや失敗など、全部記録!<br>
綺麗にいかない開発環境構築を記載します!<br></p>
<h2>事前に入手した情報</h2>
<p>KotlinでAndroidのアプリが開発できるようになった。<br>
Android Studioで開発が可能、以上!<br>
つまり、事前知識ほぼ皆無です。(Androidアプリの開発経験もなし)</p>
<h2>Android Studioをダウンロード</h2>
<p>さっそくAndroidStadioをGoogleで検索!
次のサイトがヒットしました。</p>
<p><a href="https://developer.android.com/studio/install?hl=ja">Android Studio のインストール | Android Developers</a></p>
<p>ダウンロードサイトではなかったけど、公式のインストールガイドのようです。</p>
<p>しかし、その内容にビックリ!</p>
<pre class="code" data-lang="" data-unlink>Mac で Android Studio を利用する際に JDK 1.8 を使用していると、
安定性が低下することが知られています。
これらの問題が解決されるまでは、古いバージョン(JDK 1.6 以前)の
JDK をダウンロードすることで安定性を改善することができます。</pre>
<p>なんだと!?JDKってJavaの開発キットだから、つまり古いJava使えってことになる。<br>
できれば、バージョンダウンとかはしたく無い!</p>
<p>とりあえず、自分のMacのJDKを確認です!</p>
<p>ターミナルを開いて、</p>
<pre class="code" data-lang="" data-unlink>Java -version</pre>
<p>と入力、「java version "1.8.0_151"」とバージョンが判明・・・<br>
できれば、このバージョンを使いたい!</p>
<p>いろんなサイトを見ても、JDKの話は記載がありませんでした。<br>
安定してないかもしれないけど、このバージョンのままで行きましょう!<br>
では、早速以下のサイトからダウンロード開始です!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.android.com%2Fstudio%2F%3Fhl%3Dja" title="Download Android Studio and SDK tools | Android Developers" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://developer.android.com/studio/?hl=ja">developer.android.com</a></cite></p>
<p>「DOWNLOAD ANDROID SYUDIO」ボタンからAndroidStudioをダウンロードします。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180721/20180721174001.png" alt="f:id:project-unknown:20180721174001p:plain" title="f:id:project-unknown:20180721174001p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>「android-studio-ide-173.4819257-mac.dmg」ってファイルがダウンロードできました。</p>
<h2>インストール</h2>
<hr />
<p>「android-studio-ide-173.4819257-mac.dmg」を実行すると、こんな感じになりました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180721/20180721175417.png" alt="f:id:project-unknown:20180721175417p:plain:w350" title="f:id:project-unknown:20180721175417p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>早速、Androidstudioをアプリケーションに入れます。<br>
少し待つと、AndroidStudioがアプリケーションファイルに入りました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180721/20180721175925.png" alt="f:id:project-unknown:20180721175925p:plain:w200" title="f:id:project-unknown:20180721175925p:plain:w200" class="hatena-fotolife" style="width:200px" itemprop="image"></span></p>
<p>では早速、Android Studioを実行してみます!</p>
<hr />
<p>まず、以前のバージョンの設定を引き継ぐか聞かれました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180721/20180721180321.png" alt="f:id:project-unknown:20180721180321p:plain:w350" title="f:id:project-unknown:20180721180321p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>これは素直に「Do not import setting」を選択です。</p>
<hr />
<p>次に出てきたのは、これです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180825/20180825183713.png" alt="f:id:project-unknown:20180825183713p:plain:w350" title="f:id:project-unknown:20180825183713p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>「Server's certificate is not trusted」って出てきました・・・。なんでしょうこれ・・・</p>
<p>翻訳すると「サーバーの証明書が信頼されていない」ですって!<br>
ですが、信頼してもいいじゃないですか!ってことで「Accept(同意)」をクリック!</p>
<hr />
<p>なんとか先に進みました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180825/20180825194352.png" alt="f:id:project-unknown:20180825194352p:plain:w350" title="f:id:project-unknown:20180825194352p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>とりあえず「Next」</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180825/20180825195434.png" alt="f:id:project-unknown:20180825195434p:plain:w350" title="f:id:project-unknown:20180825195434p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>install Typeを選択ですが、初心者は迷わず「Standerd」を選択です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180825/20180825200559.png" alt="f:id:project-unknown:20180825200559p:plain:w350" title="f:id:project-unknown:20180825200559p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>次にUIの選択です。
これは単純に好きな方を選びましょう(後で選択可能か確認!)
ちなみに、私は「Darcula」を選択しました。</p>
<h3>Errorでグダグダに・・・</h3>
<hr />
<p>次に「verify setting」と表示されました。</p>
<p>これは設定確認みたいです・・・
って、ここでまさかのエラーが・・・</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180825/20180825202512.png" alt="f:id:project-unknown:20180825202512p:plain:w350" title="f:id:project-unknown:20180825202512p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>「an error occurred while trying to computer required packages」ってなんだよ!</p>
<p>いろいろ調べてみたら、「Finish」を押したら次の処理の画面に遷移して、問題なかった的な英語の記事を発見したので、私もとりあえず進んでみます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180825/20180825203238.png" alt="f:id:project-unknown:20180825203238p:plain:w350" title="f:id:project-unknown:20180825203238p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>・・・だめじゃん!<br>
「Failed to determine required packages」<br>
「必要なパッケージの決定に失敗しました」ですって!<br>
なんじゃそりゃ!しかもPreviousボタンが非活性・・・
戻れないじゃんか!<br>
・・・進んでみますか!</p>
<p>「Welcome to Android Studio」だって!<br>
やったね!!!って、よくみてみると</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180908/20180908150156.png" alt="f:id:project-unknown:20180908150156p:plain:w350" title="f:id:project-unknown:20180908150156p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>なんか、びっくりマークが出てます・・・。<br>
びっくりマークをクリックして詳細を表示してみました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180908/20180908150638.png" alt="f:id:project-unknown:20180908150638p:plain:w350" title="f:id:project-unknown:20180908150638p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>「Failed to determine required packages」<br>
さっきのエラーじゃないか!</p>
<p>前途多難過ぎますね・・・<br></p>
<h3>エラー解決へ</h3>
<hr />
<p>結局、必要なコンポーネントやパッケージはわからないかったです。<br>
ですのでWelcome画面の下の方にある、Configure>SDK Managerで必要そうなSDKをダウンロードしていこうと思います。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180908/20180908154543.png" alt="f:id:project-unknown:20180908154543p:plain:w350" title="f:id:project-unknown:20180908154543p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>どうやら、「Android SDK Local」に値が設定されて
それならば、Android SDKを自分でインストールしてみます。</p>
<p>HomeBrewをインストールしていたので、下記のコマンドでインストール開始です!</p>
<p><code>
brew cask install android-sdk
</code></p>
<p>どうやら、成功したようです。<br>
では、早速、「Android SDK Local」のEditをクリック設定だ〜〜〜</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180908/20180908155757.png" alt="f:id:project-unknown:20180908155757p:plain:w350" title="f:id:project-unknown:20180908155757p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>もしかして、余計なことしなくても、Android SDKが普通にインストールできるっぽいぞ・・・
余計なことやってしまったのか?</p>
<p>AndroidStudioを動かしたところ、また、インストール画面が表示されてました。<br>
とりあえず、やり直してみるか〜と先ほどと同じようにボタンをぽちぽちやりました。</p>
<p>そして、問題の「verify setting」ですが・・・</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180908/20180908160207.png" alt="f:id:project-unknown:20180908160207p:plain:w350" title="f:id:project-unknown:20180908160207p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>なんか、エラーが解消されている!<br>
AndroidSDKをインストールしたことで解消されたのかもしれませんね。</p>
<p>これで心置きなく、インストール作業が実行できます!<br>
「Finish」ボタンをクリックすると、今度はコンポーネントのダウンロードが始まりました!!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180908/20180908162402.png" alt="f:id:project-unknown:20180908162402p:plain:w350" title="f:id:project-unknown:20180908162402p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>なんとかなったっぽいです。<br>
では、Finishさせて、やっとWelCome画面です!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909122638.png" alt="f:id:project-unknown:20180909122638p:plain:w350" title="f:id:project-unknown:20180909122638p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>今度は!マークがないので、正しくインストールができたみたいです。</p>
<h2>プロジェクトを作成</h2>
<p>では早速、Androidのプロジェクトを作ってみます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909122924.png" alt="f:id:project-unknown:20180909122924p:plain:w350" title="f:id:project-unknown:20180909122924p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>適当なプロジェクト名を入力して、<br>
あと、kotlin使うから、「Include Kotlin suppurt」にチェックを入れます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909123116.png" alt="f:id:project-unknown:20180909123116p:plain:w350" title="f:id:project-unknown:20180909123116p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>ここでは、Androidのバージョンをどこまでサポートするか選択できるようです。
ここは初心者なので、デフォルトでいきます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909123239.png" alt="f:id:project-unknown:20180909123239p:plain:w350" title="f:id:project-unknown:20180909123239p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>今度はアクティビティの選択、デザインのテンプレートを選ぶようですので、とりあえずデフォルト!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909124731.png" alt="f:id:project-unknown:20180909124731p:plain:w350" title="f:id:project-unknown:20180909124731p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>メインアクティビティの名前を決定します。
まぁ、これもデフォルトにします。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909125324.png" alt="f:id:project-unknown:20180909125324p:plain:w350" title="f:id:project-unknown:20180909125324p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>コンポーネントのインストールを行うようですが、
デフォルトだけあって、全てインストール済みのようです。</p>
<p>では、OKなのでFinish!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909125420.png" alt="f:id:project-unknown:20180909125420p:plain:w350" title="f:id:project-unknown:20180909125420p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>buildが始まりました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180909/20180909125537.png" alt="f:id:project-unknown:20180909125537p:plain:w350" title="f:id:project-unknown:20180909125537p:plain:w350" class="hatena-fotolife" style="width:350px" itemprop="image"></span></p>
<p>おお!<br>
ついにメイン画面が現れました!<br></p>
<h2>まとめ</h2>
<p>Android Studioのインストールをやってみましたが、<br>
思ったより難しくはなかったような気がします。</p>
<p>正直、エラーが発生して、1からやり直しが発生すると思っていたのですが・・・。</p>
<p>Javaのバージョンが気になりますが、現在のところ、特に問題はありません。</p>
<p>これから、Kotlin開発頑張ります!</p>
project-unknown
XCode10でビルドできなくなった時にやったこと - Multiple commands produce error
hatenablog://entry/10257846132637840594
2018-09-23T19:11:49+09:00
2019-08-31T19:39:48+09:00 XCode10がリリースされ、早速手元にあるXCode9でビルドしたアプリもXCode10用にビルドしようとした際に以下のエラーが発生しました。 error: Multiple commands produce '/Users/{アカウント名}/Library/Developer/Xcode/DerivedData/{アプリ名}-hievebmhhbbmlybyrcuahkuwpdgk/Build/Products/Debug-iphonesimulator/{アプリ名}.app': 1) Target '{アプリ名}' has create directory command with ou…
<p>XCode10がリリースされ、早速手元にあるXCode9でビルドしたアプリもXCode10用にビルドしようとした際に以下のエラーが発生しました。</p>
<pre class="code" data-lang="" data-unlink>error: Multiple commands produce '/Users/{アカウント名}/Library/Developer/Xcode/DerivedData/{アプリ名}-hievebmhhbbmlybyrcuahkuwpdgk/Build/Products/Debug-iphonesimulator/{アプリ名}.app':
1) Target '{アプリ名}' has create directory command with output '/Users/{アカウント名}/Library/Developer/Xcode/DerivedData/{アプリ名}-hievebmhhbbmlybyrcuahkuwpdgk/Build/Products/Debug-iphonesimulator/{アプリ名}.app'
2) That command depends on command in Target '{アプリ名}': script phase “[CP] Copy Pods Resources”</pre>
<p>原因は、XCodeのbuildシステムのデフォルトが変更された事によります。<br/>
buildシステムについては、以下から確認できます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923190113.png" alt="f:id:project-unknown:20180923190113p:plain" title="f:id:project-unknown:20180923190113p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923190145.png" alt="f:id:project-unknown:20180923190145p:plain" title="f:id:project-unknown:20180923190145p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>上記のように、XCode10からは、New Build Systemがデフォルトになっています。<br/>
(XCode9までは、Legacy Build Systemに該当するものがこれに当たります)</p>
<p>これに対する対処法は、2通りあります。</p>
<ol>
<li>Product Nameで${TARGET_NAME}を使うのをやめる</li>
<li>Legacy Build Systemを採用する</li>
</ol>
<p>結論を言うと、私は今回の対応で2を選択しました。<br/>
今回は、それぞれの対応方法についでご紹介します。</p>
<h2>対応方法1 Product Nameで${TARGET_NAME}を使うのをやめる</h2>
<p>おそらくこれが推奨されるやり方です。</p>
<p>Build Settings -> Product Name</p>
<p>ここに${TARGET_NAME}で設定されているものを、環境変数を使うのではなくて、手打ちでProduct Nameを入力します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923190543.png" alt="f:id:project-unknown:20180923190543p:plain" title="f:id:project-unknown:20180923190543p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>今回、私がやったのは<a href="https://itunes.apple.com/jp/app/time-hacker-%E6%99%82%E9%96%93%E7%AE%A1%E7%90%86/id1381203243?mt=8">TimeHackerアプリ</a>の設定を行ったので、「TimeHacker」と入力しました。</p>
<p>後は、WidgetやWatchでも同様の事を実施して、Buildするだけです。</p>
<p>…が、私の場合、おそらくCocoaPodsのパッケージ依存か何かでうまく動きませんでした。</p>
<h2>対応方法2 Legacy Build Systemを採用する</h2>
<p>冒頭で紹介したWorkspace Settings...の設定をLegacy Build Systemに変更します。<br/>
これでこれまでのBuild(XCode9までのBuild)が実行されます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923190113.png" alt="f:id:project-unknown:20180923190113p:plain" title="f:id:project-unknown:20180923190113p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180923/20180923190850.png" alt="f:id:project-unknown:20180923190850p:plain" title="f:id:project-unknown:20180923190850p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<h2>最後に</h2>
<p>New Build SystemはSwift製と言うのと、今後、こちらをデファクトスタンダードになっていくと思われます。<br/>
Podsやパッケージが対応されたら、New Build Systemにうつしていこうと思います。</p>
project-unknown
Apple Watch - Watch OS4 でCoreData等を用いた開発 - Swift4
hatenablog://entry/10257846132618549419
2018-09-02T01:09:56+09:00
2019-08-31T19:52:54+09:00 Apple Watch (Watch OS4) この記事では、Apple Watch (以降Watch OS)の開発にあたっての私なりの開発メモを記します。 また、何の断りがなければ、Watch OSはWatch OS4の事を指しています。 環境としては、 XCode 9.4.x Watch OS 4 (Watch Kit2) での開発です。 特にCoreData等のリソース周りの資料があまり見つからなかったので、そこを重点的に記載しています。 また、今回の記事はTIME HACKERでApple Watch対応を行っている時に、主に私が悩んだ所ですので、CoreDataとのデータのやり取りを…
<h2 id="Apple-Watch-Watch-OS4">Apple Watch (Watch OS4)</h2>
<p>この記事では、Apple Watch (以降Watch OS)の開発にあたっての私なりの開発メモを記します。<br/>
また、何の断りがなければ、Watch OSはWatch OS4の事を指しています。</p>
<p>環境としては、</p>
<ul>
<li>XCode 9.4.x</li>
<li>Watch OS 4 (Watch Kit2)</li>
</ul>
<p>での開発です。<br/>
特にCoreData等のリソース周りの資料があまり見つからなかったので、そこを重点的に記載しています。</p>
<p>また、今回の記事は<a href="https://itunes.apple.com/jp/app/time-hacker-%E6%99%82%E9%96%93%E7%AE%A1%E7%90%86/id1381203243?mt=8">TIME HACKER</a>でApple Watch対応を行っている時に、主に私が悩んだ所ですので、CoreDataとのデータのやり取りを主にフォーカスしています。</p>
<ul class="table-of-contents">
<li><a href="#Apple-Watch-Watch-OS4">Apple Watch (Watch OS4)</a></li>
<li><a href="#これまでのWatch-OS-初代Watch-Kitとの違い">これまでのWatch OS (初代Watch Kit)との違い</a></li>
<li><a href="#新しいWatch-OSでのCoreData等のリソースアクセスの考え方-Watch-Connectivity">新しいWatch OSでのCoreData等のリソースアクセスの考え方 Watch Connectivity</a></li>
<li><a href="#データ送受信部分">データ送受信部分</a><ul>
<li><a href="#iPhone-App側の実装">iPhone App側の実装</a></li>
<li><a href="#Watch-OS側の実装">Watch OS側の実装</a></li>
</ul>
</li>
<li><a href="#Watch-OSとiPhoneとでCoreData等のデータのやり取りを行います">Watch OSとiPhoneとでCoreData等のデータのやり取りを行います。</a><ul>
<li><a href="#前提として">前提として</a></li>
<li><a href="#iPhone-App側の実装-1">iPhone App側の実装</a></li>
<li><a href="#Watch-OS側の実装-1">Watch OS側の実装</a></li>
</ul>
</li>
<li><a href="#詰まった所">詰まった所</a></li>
<li><a href="#最後に">最後に</a></li>
</ul>
<table>
<tr>
<td style="border-style: none;">
<a href="//af.moshimo.com/af/c/click?a_id=1092401&p_id=966&pc_id=1260&pl_id=13906&guid=ON" target="_blank" rel="nofollow"><img src="//image.moshimo.com/af-img/0304/000000013906.png" width="300" height="250" style="border:none;"></a><img src="//i.moshimo.com/af/i/impression?a_id=1092401&p_id=966&pc_id=1260&pl_id=13906" width="1" height="1" style="border:none;">
</td><td style="border-style: none;">
<a href="//af.moshimo.com/af/c/click?a_id=1093050&p_id=936&pc_id=1196&pl_id=13777&guid=ON" target="_blank" rel="nofollow"><img src="//image.moshimo.com/af-img/0288/000000013777.png" width="300" height="250" style="border:none;"></a><img src="//i.moshimo.com/af/i/impression?a_id=1093050&p_id=936&pc_id=1196&pl_id=13777" width="1" height="1" style="border:none;">
</td>
</tr>
</table>
<h2 id="これまでのWatch-OS-初代Watch-Kitとの違い">これまでのWatch OS (初代Watch Kit)との違い</h2>
<p>Watch OS開発は、これまで(初代Watch Kit)とは、特にリソースの取扱方法が異なってます。</p>
<p>その違いは、以下のAppleが展開している図を見ると明らかです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180902/20180902001023.png" alt="f:id:project-unknown:20180902001023p:plain" title="f:id:project-unknown:20180902001023p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>これまでは、Apple WatchはiPhone上で動作させること前提の仕組みとなっていた為、CoreDataやUserDefaultsをApp Groupsの設定さえ行っていれば、共有リソースとしてアクセスすることが出来ました。</p>
<p>しかし、Watch Kit 2からは、Apple Watch上で独立して動作する事になり、<br/>
Apple Watchから共有リソースへのアクセスができなくなりました。</p>
<h2 id="新しいWatch-OSでのCoreData等のリソースアクセスの考え方-Watch-Connectivity">新しいWatch OSでのCoreData等のリソースアクセスの考え方 Watch Connectivity</h2>
<p>UserDefaults等で取り扱う、一時的な設定等は、Apple Watchでそのまま使えば良いでしょう。<br/>
しかし、iPhone上のアプリとデータを共有する場合は、Apple Watch上のアプリからiPhoneアプリへデータの送受信を行うと言う考え方で取り扱う事になります。<br/>
このときに、Watch Connectivityを用いて送受信を行います。</p>
<p>図で言う、枠で囲った所がそれに当たります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180902/20180902001752.png" alt="f:id:project-unknown:20180902001752p:plain" title="f:id:project-unknown:20180902001752p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>しかし、これには注意点があります。</p>
<p>これまでは単純に同一デバイス内のリソースファイルへアクセスしていたので、それなりに高速に取り扱うことが出来ましたが。<br/>
Apple WatchとiPhoneとは物理的にも分断されている通り、Sessionでのデータのやり取りになります。<br/>
当然、<strong>画像ファイル等のやり取りを行うと、その分データの送受信に時間がかかるので注意が必要です</strong>。</p>
<p>画像ファイルをWebから取得して利用するのではなく、予めアプリに組み込んだものを利用するのであれば、iPhoneとApple Watch両方に同一画像を埋め込んでおくのが妥当だと思います。</p>
<p>では、実際にiPhoneとWatch OSとでリソースのやり取りを行うサンプルを造っていきます。</p>
<h2 id="データ送受信部分">データ送受信部分</h2>
<p>まずは、データの送受信を行う所を造ります。<br/>
イメージは、繰り返しになりますが、以下の図の用に考えます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180902/20180902003023.png" alt="f:id:project-unknown:20180902003023p:plain" title="f:id:project-unknown:20180902003023p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>iPhone, Watch OS共にデータの送受信を行う為のSessionをこさえます。</p>
<h3 id="iPhone-App側の実装">iPhone App側の実装</h3>
<p>以下のようにWatchManagerクラスを用意します。</p>
<p>WatchManagerは以下の役割を担います。</p>
<ul>
<li>Watch OSとのSession</li>
<li>Watch OSから来たEventの種別を判定 (後述)</li>
</ul>
<p>また、WatchManagerはSessionを取り扱う為、念の為シングルトン設計にしています。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> WatchConnectivity
<span class="synPreProc">class</span> <span class="synType">WatchManager</span><span class="synSpecial">:</span> <span class="synType">NSObject</span>, WCSessionDelegate {
<span class="synPreProc">let</span> <span class="synIdentifier">wcSession</span> <span class="synIdentifier">=</span> WCSession.<span class="synStatement">default</span>
<span class="synPreProc">private</span> <span class="synPreProc">static</span> <span class="synPreProc">let</span> <span class="synIdentifier">instance</span> <span class="synIdentifier">=</span> WatchManager()
<span class="synPreProc">public</span> <span class="synPreProc">class</span> <span class="synType">var</span> shared<span class="synSpecial">:</span> <span class="synType">WatchManager</span> {
<span class="synStatement">return</span> instance
}
<span class="synPreProc">func</span> <span class="synIdentifier">initWCSession</span>() {
<span class="synStatement">if</span> WCSession.isSupported() {
wcSession.delegate <span class="synIdentifier">=</span> <span class="synIdentifier">self</span>
wcSession.activate()
print(<span class="synConstant">"[INFO] session activate"</span>)
} <span class="synStatement">else</span> {
print(<span class="synConstant">"[INFO] session error"</span>)
}
}
}
<span class="synPreProc">extension</span> <span class="synType">WatchManager</span> {
<span class="synComment">/// メッセージを受信した時の処理</span>
<span class="synComment">///</span>
<span class="synComment">/// - Parameters:</span>
<span class="synComment">/// - session:</span>
<span class="synComment">/// - message:</span>
<span class="synComment">/// - replyHandler:</span>
<span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveMessage message<span class="synSpecial">:</span> <span class="synPreProc">[String : Any]</span>, replyHandler<span class="synSpecial">:</span> <span class="synType">@escaping</span> ([String <span class="synSpecial">:</span> <span class="synType">Any</span>]) <span class="synSpecial">-></span> <span class="synType">Void</span>) {
<span class="synPreProc">var</span> <span class="synIdentifier">result</span><span class="synSpecial">:</span> <span class="synPreProc">[String: Any]</span> <span class="synIdentifier">=</span> [<span class="synConstant">"success"</span> <span class="synSpecial">:</span> <span class="synType">false</span>]
replyHandler(result)
}
}
<span class="synComment">/// Session Delegate</span>
<span class="synPreProc">extension</span> <span class="synType">WatchManager</span> {
<span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, activationDidCompleteWith activationState<span class="synSpecial">:</span> <span class="synType">WCSessionActivationState</span>, error<span class="synSpecial">:</span> <span class="synType">Error</span>?) {
print(<span class="synConstant">"[INFO] The session has completed activation."</span>)
}
<span class="synPreProc">func</span> <span class="synIdentifier">sessionDidBecomeInactive</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>) {
print(<span class="synConstant">"[INFO] The session has got into inactivation."</span>)
}
<span class="synPreProc">func</span> <span class="synIdentifier">sessionDidDeactivate</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>) {
print(<span class="synConstant">"[INFO] The session has deactivated"</span>)
}
<span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveUserInfo userInfo<span class="synSpecial">:</span> <span class="synPreProc">[String : Any]</span> <span class="synIdentifier">=</span> [<span class="synSpecial">:</span>]) {
print(userInfo)
}
<span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveMessage message<span class="synSpecial">:</span> <span class="synPreProc">[String : Any]</span>) {
print(<span class="synConstant">"[INFO] try to send message to watch"</span>)
sendMessage()
}
}
</pre>
<p>やっていることは、WCSessionオブジェクトを生成し、delegateを自分自身にしているだけです。<br/>
これを例えば、AppDelegate等で</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">class</span> <span class="synType">AppDelegate</span><span class="synSpecial">:</span> <span class="synType">UIResponder</span>, UIApplicationDelegate {
<span class="synPreProc">func</span> <span class="synIdentifier">application</span>(_ application<span class="synSpecial">:</span> <span class="synType">UIApplication</span>, didFinishLaunchingWithOptions launchOptions<span class="synSpecial">:</span> <span class="synPreProc">[UIApplicationLaunchOptionsKey: Any]</span>?) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
WatchManager.shared.initWCSession()
}
}
</pre>
<p>と、呼んであげるとWatch OS用のSessionを貼ることが出来ます。<br/>
後は、Watch OSからEventが飛んでくると、適宜Watch Manager内のDelegateが呼び出されます。</p>
<p>上記サンプルで</p>
<pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveMessage message<span class="synSpecial">:</span> <span class="synPreProc">[String : Any]</span>, replyHandler<span class="synSpecial">:</span> <span class="synType">@escaping</span> ([String <span class="synSpecial">:</span> <span class="synType">Any</span>]) <span class="synSpecial">-></span> <span class="synType">Void</span>) {
}
</pre>
<p>このコードだけ、中身に軽い処理を入れているのは、今回のサンプルでは、主にこのDelegateを用いる為です。念の為。</p>
<h3 id="Watch-OS側の実装">Watch OS側の実装</h3>
<p>iPhone App側の実装は出来たので、Watch OS側の実装を行います。<br/>
今回のサンプルでは、Watch OSが起動した際に、iPhone AppへSessionを開始するコードとなっています(この際はloadDataメソッドをコールします)。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> WatchKit
<span class="synPreProc">import</span> Foundation
<span class="synPreProc">import</span> WatchConnectivity
<span class="synPreProc">class</span> <span class="synType">InterfaceController</span><span class="synSpecial">:</span> <span class="synType">WKInterfaceController</span>, WCSessionDelegate {
<span class="synPreProc">let</span> <span class="synIdentifier">wcSession</span> <span class="synIdentifier">=</span> WCSession.<span class="synStatement">default</span>
<span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">awake</span>(withContext context<span class="synSpecial">:</span> <span class="synType">Any</span>?) {
<span class="synIdentifier">super</span>.awake(withContext<span class="synSpecial">:</span> <span class="synType">context</span>)
<span class="synComment">// WCSessionの開始</span>
<span class="synStatement">if</span> WCSession.isSupported() {
wcSession.delegate <span class="synIdentifier">=</span> <span class="synIdentifier">self</span>
wcSession.activate()
}
}
<span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">didAppear</span>() {
<span class="synIdentifier">super</span>.didAppear()
print(<span class="synConstant">"[Info] didAppear"</span>)
<span class="synStatement">if</span> wcSession.isReachable {
print(<span class="synConstant">"[Info] reachable"</span>)
loadData()
} <span class="synStatement">else</span> {
print(<span class="synConstant">"[Info] not reachable"</span>)
}
}
<span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">willActivate</span>() {
<span class="synIdentifier">super</span>.willActivate()
}
<span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">didDeactivate</span>() {
<span class="synIdentifier">super</span>.didDeactivate()
}
<span class="synPreProc">func</span> <span class="synIdentifier">loadData</span>() {
<span class="synPreProc">let</span> <span class="synIdentifier">message</span> <span class="synIdentifier">=</span> [ <span class="synConstant">"message"</span> <span class="synSpecial">:</span> <span class="synConstant">"test"</span>]
wcSession.sendMessage(message, replyHandler<span class="synSpecial">:</span> { replyDict <span class="synStatement">in</span>
print(<span class="synConstant">"[INFO] responce action data"</span>)
print(replyDict)
}, errorHandler<span class="synSpecial">:</span> { error <span class="synStatement">in</span>
print(error)
})
}
}
<span class="synComment">// MARK: - Session Delegate</span>
<span class="synPreProc">extension</span> <span class="synType">InterfaceController</span> {
<span class="synPreProc">func</span> <span class="synIdentifier">sessionReachabilityDidChange</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>) {
print(<span class="synConstant">"[Info] sessionReachabilityDidChange"</span>)
<span class="synStatement">if</span> wcSession.isReachable {
print(<span class="synConstant">"[Info] reachable"</span>)
loadData()
} <span class="synStatement">else</span> {
print(<span class="synConstant">"[Info] not reachable"</span>)
}
}
<span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, activationDidCompleteWith activationState<span class="synSpecial">:</span> <span class="synType">WCSessionActivationState</span>, error<span class="synSpecial">:</span> <span class="synType">Error</span>?) {
print(<span class="synConstant">"[Info] The session has completed activation."</span>)
}
<span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveMessage message<span class="synSpecial">:</span> <span class="synPreProc">[String: Any]</span>, replyHandler<span class="synSpecial">:</span> <span class="synType">@escaping</span> ([String<span class="synSpecial">:</span> <span class="synType">Any</span>]) <span class="synSpecial">-></span> <span class="synType">Void</span>) {
<span class="synComment">// iPhoneからのデータを受け取る</span>
print(<span class="synConstant">"[Info] didReceiveMessage"</span>)
}
}
</pre>
<p>上記コードの、</p>
<pre class="code lang-swift" data-lang="swift" data-unlink> wcSession.sendMessage(message, replyHandler<span class="synSpecial">:</span> { replyDict <span class="synStatement">in</span>
print(<span class="synConstant">"[INFO] responce action data"</span>)
print(replyDict)
}, errorHandler<span class="synSpecial">:</span> { error <span class="synStatement">in</span>
print(error)
})
</pre>
<p>この部分で、iPhone App側に通信を送っています。<br/>
ここで、例えばWatchManagerのメソッドを以下のように変更します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveMessage message<span class="synSpecial">:</span> <span class="synPreProc">[String : Any]</span>, replyHandler<span class="synSpecial">:</span> <span class="synType">@escaping</span> ([String <span class="synSpecial">:</span> <span class="synType">Any</span>]) <span class="synSpecial">-></span> <span class="synType">Void</span>) {
print(message)
<span class="synPreProc">var</span> <span class="synIdentifier">result</span><span class="synSpecial">:</span> <span class="synPreProc">[String: Any]</span> <span class="synIdentifier">=</span> [<span class="synConstant">"success"</span> <span class="synSpecial">:</span> <span class="synType">false</span>]
replyHandler(result)
}
</pre>
<p>これで実行すると、 以下の情報が出力されます。</p>
<pre class="code" data-lang="" data-unlink>[ "message" : "test"]</pre>
<p>ここまでで、iPhone AppとWatch OS Appとの接続の部分が出来ました</p>
<h2 id="Watch-OSとiPhoneとでCoreData等のデータのやり取りを行います">Watch OSとiPhoneとでCoreData等のデータのやり取りを行います。</h2>
<p>ここに来て、やっと表題のお話が出来ます…。</p>
<p>これまでは、Watch OS AppとiPhone Appとの通信部分を造ってきたので、<br/>
この流れの上で、リソースデータのやり取りを行います。</p>
<p>といっても、設計はごく単純で、<br/>
すべてのデータのやり取りはiPhone側で行い、Watch OS側は適宜データの送受信のEventを送るだけです。</p>
<p>最初の方に載せた図で言うと、以下のようにApple Watchから来たEventのレシーバからCoreDataへアクセスし、データが必要であれば、返却する仕組みです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180902/20180902005106.png" alt="f:id:project-unknown:20180902005106p:plain" title="f:id:project-unknown:20180902005106p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<h3 id="前提として">前提として</h3>
<p>今回のデータの送受信を行うにあたり、以下のデータ構造をDictionary形式でやり取りします。</p>
<p><strong>Apple Watchからデータを取得するリクエストを投げる時</strong></p>
<pre class="code" data-lang="" data-unlink>[
"kind" : 1,
]</pre>
<p>kind : 1はデータ取得を行いたい識別子です。</p>
<p><strong>Apple Watchからデータを更新した際にリクエストを投げる時</strong></p>
<pre class="code" data-lang="" data-unlink>[
"kind" : 2,
"data" : Any
]</pre>
<p>この場合は、kind : 2として、一緒に更新したいデータを送信します。</p>
<p>では、コードの方を見てみます。</p>
<h3 id="iPhone-App側の実装-1">iPhone App側の実装</h3>
<p>これまで書いてきた、WatchManagerのメソッドを以下のように変更します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveMessage message<span class="synSpecial">:</span> <span class="synPreProc">[String : Any]</span>, replyHandler<span class="synSpecial">:</span> <span class="synType">@escaping</span> ([String <span class="synSpecial">:</span> <span class="synType">Any</span>]) <span class="synSpecial">-></span> <span class="synType">Void</span>) {
<span class="synPreProc">var</span> <span class="synIdentifier">result</span><span class="synSpecial">:</span> <span class="synPreProc">[String: Any]</span> <span class="synIdentifier">=</span> [<span class="synConstant">"success"</span> <span class="synSpecial">:</span> <span class="synType">false</span>]
<span class="synStatement">guard</span> <span class="synPreProc">let</span> <span class="synIdentifier">kind</span> <span class="synIdentifier">=</span> message[<span class="synConstant">"kind"</span>] <span class="synStatement">as</span>? Int <span class="synStatement">else</span> {
result[<span class="synConstant">"success"</span>] <span class="synIdentifier">=</span> <span class="synConstant">false</span>
replyHandler(result)
<span class="synStatement">return</span>
}
<span class="synStatement">switch</span> kind {
<span class="synStatement">case</span> <span class="synConstant">1</span><span class="synSpecial">:</span>
result[<span class="synConstant">"data"</span>] <span class="synIdentifier">=</span> <span class="synComment">/* CoreDataからとってきたデータ */</span>
result[<span class="synConstant">"success"</span>] <span class="synIdentifier">=</span> <span class="synConstant">true</span>
<span class="synStatement">return</span>
<span class="synStatement">case</span> <span class="synConstant">2</span><span class="synSpecial">:</span>
<span class="synComment">/* CoreDataのデータ更新 */</span>
result[<span class="synConstant">"success"</span>] <span class="synIdentifier">=</span> <span class="synConstant">true</span>
<span class="synStatement">return</span>
<span class="synStatement">default</span><span class="synSpecial">:</span>
replyHandler(result)
<span class="synStatement">return</span>
}
}
</pre>
<p>kindで、処理の識別を行い、適宜CoreDataの操作を行います。<br/>
最後に結果をreplyHandlerに渡して上げるだけです。</p>
<h3 id="Watch-OS側の実装-1">Watch OS側の実装</h3>
<p>iPhone Appでデータを受信して、適切なレスポンスを返せるようになったので、<br/>
今度はWatch OS側からEventを送信する方法です。</p>
<p>まずは、データを受け取る所ですが、<br/>
loadDataメソッドを以下のように改変します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">func</span> <span class="synIdentifier">loadData</span>() {
<span class="synPreProc">let</span> <span class="synIdentifier">message</span> <span class="synIdentifier">=</span> [ <span class="synConstant">"kind"</span> <span class="synSpecial">:</span> <span class="synConstant">1</span>]
wcSession.sendMessage(message, replyHandler<span class="synSpecial">:</span> { replyDict <span class="synStatement">in</span>
print(<span class="synConstant">"[INFO] responce action data"</span>)
print(replyDict)
<span class="synComment">/* replyDictにCoreDataから取得した情報が入っているので、以後適切な処理を行う */</span>
}, errorHandler<span class="synSpecial">:</span> { error <span class="synStatement">in</span>
print(error)
})
}
</pre>
<p>データ受信の際は、kind : 1でSession通信を行えば、iPhone側でCoreDataにアクセスし、データを返却してくれます。</p>
<p>次に、データ更新は、上記loadDataと殆ど同じですが、念の為載せておきます。<br/>
データ更新ように、saveDataメソッドを追加しています。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink> <span class="synPreProc">func</span> <span class="synIdentifier">saveData</span>() {
<span class="synPreProc">var</span> <span class="synIdentifier">message</span><span class="synSpecial">:</span> <span class="synPreProc">[String: Any]</span> <span class="synIdentifier">=</span> [<span class="synSpecial">:</span>]
message[<span class="synConstant">"kind"</span>] <span class="synIdentifier">=</span> <span class="synConstant">2</span>
message[<span class="synConstant">"data"</span>] <span class="synIdentifier">=</span> data
wcSession.sendMessage(message, replyHandler<span class="synSpecial">:</span> { replyDict <span class="synStatement">in</span>
print(replyDict)
}, errorHandler<span class="synSpecial">:</span> { error <span class="synStatement">in</span>
print(error)
})
}
</pre>
<p>これで、適切なタイミングでsaveDataを呼び出せば、保存したいデータをCoreDataに保存することが出来ます。</p>
<h2 id="詰まった所">詰まった所</h2>
<p>終わりに、私が盛大に詰まった所として…。<br/>
当たり前のように、iPhone AppのWatch ManagerでreplyHandlerを使っていますが、<br/>
Apple WatchでSession処理を行う際に、適切なDelegateをiPhone App側で実装していないと通信エラーとなります。</p>
<p>この詳細は、以下にまとめておきましたので、もし詰まってしまった場合の参考にしてください。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Fwatch-error1" title="Watch OS 2 - WCErrorCodeDeliveryFailed が出てiPhoneとWatchでデータのやり取りが出来ない時の対応 - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/watch-error1">www.project-unknown.jp</a></cite></p>
<h2 id="最後に">最後に</h2>
<p>Watch OSは他のWidget等と違って、これまでのWidget等と違って、考え方を大きくシフトしないとなかなか造りにくいです。<br/>
しかし、WebAPIを用いた実装を行っているんだと、考えると一気に考え方が楽になります。(Appleはここまで考えているんでしょうね・・・さすがや)</p>
<p>最後に宣伝となりますが、<br/>
この記事は、現在<a href="https://itunes.apple.com/jp/app/time-hacker-%E6%99%82%E9%96%93%E7%AE%A1%E7%90%86/id1381203243?mt=8">TIME HACKER</a>でApple Watch対応を行っており、その際の備忘録として記したものです。</p>
<p>もう少しでリリースが見えている所ですので、是非とも皆さんお使いになってくださいね!</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
project-unknown
Watch OS 2 - WCErrorCodeDeliveryFailed が出てiPhoneとWatchでデータのやり取りが出来ない時の対応
hatenablog://entry/10257846132614665314
2018-08-26T16:08:29+09:00
2019-08-31T19:39:48+09:00 はじめに Watch OS 2からは、Apple WatchがiPhoneアプリから独立して動くことになったので、これまでとは違った書き方が必要です。 この対応を行っている最中に、以下の様なエラーが出て詰まってしまったので、その備忘録を示します。 エラーの内容 WCErrorCodeDeliveryFailed Watch Extension[3994:434267] [WC] -[WCSession _onqueue_notifyOfMessageError:messageID:withErrorHandler:] CFEDC3E4-FE0F-439D-B884-1CB2EC349DED e…
<h2>はじめに</h2>
<p>Watch OS 2からは、Apple WatchがiPhoneアプリから独立して動くことになったので、これまでとは違った書き方が必要です。</p>
<p>この対応を行っている最中に、以下の様なエラーが出て詰まってしまったので、その備忘録を示します。</p>
<h2>エラーの内容 WCErrorCodeDeliveryFailed</h2>
<pre class="code" data-lang="" data-unlink>Watch Extension[3994:434267] [WC] -[WCSession _onqueue_notifyOfMessageError:messageID:withErrorHandler:] CFEDC3E4-FE0F-439D-B884-1CB2EC349DED errorHandler: YES with WCErrorCodeDeliveryFailed
Error Domain=WCErrorDomain Code=7014 "Payload could not be delivered." UserInfo={NSLocalizedDescription=Payload could not be delivered.}</pre>
<h2>実際に書いていたコードの中身</h2>
<p>iPhone側、AppleDelegate</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>
<span class="synPreProc">let</span> <span class="synIdentifier">wcSession</span> <span class="synIdentifier">=</span> WCSession.<span class="synStatement">default</span>
<span class="synPreProc">func</span> <span class="synIdentifier">application</span>(_ application<span class="synSpecial">:</span> <span class="synType">UIApplication</span>, didFinishLaunchingWithOptions launchOptions<span class="synSpecial">:</span> <span class="synPreProc">[UIApplicationLaunchOptionsKey: Any]</span>?) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
initWCSession()
}
<span class="synPreProc">func</span> <span class="synIdentifier">initWCSession</span>() {
<span class="synStatement">if</span> WCSession.isSupported() {
wcSession.delegate <span class="synIdentifier">=</span> <span class="synIdentifier">self</span>
wcSession.activate()
print(<span class="synConstant">"session activate"</span>)
} <span class="synStatement">else</span> {
print(<span class="synConstant">"session error"</span>)
}
}
</pre>
<p>Watch Extension側</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>
<span class="synPreProc">let</span> <span class="synIdentifier">wcSession</span> <span class="synIdentifier">=</span> WCSession.<span class="synStatement">default</span>
<span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">awake</span>(withContext context<span class="synSpecial">:</span> <span class="synType">Any</span>?) {
<span class="synIdentifier">super</span>.awake(withContext<span class="synSpecial">:</span> <span class="synType">context</span>)
<span class="synComment">// WCSessionの開始</span>
<span class="synStatement">if</span> WCSession.isSupported() {
wcSession.delegate <span class="synIdentifier">=</span> <span class="synIdentifier">self</span>
wcSession.activate()
sendMessageToApp()
}
}
<span class="synPreProc">func</span> <span class="synIdentifier">sendMessageToApp</span>(){
<span class="synStatement">guard</span> wcSession.isReachable <span class="synStatement">else</span> {
<span class="synStatement">return</span>
}
print(<span class="synConstant">"sendMessageToParent()"</span>)
<span class="synPreProc">let</span> <span class="synIdentifier">message</span> <span class="synIdentifier">=</span> [ <span class="synConstant">"toParent"</span> <span class="synSpecial">:</span> <span class="synConstant">"OK"</span> ]
wcSession.sendMessage(message, replyHandler<span class="synSpecial">:</span> { replyDict <span class="synStatement">in</span>
print(replyDict)
}, errorHandler<span class="synSpecial">:</span> { error <span class="synStatement">in</span>
print(error)
})
}
</pre>
<p>今回上述しているエラーは、このprintで表示したエラーの中身です。</p>
<h2>対応方法</h2>
<p>今回の原因は、メッセージを受信する(iPhoneのAppDelegate側)方のレシーバーの処理が足りていませんでした。</p>
<p>AppDelegateに以下のメソッドを追加する事で、レシーバーがメッセージを受信できるようになるので、正常にデータを送信することができるようになります。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">session</span>(_ session<span class="synSpecial">:</span> <span class="synType">WCSession</span>, didReceiveMessage message<span class="synSpecial">:</span> <span class="synPreProc">[String : Any]</span>, replyHandler<span class="synSpecial">:</span> <span class="synType">@escaping</span> ([String <span class="synSpecial">:</span> <span class="synType">Any</span>]) <span class="synSpecial">-></span> <span class="synType">Void</span>) {
replyHandler([<span class="synConstant">"reply"</span> <span class="synSpecial">:</span> <span class="synConstant">"OK"</span>])
}
</pre>
project-unknown
TIME HACKER Version 1.1.0 ウィジェット対応と新規アイコンを追加しました
hatenablog://entry/10257846132612917968
2018-08-21T00:16:57+09:00
2018-08-21T00:16:57+09:00 いつもTIME HACKERをご利用いただきありがとうございます。 今回のバージョン1.1.0で以下の対応を行いました。 ウィジェットでも記録できるようになりました。 ご要望のあったアイコンを追加しました。 ウィジェットでも記録できるようになりました。 ウィジェットにTIME HACKERを表示していただく事で、アプリを起動しなくても行動の記録が取れるようになりました。 これにより、更に日々の行動が記録しやすくなりましたので、是非ともご利用ください! ウィジェットに表示されるアクションは、アプリに登録してあるアクションの上から6番目までが表示されます。 また、記録を計測している場合は、以下のよ…
<p>いつもTIME HACKERをご利用いただきありがとうございます。</p>
<p>今回のバージョン1.1.0で以下の対応を行いました。</p>
<ul>
<li>ウィジェットでも記録できるようになりました。</li>
<li>ご要望のあったアイコンを追加しました。</li>
</ul>
<h1>ウィジェットでも記録できるようになりました。</h1>
<p>ウィジェットにTIME HACKERを表示していただく事で、アプリを起動しなくても行動の記録が取れるようになりました。</p>
<p>これにより、更に日々の行動が記録しやすくなりましたので、是非ともご利用ください!</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180821/20180821000617.png" alt="f:id:project-unknown:20180821000617p:plain" title="f:id:project-unknown:20180821000617p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>ウィジェットに表示されるアクションは、アプリに登録してあるアクションの上から6番目までが表示されます。</p>
<p>また、記録を計測している場合は、以下のような画面となります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180821/20180821000820.png" alt="f:id:project-unknown:20180821000820p:plain" title="f:id:project-unknown:20180821000820p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>このように2件までの記録中のアクションが表示されます。</p>
<p>また、ストップボタンを押すことで、計測を終了することも出来ます。</p>
<h1>ご要望のあったアイコンを追加しました</h1>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180821/20180821001325.png" alt="f:id:project-unknown:20180821001325p:plain" title="f:id:project-unknown:20180821001325p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>今回は育児系のアイコンのご要望を戴いたので、6種類のアイコンをご用意致しました!<br/>
是非ご利用ください!</p>
<hr />
<p>まだTIME HACKERをお使いでなければ、是非ともこの機会にTIME HACKERをご利用ください!</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
<hr />
<p><a href="http://www.project-unknown.jp/timehacker/jp/help/index">ヘルプページ</a><br/>
<a href="https://goo.gl/forms/btmuW8EGwQ8TyzzO2">ご意見・ご要望</a><br/>
<a href="http://www.project-unknown.jp/archive/category/TIME%20HACKER">TIME HACKER関連記事</a></p>
project-unknown
Firebaseのエラーへの対処 - Terminating app due to uncaught exception 'com.firebase.core', reason: 'Default app has already been configured.
hatenablog://entry/10257846132611146227
2018-08-16T00:46:52+09:00
2019-08-31T19:41:26+09:00 はじめに iOSのFirebaseでは、以下を二重に呼ぶ事は禁止されています(クラッシュします) FirebaseApp.configure() この時のクラッシュの内容は以下のようになります。 Terminating app due to uncaught exception 'com.firebase.core', reason: 'Default app has already been configured. しかし、TodayExtension等、回避しづらいケースがあり、その解決法です。 FirebaseApp.app()を使う 要はFirebaseApp.configure()は…
<h2>はじめに</h2>
<p>iOSのFirebaseでは、以下を二重に呼ぶ事は禁止されています(クラッシュします)</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>FirebaseApp.configure()
</pre>
<p>この時のクラッシュの内容は以下のようになります。</p>
<pre class="code" data-lang="" data-unlink>Terminating app due to uncaught exception 'com.firebase.core', reason: 'Default app has already been configured.</pre>
<p>しかし、TodayExtension等、回避しづらいケースがあり、その解決法です。</p>
<h2>FirebaseApp.app()を使う</h2>
<p>要はFirebaseApp.configure()は、FirebaseAppのオブジェクトを生成し、設定を行う事ですので、 <br/>
インスタンスが生成されていないかをチェックすれば回避出来ます。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>
<span class="synPreProc">import</span> Firebase
<span class="synPreProc">class</span> <span class="synType">TodayViewController</span><span class="synSpecial">:</span> <span class="synType">UIViewController</span>, NCWidgetProviding {
<span class="synStatement">override</span> <span class="synPreProc">func</span> <span class="synIdentifier">viewDidLoad</span>() {
<span class="synComment">// 省略</span>
<span class="synStatement">if</span> FirebaseApp.app() <span class="synIdentifier">==</span> <span class="synConstant">nil</span> {
FirebaseApp.configure()
}
<span class="synComment">// 省略</span>
}
}
</pre>
project-unknown
TIME HACKER 伝わる不具合の報告方法とは
hatenablog://entry/10257846132605119261
2018-08-01T07:10:09+09:00
2018-08-14T12:10:26+09:00 はじめに どうも、どうまずです。 今回の新アプリのTIME HACKERでは、テストを中心にプロジェクトへの参加をさせてもらいました。 テストで一番大変だったのは・・・不具合の報告です。 メールで不具合を報告したのに、伝わらない。 不具合一覧の内容が何を書いているのかわからない。 そんな場合に参考にして下さい。 はじめに 不具合報告の大変さ 画面名、機能名を適当に書いている 不確定要素が含まれる不具合報告 文章の不具合報告 テキストハレーションを生まないようにする。 再テストの結果は早めに 結論 不具合報告の大変さ なんで、不具合報告って大変なんでしょうか・・・ 今までの不具合報告の内容から、…
<h2 id="はじめに">はじめに</h2>
<p>どうも、どうまずです。<br>
今回の新アプリの<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>では、テストを中心にプロジェクトへの参加をさせてもらいました。<br></p>
<p>テストで一番大変だったのは・・・不具合の報告です。<br></p>
<ul>
<li>メールで不具合を報告したのに、伝わらない。</li>
<li>不具合一覧の内容が何を書いているのかわからない。</li>
</ul>
<p>そんな場合に参考にして下さい。</p>
<ul class="table-of-contents">
<li><a href="#はじめに">はじめに</a></li>
<li><a href="#不具合報告の大変さ">不具合報告の大変さ</a><ul>
<li><a href="#画面名機能名を適当に書いている">画面名、機能名を適当に書いている</a></li>
<li><a href="#不確定要素が含まれる不具合報告">不確定要素が含まれる不具合報告</a></li>
<li><a href="#文章の不具合報告">文章の不具合報告</a></li>
<li><a href="#テキストハレーションを生まないようにする">テキストハレーションを生まないようにする。</a></li>
<li><a href="#再テストの結果は早めに">再テストの結果は早めに</a></li>
</ul>
</li>
<li><a href="#結論">結論</a></li>
</ul>
<h2 id="不具合報告の大変さ">不具合報告の大変さ</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180729/20180729194226.png" alt="f:id:project-unknown:20180729194226p:plain" title="f:id:project-unknown:20180729194226p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>なんで、不具合報告って大変なんでしょうか・・・<br>
今までの不具合報告の内容から、何個かパターンを出して見ました。</p>
<h4 id="画面名機能名を適当に書いている">画面名、機能名を適当に書いている</h4>
<p>例えば、TIME HACKERを例にしますと、</p>
<hr />
<p>レポート画面でレポートが表示されない。</p>
<hr />
<p>こんな感じの不具合報告です。<br>
この報告で何が一番困るかというと、<br>
「レポート画面ってどこのこといっているの?」<br>
って話になります。</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>をお使いの方はわかるかと思いますが、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>には</p>
<ul>
<li>レポート-行動履歴</li>
<li>レポート-アクション毎</li>
<li>レポート-時間管理</li>
</ul>
<p>と3つのレポート画面があり、どの画面をさしているのかよくわからないのです。</p>
<p>簡単な話かもしれませんが、意外と出来ていない場合が多い失敗報告例です。</p>
<h4 id="不確定要素が含まれる不具合報告">不確定要素が含まれる不具合報告</h4>
<p>場合によっては、不具合解消までの時間が大幅に伸びるパターンです。<br>
これは、過去私が会社で報告された内容です。</p>
<hr />
<p>他に何も作業していないのに、A機能の処理が遅延した。</p>
<hr />
<p>という報告ですが、ログを確認すると、裏でDBバックアップをやっており、遅くなっていたという問題でした。<br>
このように確認していないけど、さも確認したかのような報告がされてしまうと、不具合の解析が大幅に遅れ、最悪迷宮入りです。</p>
<p>不具合を報告するときは、確実な情報を伝えましょう。<br></p>
<ul>
<li>理論的に動かない</li>
<li>仕様的に動かない</li>
</ul>
<p>という場合、ログなどで「動いていないこと」を確認しましょう。<br>
だって、理論的に仕様的に完璧だと思って動かして、発生した不具合なのですから。<br></p>
<p>また、SEならば、</p>
<ul>
<li>ユーザからの報告</li>
</ul>
<p>これも不確定要素です。<br>
ユーザはシステムの細部までわかっていない場合が多いので、誤った内容になることが多いです。<br>
そのため、ユーザからの報告はヒアリングしたり、実際に動かして確認を取らないといけません。</p>
<h4 id="文章の不具合報告">文章の不具合報告</h4>
<p>これは実際に<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>で私どうまずが報告した内容を例に見て見ましょう。</p>
<hr />
<p>レポート時間管理の下部の日付(6/15)をタップして、<br>
リスト形式で「確保したいアクション」を表示している画面に移動して、<br>レポート時間管理画面に戻ると、日付が6/16になっている。</p>
<hr />
<p>・・・自分でも分かり難い。<br>
不具合を発見した当初はこれでも分かりやすい報告をしたつもりなのですが・・・<br></p>
<p>この報告は複数の動作をした結果、日付が正しくなかったと言っています。<br>
しかし、複数の動作を1つの文にしてしまったため、どんな動作をしたのか分かりにくいです。<br>
また、1つの文の中に不具合の内容も書いているので、結局何が不具合なのか分かりません。</p>
<p>もちろん、この不具合報告は「何いっているかわからない」という結果になってしまいました。</p>
<p>そのため、下記のように書き換えました。</p>
<hr />
<p>1.レポート時間管理画面で日付をタップして6/15に変更<br>
2.レポート時間管理画面のグラフの下の日付6/15をタップ<br>
3.時間がリスト形式で表示される画面に遷移<br>
4.レポート時間管理画面に戻る<br>
5.レポート時間管理画面の日付が今日日付(6/16)になってる。<br></p>
<p>5の日付は6/15が正しいと思います。</p>
<hr />
<p>箇条書きにすることで、以下のメリットがあります。</p>
<ul>
<li>動作を1つずつ記載できる。</li>
<li>不具合の再現がし易い。</li>
<li>機械的に事実を淡々と記載することができる。</li>
</ul>
<p>文章にすると、文章に含まれる意味や内容を読み解く必要があります。<br>
箇条書きにすることで、事実のみを淡々と報告していることが伝わり、余計な労力は不要となります。</p>
<p><a href="//af.moshimo.com/af/c/click?a_id=1092401&p_id=966&pc_id=1260&pl_id=13912&guid=ON" target="_blank" rel="nofollow"><img src="//image.moshimo.com/af-img/0304/000000013912.png" width="728" height="90" style="border:none;"></a><img src="//i.moshimo.com/af/i/impression?a_id=1092401&p_id=966&pc_id=1260&pl_id=13912" width="1" height="1" style="border:none;"></p>
<h4 id="テキストハレーションを生まないようにする">テキストハレーションを生まないようにする。</h4>
<p>テキストのやりとりは相手の顔が見えません。<br>
そのため、余計なハレーション(悪影響)を及ぼすことがあります。</p>
<p>これも実際の例です。</p>
<p>レポート時間管理のグラフが正しく表示されない不具合があり、その報告をslackで正しく表示されない画像を送付して、</p>
<hr />
<p>こんな風になっちゃったよ</p>
<hr />
<p>と、何気なく送ってしまったのです。<br>
その後、</p>
<hr />
<p>これは、煽ってるの?</p>
<hr />
<p>と返信があり、全力で謝罪しました。</p>
<p>不具合報告は、嫌な言い方をすると、<br>
相手の失敗を見つけて、指摘する、かなりネガティブな報告なのです。<br>
かなり注意して報告しましょう。</p>
<p>あと、新人の時は、バグという言葉を使いたがる傾向にあるようですが、</p>
<p>「バグってる」</p>
<p>という報告は、悪く言うと「お前、ミスってんぞ」と同意義です。<br>
軽々しく使うとテキストハレーション発生ですから要注意です。<br>
(新人のときはバグと思っても自分が知らない仕様があると思って、「仕様か確認して下さい」と言った方がいいです。)</p>
<h4 id="再テストの結果は早めに">再テストの結果は早めに</h4>
<p>不具合報告が終え、ソースを修正してもらったら、修正が正しく行われているか再テストをして、やっと不具合修正が完了です。</p>
<p>「ソース修正しました」</p>
<p>という報告を受けた側は、「修正終わったんだ〜」くらいに捉えてしまうことが多いです。</p>
<p>しかし、修正した側は</p>
<ul>
<li>他に想定外や考慮不足の点がないか</li>
<li>修正した結果、他の不具合を発生しないか</li>
<li>そもそも、修正した箇所とは別のところで不具合が発生してるんじゃ</li>
</ul>
<p>と、気が気ではありません。</p>
<p>速やかに再テストを実施し、結果を報告しましょう。<br></p>
<h2 id="結論">結論</h2>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180729/20180729194352.png" alt="f:id:project-unknown:20180729194352p:plain" title="f:id:project-unknown:20180729194352p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>不具合報告をするときは、次のことを気をつけましょう</p>
<ul>
<li>画面名・機能名は正確に記載する。</li>
<li>不確定要素は記載しない。(確認してから記載する。)</li>
<li>箇条書きで機械的に記載する。</li>
<li>不具合報告はテキストハレーションを起こしやすいから要注意する。</li>
<li>再テストの結果は速やかに報告する。</li>
</ul>
<p>これだけで、かなりわかりやすい内容になると思います。<br>
ぜひ参考にしてみて下さい。</p>
<p>最後に・・・</p>
<p>記載したようなハレーションや問題が数々ありましたが、<br>
「いいものを作りたい」という目的を共有し、メンバー一丸となり完成させた<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>をぜひ使ってみてください!</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Ftimehacker%2Fjp%2Findex" title="TIME HACKER - 時間管理 - Project Unknown" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/timehacker/jp/index">www.project-unknown.jp</a></cite></p>
project-unknown
TIME HACKER メイキング
hatenablog://entry/17391345971648902325
2018-07-22T17:42:11+09:00
2019-08-31T19:55:31+09:00 TIME HACKERリリースしました! TIME HACKERをめでたくリリースすることができました。 まだの人は是非この機会にお試しください! 今回は、TIME HACKERを作るにあたり、その制作過程について記載していこうと思います。 今回の記事では、 どんなUX思想で作ったのか? 設計思想はどうしたのか? アプリ実装面での設計は? マネタイズについて 上記を、書ける範囲で載せましたので、 これからアプリを作ろうとしている人へは、制作過程の参考に、 同業者の方へは、苦難の共感が得られれば幸いです。 TIME HACKERリリースしました! TIME HACKERを作ることとなった動機 時…
<h2 id="TIME-HACKERリリースしました"><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>リリースしました!</h2>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>をめでたくリリースすることができました。<br/>
まだの人は是非この機会にお試しください!</p>
<p>今回は、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>を作るにあたり、その制作過程について記載していこうと思います。<br/>
今回の記事では、</p>
<ul>
<li>どんなUX思想で作ったのか?</li>
<li>設計思想はどうしたのか?</li>
<li>アプリ実装面での設計は?</li>
<li>マネタイズについて</li>
</ul>
<p>上記を、書ける範囲で載せましたので、
これからアプリを作ろうとしている人へは、制作過程の参考に、<br/>
同業者の方へは、苦難の共感が得られれば幸いです。</p>
<ul class="table-of-contents">
<li><a href="#TIME-HACKERリリースしました">TIME HACKERリリースしました!</a></li>
<li><a href="#TIME-HACKERを作ることとなった動機">TIME HACKERを作ることとなった動機</a><ul>
<li><a href="#時間が欲しい">時間が欲しい</a></li>
<li><a href="#ライフハック大全">ライフハック大全</a></li>
</ul>
</li>
<li><a href="#仕様検討">仕様検討</a><ul>
<li><a href="#コアバリューを決める">コアバリューを決める</a></li>
<li><a href="#ターゲット層を考える">ターゲット層を考える</a></li>
<li><a href="#UXを検討する">UXを検討する</a></li>
<li><a href="#UIを検討する">UIを検討する</a></li>
</ul>
</li>
<li><a href="#マネタイズを検討する">マネタイズを検討する</a><ul>
<li><a href="#バナー広告">バナー広告</a></li>
<li><a href="#インタースティシャル広告">インタースティシャル広告</a></li>
</ul>
</li>
<li><a href="#実装開始">実装開始</a><ul>
<li><a href="#プロトタイプ開発">プロトタイプ開発</a></li>
<li><a href="#DBシステムの変更">DBシステムの変更</a></li>
<li><a href="#アイコンをやっぱり全部自分らで造ることにする">アイコンをやっぱり全部自分らで造ることにする</a></li>
<li><a href="#始まるエターナル化">始まるエターナル化</a></li>
<li><a href="#終わらないローカライズ">終わらないローカライズ</a></li>
<li><a href="#最後の追い込み">最後の追い込み</a></li>
</ul>
</li>
<li><a href="#くらうリジェクト">くらうリジェクト</a></li>
<li><a href="#初回リリースで諦めた事">初回リリースで諦めた事</a><ul>
<li><a href="#通知機能">通知機能</a></li>
<li><a href="#Apple-Watch">Apple Watch</a></li>
<li><a href="#Widget対応">Widget対応</a></li>
<li><a href="#分析機能の何点か">分析機能の何点か</a></li>
<li><a href="#アプリ内課金">アプリ内課金</a></li>
</ul>
</li>
<li><a href="#KPT">KPT</a><ul>
<li><a href="#Keep">Keep</a></li>
<li><a href="#Problem">Problem</a></li>
<li><a href="#Try">Try</a></li>
</ul>
</li>
<li><a href="#最後に">最後に</a></li>
</ul>
<h2 id="TIME-HACKERを作ることとなった動機">TIME HACKERを作ることとなった動機</h2>
<h3 id="時間が欲しい">時間が欲しい</h3>
<p>世の中のサラリーマンの殆どがそうだと思いますが、とにかく時間が無い。<br/>
平日に、スキルアップのための勉強や、アプリ造りを行いたくても、家に着くのが日付をまたがる時間帯になる為、中々実施することができません。</p>
<p>休日にやろうと思うのですが、、いざ休日になったら平日の疲れを癒やすために、ガッツリ休んでしまう等、自分のやりたいことがやれない毎日でした。</p>
<p>もう社会人生活も10年を超えてきて、一向に改善しない毎日に諦めの気持ちすら抱いていた際に、一冊の本と出会いました。</p>
<h3 id="ライフハック大全">ライフハック大全</h3>
<p>ふと手にとった、以下の「<a target="_blank" href="//af.moshimo.com/af/c/click?a_id=1085367&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2F%25E3%2583%25A9%25E3%2582%25A4%25E3%2583%2595%25E3%2583%258F%25E3%2583%2583%25E3%2582%25AF%25E5%25A4%25A7%25E5%2585%25A8%25E2%2580%2595%25E2%2580%2595%25E2%2580%2595%25E4%25BA%25BA%25E7%2594%259F%25E3%2581%25A8%25E4%25BB%2595%25E4%25BA%258B%25E3%2582%2592%25E5%25A4%2589%25E3%2581%2588%25E3%2582%258B%25E5%25B0%258F%25E3%2581%2595%25E3%2581%25AA%25E7%25BF%2592%25E6%2585%25A3250-%25E5%25A0%2580-%25E6%25AD%25A3%25E5%25B2%25B3-ebook%2Fdp%2FB0779KV65Z" rel="nofollow">ライフハック大全―人生と仕事を変える小さな習慣250</a><img src="//i.moshimo.com/af/i/impression?a_id=1085367&p_id=170&pc_id=185&pl_id=4062" alt="" width="1" height="1" style="border: 0px;" />」という本が私の中で革命的でした。</p>
<p><a target="_blank" href="//af.moshimo.com/af/c/click?a_id=1085367&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2F%25E3%2583%25A9%25E3%2582%25A4%25E3%2583%2595%25E3%2583%258F%25E3%2583%2583%25E3%2582%25AF%25E5%25A4%25A7%25E5%2585%25A8%25E2%2580%2595%25E2%2580%2595%25E2%2580%2595%25E4%25BA%25BA%25E7%2594%259F%25E3%2581%25A8%25E4%25BB%2595%25E4%25BA%258B%25E3%2582%2592%25E5%25A4%2589%25E3%2581%2588%25E3%2582%258B%25E5%25B0%258F%25E3%2581%2595%25E3%2581%25AA%25E7%25BF%2592%25E6%2585%25A3250-%25E5%25A0%2580-%25E6%25AD%25A3%25E5%25B2%25B3-ebook%2Fdp%2FB0779KV65Z" rel="nofollow"><img src="https://images-fe.ssl-images-amazon.com/images/I/41N4-9n5NKL._SL160_.jpg" alt="" style="border: none;" /></a><img src="//i.moshimo.com/af/i/impression?a_id=1085367&p_id=170&pc_id=185&pl_id=4062" alt="" width="1" height="1" style="border: 0px;" /></p>
<p>何個もあるライフハックは、殆どが私の血肉になる位に、良い知識を仕入れる事ができたのですが、その中でも、最も私の中でためになったのは、</p>
<p>「<strong>自分の1日の行動ログを取って、無駄に過ごしているところは無いか?を見返すことで時間を捻出する術</strong>」</p>
<p>これにものすごく感銘を受け、その時のツイートが以下。</p>
<p><blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ライフハック大全読んだ。<br>どれも良かったけど個人的に「時間は分•秒まで正確に意識する」が革命的で、数日全ての行動を秒単位まで測定して振り返ると無理なく余裕な時間が生まれて衝撃だった。<br>まさにHappy Lifehacking!<a href="https://twitter.com/hashtag/%E3%83%A9%E3%82%A4%E3%83%95%E3%83%8F%E3%83%83%E3%82%AF%E5%A4%A7%E5%85%A8?src=hash&ref_src=twsrc%5Etfw">#ライフハック大全</a></p>— ゆう@あんのうん (@YuwUnknown) <a href="https://twitter.com/YuwUnknown/status/941318481332416512?ref_src=twsrc%5Etfw">2017年12月14日</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p>
<p><blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">こういう自分の行動をラップタイムで測れる便利なアプリないものか…探して無ければApple Watchアプリとして作る(°▽°)<a href="https://twitter.com/hashtag/%E3%83%A9%E3%82%A4%E3%83%95%E3%83%8F%E3%83%83%E3%82%AF%E5%A4%A7%E5%85%A8?src=hash&ref_src=twsrc%5Etfw">#ライフハック大全</a></p>— ゆう@あんのうん (@YuwUnknown) <a href="https://twitter.com/YuwUnknown/status/941318876511444992?ref_src=twsrc%5Etfw">2017年12月14日</a></blockquote><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script></p>
<p>実際にこのライフハックを実施してみました。<br/>
私は1日の中で、以下の時間をもっと取りたいと考えています。</p>
<ul>
<li>読書をして自己啓発と勉強</li>
<li>PCを使ってプログラムや新技術の勉強</li>
<li>個人アプリ、ゲームの開発</li>
</ul>
<p>この観点を元に、1日の行動ログを記録し、時間を生み出すことが出来ないか分析してみます。<br/>
(メモ帳に走り書きしていた為、時間が入っていたり入っていなかったりしてます。)</p>
<blockquote><ol>
<li>8:00 起床</li>
<li>出る準備 20分 8:20</li>
<li>家を出てからバスにならぶ 10分 8:31</li>
<li>バス乗車時間 13分 8:45</li>
<li>電車待ち時間 16分 9:02</li>
<li>特急 30分 乗り換え後、○○から△△ 20分 計50分 9:50</li>
<li>以降会社</li>
<li>エレベーター待ちと朝の準備 16分 10:06</li>
<li>タバコ 5分</li>
<li>インシデント対応 41分 10:53</li>
<li>今日のタスクプランニング 9分 11:00</li>
<li>○○ミーティング 24分 11:27</li>
<li>トイレとタバコ 15分 11:45</li>
<li>今年度のプロジェクト目標について議論 15分 12:00</li>
<li>昼飯と移動 30分 12:30</li>
<li>タバコ 15分 12:45</li>
<li>目標についての議論 15分 13:00</li>
<li>溜まっていたSlackやり取り 21分</li>
<li>タバコ 15分 13:40</li>
<li>雑務 15分</li>
<li>新規企画のKPI試算 vol1 45分 14:40</li>
<li>タバコ 10分</li>
<li>新規企画のKPI試算 vol2 48分 15:45</li>
<li>タバコ10分</li>
<li>部下面談 36分</li>
<li>タバコ 10分</li>
<li>○○チーム向けプレゼン資料作成 10分</li>
<li>△△MTG 40分</li>
<li>☓☓MTG 1時間30分</li>
<li>タバコと帰る準備 10分 19:30</li>
<li>○○まで移動 20分</li>
<li>△△まで 35分</li>
<li>ジムで運動 1時間</li>
<li>ジムの風呂 30分</li>
<li>買い物 10分</li>
<li>バス停まで移動 5分</li>
<li>バス待ちとバス乗車 18分</li>
<li>タバコ 5分</li>
<li>家移動2分</li>
<li>家に着いて落ち着くまで 7分</li>
<li>ダラダラ過ごす 2時間</li>
<li>風呂とダラダラ 30分</li>
<li>就寝 1:17</li>
</ol>
</blockquote>
<p>この日を振り返って見ると、例えば、以下を改善できそうです。</p>
<p><strong>タバコ時間を読書時間に企てる</strong></p>
<p>タバコの時間をタバコ + スキルアップのための読書(Kindle)を行うとしたら、80分読書時間が確保出来ます。<br/>
全てのタバコの時間を企てるのは難しいのと、読書の開始時には思い返しの時間も必要な事を鑑みても、50分は読書時間が確保できます。</p>
<p><strong>移動時間を読書・勉強時間に企てる</strong></p>
<p>また、バスの時間がトータル31分。電車の時間は特急が65分、在来線が40分。<br/>
バスと在来線は基本立つので、ここを読書に当てると71分。</p>
<p>特急は座ることが出来ますが、片道30分程度なので開発するには時間が足りなく見えるので、PCを使った勉強時間に企てると、65分勉強時間が確保できます。</p>
<p>ここまで考えると、家に帰るまでに、以下の時間を確保することがわかります。</p>
<ul>
<li>読書時間を121分</li>
<li>勉強に65分</li>
</ul>
<p>ただ、この勉強・読書時間は断片化した時間なので、効率は悪いですが、それでも家に変えるまでに、これだけの事が行えるので、</p>
<p><strong>家では、アプリ開発に集中することができる。いやアプリ開発だけしていていいのです。</strong></p>
<p>これまでだと、「勉強もしないと」「本も読まないと…」と考えがよぎって中々集中出来なかったのですが、<br/>
一日の無駄な時間、スキマ時間に十分に勉強を行えているので、家でやる必要が無いのです。</p>
<p>そして、家でダラダラとする時間を2時間から半分削るだけで、1時間確保出来ます。<br/>
更にストイックに削れば1時間半〜2時間確保できるでしょう。</p>
<p>ここまで見てわかるように、</p>
<p><strong> 1日の時間を細かく記録する事と、分析する事は、忙しいビジネスマンに取って、ものすごく有益である</strong></p>
<p>ことがわかります。</p>
<p>そして、有益で有ることがわかった次は、もっと楽に記録したい・分析したいと思い、今回の「<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>」アプリに繋がります。</p>
<p>私自身の生活の質を向上させるために、どうしてもこの手のアプリが欲しかったのと、私の中でしっくり来るアプリがStoreに並んでいなかったので、何としてでも手に入れたかった。</p>
<p>気がつけば、全ての開発や勉強を停止して、着手していました。</p>
<p> (なんだかんだで7ヶ月も造ってた…)</p>
<p><br></p>
<h2 id="仕様検討">仕様検討</h2>
<h3 id="コアバリューを決める">コアバリューを決める</h3>
<p>いつもは凄い悩む所だったのですが、ここは作り出した動機が動機なので、すぐに固まっていました。</p>
<p>アプリ制作初期は</p>
<p>「<strong>無駄な時間を管理し、自分のスキルアップに企てたい時間を確保する</strong>」</p>
<p>で、最後までここは変わらなかったのですが、<br/>
もっと範囲を広げて、以下を最終的なコアバリューとしました。</p>
<p>「<strong>自分の時間を管理し、生活改善のサポートをするアプリ</strong>」</p>
<h3 id="ターゲット層を考える">ターゲット層を考える</h3>
<p>ターゲット層は、女性でも・男性でもありませんし、<br/>
特段ペルソナも考えていません。</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>では、以下のユーザーをターゲットとして捉えています。</p>
<ul>
<li>気づいたら時間ばかり過ぎている人</li>
<li>忙しくて自分の時間が確保出来ない人</li>
<li>自分のスキルアップの時間を確保したい人</li>
<li>日々の生活をもっと有意義に送りたい人</li>
</ul>
<h3 id="UXを検討する">UXを検討する</h3>
<p>大体のアプリに言えることですが、<br/>
このアプリはUXが特に肝となります。</p>
<ul>
<li>何より、毎日、しかも小刻みにアプリを立ち上げて行動のログを計測しないといけない。</li>
<li>それには、ユーザーのアプリ上の動作だけではなく、生活のリズムを一瞬でも妨げてはいけない。</li>
<li>特に、行動ログを記録忘れた場合は、一気に記録するモチベーションが下がります。なので記録忘れを防がなければならない。
<ul>
<li>(結局、ここのサポート部分は1stリリースからは見送りました。)</li>
</ul>
</li>
</ul>
<p>上記をいろいろ考えてUXを造っている際に、取っていたメモが以下です。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180528/20180528223801.png" alt="f:id:project-unknown:20180528223801p:plain" title="f:id:project-unknown:20180528223801p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180528/20180528223811.png" alt="f:id:project-unknown:20180528223811p:plain" title="f:id:project-unknown:20180528223811p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>結局、当時考えていた機能の半分くらいしか実現できていないのですが…。</p>
<h3 id="UIを検討する">UIを検討する</h3>
<p>ここもUX並にメチャクチャ苦労しました。<br/>
なんだかんだで、2ヶ月近く、あーでもないこーでもないをやっていたんじゃないかしら。</p>
<p>ちなみに、最初期の頃に考えていたUI</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180713/20180713010102.png" alt="f:id:project-unknown:20180713010102p:plain:w400" title="f:id:project-unknown:20180713010102p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>全く違いますね…。<br/>
今見ると、個人的に結構しんどいUIですが、当時はこれでいける!!と信じて止みませんでした。<br/>
結局、<strong>2日位、寝かせてから再度この画像を見て、くっそだせぇって思って、やり直しました</strong>。<br/>
その時の温度感だけで決めるのではなく、一度寝かせるのは大事ですね。</p>
<p>次に考えたのが、以下のUIです。<br/>
文字情報を見るのではなくて、行動をアイコン化し、なれたら無意識で記録ができる事を狙っています。<br/>
また、無意識で押せる事を狙って、アイコンも結構大きめに設定してます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180713/20180713010403.png" alt="f:id:project-unknown:20180713010403p:plain:w400" title="f:id:project-unknown:20180713010403p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<p>だいぶ、今の原型が見え隠れしていますね。<br/>
ここから、紆余曲折を経て、以下の様な画面で落ち着きました。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180713/20180713010500.png" alt="f:id:project-unknown:20180713010500p:plain:w400" title="f:id:project-unknown:20180713010500p:plain:w400" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p>
<h2 id="マネタイズを検討する">マネタイズを検討する</h2>
<p>TIME HACKERで、現在実装済みのマネタイズをご紹介します。<br/>
今後のマネタイズは敢えて載せません。<br/>
お金の匂いがプンプンする機能が追加されたら、「あっ、マネタイズに走ってるな」とでも思ってください。</p>
<ul>
<li>アプリトップにバナー広告</li>
<li>一定の回数計測を行った際のインタースティシャル</li>
</ul>
<h3 id="バナー広告">バナー広告</h3>
<p>バナー広告は、<a href="https://keyholder.page.link/page">KeyHolder</a>でも実装しているのと、<a href="https://keyholder.page.link/page">KeyHolder</a>でも操作の邪魔にならないところに設置していますが、結構馬鹿にならないくらいの収益を上げてくれます。<br/>
UXの所でも述べていますが、<strong><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>は、UXがとにかく命です。ユーザの行動を阻害する事をしようものなら、記録が面倒になり一気に使わなくなります</strong>。<br/>
なので、バナー広告を設置するにあたり、アプリの操作を阻害する事はしないような画面設計にしています。</p>
<h3 id="インタースティシャル広告">インタースティシャル広告</h3>
<p><a href="https://keyholder.page.link/page">KeyHolder</a>では、インタースティシャル広告を出さずに本当に公開しました。<br/>
というか、<a href="https://keyholder.page.link/page">KeyHolder</a>はシンプルを極めすぎてインタースティシャル広告を入れる余地が無く、入れようものならアプリとしての価値を大きく欠損しかねるので、断念しました。</p>
<p>なので、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>ではインタースティシャルは仕様検討の総初期から、なんとかして入れられないかを深く検討しています。<br/>
(書けば書くほど、金儲けが全面に出てしまうので、引かれるリスクはありますが、それでもこの記事を記載しています)</p>
<p>バナー広告の項でも記載しましたが、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>は何よりUXが命なので、インタースティシャル広告を入れる箇所には最新の注意を払っています。</p>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>でインタースティシャスは、一定の時間、一定の回数、行動ログを取った際に、行動ログを記録したタイミングで広告表示を行っています。<br/>
これは、ユーザーとして考えた際に、行動ログを計測する直前に広告が表示されたら、ユーザーの行動を阻害するリスクが非常に高まる為、記録を開始したタイミングで広告を表示することにしました。</p>
<p>行動ログを記録した後に広告を表示しているので、マネタイズとしての期待値は大きく下がると思いますが、私が考えている一番のリスクは、</p>
<p><strong>眼の前の収益に目がくらんで、ユーザの行動を阻害する事で、アプリをアンインストールされる事を一番のリスクとして捉えています</strong></p>
<p>また、上記で一定の回数と記載しましたが、初回リリースまでにどの回数が妥当なのか?の結論を出すことが出来ませんでした。</p>
<p>ですので、FirebaseのA/Bテストを採用して、インタースティシャル広告を出すタイミングの検証を行っています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Ffirebase-abtest" title="Firebase A/Bテストを試す - Firebase A/B Testing - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/firebase-abtest">www.project-unknown.jp</a></cite></p>
<p>ここまでは、アプリ設計のお話でした。<br/>
次に、実装面でのお話をしていこうと思います。</p>
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<p><ins class="adsbygoogle"
style="display:block; text-align:center;"
data-ad-layout="in-article"
data-ad-format="fluid"
data-ad-client="ca-pub-3376977478675716"
data-ad-slot="7215657309"></ins></p>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>
<h2 id="実装開始">実装開始</h2>
<p>さぁ、ここからお祭りの始まりです。<br/>
いやぁ…とにかく紆余曲折しまくった…。</p>
<h3 id="プロトタイプ開発">プロトタイプ開発</h3>
<p>今回のアプリ開発では、特に真新しい事を中心に、やりたいことは何でもやろうと思い色々tryしました。</p>
<p>大きい所で言うと、</p>
<p>Carthageにも挑戦しましたし</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Fios-carthage" title="High SierraでCarthageをinstallする (RealmSwiftをinstallする) - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/ios-carthage">www.project-unknown.jp</a></cite></p>
<p>Quickにも挑戦しましたし</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Fquick-swift" title="Quickを使ってSwiftコードのユニットテストを行う - Carthageからの利用 - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/quick-swift">www.project-unknown.jp</a></cite></p>
<p>RealmSwiftにも挑戦しました</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Fcoredata-realmswift" title="RealmSwift vs CoreData - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/coredata-realmswift">www.project-unknown.jp</a></cite></p>
<p>Firebaseにメインの解析機能を移管しましたし、<br/>
Firebaseを使ってA/Bテストにも挑戦しています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Ffirebase-abtest" title="Firebase A/Bテストを試す - Firebase A/B Testing - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/firebase-abtest">www.project-unknown.jp</a></cite></p>
<p>以下の「<a target="_blank" href="//af.moshimo.com/af/c/click?a_id=1085367&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2F%25E3%2580%2590%25E9%259B%25BB%25E5%25AD%2590%25E5%2590%2588%25E6%259C%25AC%25E7%2589%2588%25E3%2580%2591Code-Complete-%25E7%25AC%25AC2%25E7%2589%2588-%25E5%25AE%258C%25E5%2585%25A8%25E3%2581%25AA%25E3%2583%2597%25E3%2583%25AD%25E3%2582%25B0%25E3%2583%25A9%25E3%2583%259F%25E3%2583%25B3%25E3%2582%25B0%25E3%2582%2592%25E7%259B%25AE%25E6%258C%2587%25E3%2581%2597%25E3%2581%25A6-Steve-McConnell-ebook%2Fdp%2FB01E5DYK1C" rel="nofollow">Code Complete</a><img src="//i.moshimo.com/af/i/impression?a_id=1085367&p_id=170&pc_id=185&pl_id=4062" alt="" width="1" height="1" style="border: 0px;" />」等の技術書も読み漁り、開発設計についての色々な挑戦も行いました。</p>
<p><a target="_blank" href="//af.moshimo.com/af/c/click?a_id=1085367&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2F%25E3%2580%2590%25E9%259B%25BB%25E5%25AD%2590%25E5%2590%2588%25E6%259C%25AC%25E7%2589%2588%25E3%2580%2591Code-Complete-%25E7%25AC%25AC2%25E7%2589%2588-%25E5%25AE%258C%25E5%2585%25A8%25E3%2581%25AA%25E3%2583%2597%25E3%2583%25AD%25E3%2582%25B0%25E3%2583%25A9%25E3%2583%259F%25E3%2583%25B3%25E3%2582%25B0%25E3%2582%2592%25E7%259B%25AE%25E6%258C%2587%25E3%2581%2597%25E3%2581%25A6-Steve-McConnell-ebook%2Fdp%2FB01E5DYK1C" rel="nofollow"><img src="https://images-fe.ssl-images-amazon.com/images/I/51Sdan3jlwL._SL160_.jpg" alt="" style="border: none;" /></a><img src="//i.moshimo.com/af/i/impression?a_id=1085367&p_id=170&pc_id=185&pl_id=4062" alt="" width="1" height="1" style="border: 0px;" /></p>
<p>そして気づいたのが、</p>
<p><strong>tryばっかしていて、いつまで経っても完成しない</strong></p>
<p>特に、開発設計のところは、色々な本を読みすぎて、読む本全てに影響を受けて設計が日々変わりすぎていっているのが色々と足を引っ張りました。<br/>
こんなところにもエターナル現象は顔をだすんですね。</p>
<p>なので、プロトタイプとしてある程度のものが出来上がったタイミングで、tryは封印し、アプリ完成にフォーカスする事にしました。</p>
<p>以後は、アプリ制作時に大きな節目について記載します。</p>
<h3 id="DBシステムの変更">DBシステムの変更</h3>
<p>これまでDBをRealmSwiftで開発していたのですが、<br/>
諸々の事情で、結局CoreDataに移しました。</p>
<p>RealmSwiftはCoreDataと違い、Objectを生成して値を突っ込むだけで物理領域にも保存され、直感的にデータを扱うことができる優れもので、開発当初はRealmSwiftを崇拝してやまなかったです。</p>
<p>しかし、</p>
<ul>
<li>DeleteRuleが使えない</li>
<li>マイグレーション時に、意図しないデータが潜り込む</li>
<li>トランザクション範囲が操作し辛い</li>
</ul>
<p>等、不満が次々と湧いてきて、最終的にCoreDataにデータを移管しました。</p>
<p>ココらへんは、<a href="http://www.project-unknown.jp/entry/coredata-realmswift">ここに詳細を記載しています</a>。</p>
<p>ただ、RealmSwiftとCoreDataとでは、やはりアプリ設計が異なってくる為、ここを直すために、結構作り直しを余儀なくされました。</p>
<p>(それでもDAOを全て直す気力は無く、一部のDAOはRealmSwift前提とした設計のままになってしまっています)</p>
<h3 id="アイコンをやっぱり全部自分らで造ることにする">アイコンをやっぱり全部自分らで造ることにする</h3>
<p>開発完了が大きく遅れたのは、ここが原因です。ですがこれは英断だと思っています。</p>
<p>これまで、アイコンをフリーで提供してくださっているサイト様から使わせてもらうつもりでおり、完成直前までこれで行くつもりまんまんでした。</p>
<p>ただ、せっかく造るんだからアイコンも自作する事に決め、ぽぽたが泣きながら作業開始しました。</p>
<p>元々100を超えるアイコンを他サイト様のフリー素材から用意していたのですが、<br/>
流石に、この量を開発過渡期に用意するのは無理があったので、数を70ほどにしぼりました。</p>
<p><a href="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716161615.png" class="http-image" target="_blank"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716161615.png" class="http-image" alt="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716161615.png" width="430"></a><br/>
(作成したアイコン一覧)</p>
<p>この辺の詳細については、ぽぽたが詳細の記事に起こしています。</p>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Fentry%2Ftimehacker-making-design" title="デザイン視点でTIME HACKER - Project Unknown" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/entry/timehacker-making-design">www.project-unknown.jp</a></cite></p>
<h3 id="始まるエターナル化">始まるエターナル化</h3>
<p>今回一番厄介だったエターナル化は、<strong>機能のエターナル化より、コードのエターナル化</strong>でした。<br/>
一つの機能を造る度に、4,5回リファクタリングを行ったり。<br/>
一度仕上げたコードを、設計しなおしたり。<br/>
自分が満足するまで何度も何度も作り直してました。</p>
<p>ただ、これって、</p>
<p><strong>システムの保守性だけ上がって、いつまで経ってもリリースできなくなる自体を引き起こしていました</strong>。</p>
<p>確かに保守性は非常に大事なのですが、とにかく時間がかかる。<br/>
プライベート開発なので納期を気にしないというのが更に拍車をかけてしまっていました。</p>
<p>塩梅を調整するのが本当に難しい…。<br/>
このリファクタリングも最終的には自分の中で最低限に抑えるようにしました。</p>
<p>例えば、以下の時だけリファクタリングを行うようにしています。</p>
<ul>
<li>DRYを守れていない</li>
<li>SOLIDの原則に準拠していない</li>
<li>ViewからModelクラスの細かいメソッドにまでアクセスしようとした時</li>
</ul>
<p>観点は以下です。</p>
<p><br></p>
<p><strong>DRYを守れていない</strong></p>
<p>似た機能が乱立した場合、開発過渡期になればなるほど、修正漏れによるさらなる不具合、進捗の遅れに響きます。<br/>
ここは徹底的に潰しました。<br/>
ちなみにDRYが有名になったのは、以下の「<a target="_blank" href="//af.moshimo.com/af/c/click?a_id=1085367&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2F%25E6%2596%25B0%25E8%25A3%2585%25E7%2589%2588-%25E9%2581%2594%25E4%25BA%25BA%25E3%2583%2597%25E3%2583%25AD%25E3%2582%25B0%25E3%2583%25A9%25E3%2583%259E%25E3%2583%25BC-%25E8%2581%25B7%25E4%25BA%25BA%25E3%2581%258B%25E3%2582%2589%25E5%2590%258D%25E5%258C%25A0%25E3%2581%25B8%25E3%2581%25AE%25E9%2581%2593-Andrew-Hunt%2Fdp%2F427421933X" rel="nofollow">達人プログラマー</a><img src="//i.moshimo.com/af/i/impression?a_id=1085367&p_id=170&pc_id=185&pl_id=4062" alt="" width="1" height="1" style="border: 0px;" />」からですね。この概念は道を踏み外しそうになった際の羅針盤になり得る考えでいつも大いに助けられています。</p>
<p><a target="_blank" href="//af.moshimo.com/af/c/click?a_id=1085367&p_id=170&pc_id=185&pl_id=4062&url=https%3A%2F%2Fwww.amazon.co.jp%2F%25E6%2596%25B0%25E8%25A3%2585%25E7%2589%2588-%25E9%2581%2594%25E4%25BA%25BA%25E3%2583%2597%25E3%2583%25AD%25E3%2582%25B0%25E3%2583%25A9%25E3%2583%259E%25E3%2583%25BC-%25E8%2581%25B7%25E4%25BA%25BA%25E3%2581%258B%25E3%2582%2589%25E5%2590%258D%25E5%258C%25A0%25E3%2581%25B8%25E3%2581%25AE%25E9%2581%2593-Andrew-Hunt%2Fdp%2F427421933X" rel="nofollow"><img src="https://images-fe.ssl-images-amazon.com/images/I/51aDNpMj8hL._SL160_.jpg" alt="" style="border: none;" /></a><img src="//i.moshimo.com/af/i/impression?a_id=1085367&p_id=170&pc_id=185&pl_id=4062" alt="" width="1" height="1" style="border: 0px;" /></p>
<p><br></p>
<p><strong>SOLIDの原則に準拠していない</strong></p>
<p>iOSライクなMVVMとかではなく、またはプロトコル指向でもなく、オブジェクト指向の1つの考えであるSOLIDをとにかく大事にしました。</p>
<p>プロトコル指向で統一したかった所はありますが、Swiftのプロトコル指向は熟成されていません。これを守るがあまりに無駄で汚いコードになりがちです(会社の現場でこれに囚われて、意味不明なコードを書くエンジニアを散々見ています)。</p>
<p>また、何でもかんでもMVVMを採用するのは愚の骨頂です。ただクラスを増やすだけで、自己満足だけ満たされるコードになるでしょう。<br/>
こういうのは適材適所であるべきです。</p>
<p>なので、SOLIDの原則だけを準拠することにしています。<br/>
これはDRYを包括している思想ですので、DRYルールと非常に相性が良い。</p>
<p>以下は、SOLIDの原則について記載しています。</p>
<ul>
<li>S - 単一責任の原則 (Single Responsibility Principle)</li>
<li>O - 開放・閉鎖原則 (Open/closed principle)</li>
<li>L - リスコフ置換原則 (Liskov substitution principle)</li>
<li>I - インタフェース分離の原則 (Interface segregation principle)</li>
<li>D - 依存性逆転の原則 (Dependency inversion principle)</li>
</ul>
<p>当たり前だけど、気がついたら守れていない原則ですが、ちゃんと守れている時の効果は絶大です。<br/>
このルールには非常に気を使いました。</p>
<p><br></p>
<p><strong> ViewからModelクラスの細かいメソッドにまでアクセスしようとした時 </strong></p>
<p>これはSOLIDの一種ですね。<br/>
ViewからModelのデータをごにょごにょするのはイケていません。<br/>
その場はそれで良いかもしれませんが、後ほど似たようなデータアクセスが必要になる度にインタフェースを作る羽目になります。</p>
<p><br></p>
<h3 id="終わらないローカライズ">終わらないローカライズ</h3>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>は日本以外の世界で発信しています。<br/>
細かく言うと、<a href="http://www.project-unknown.jp/entry/gdpr-app">GDPR</a>を受け、EU諸国を除いた全世界に発信しています。</p>
<p>アプリ内のローカライズは、元々ローカライズを意識した実装を行う癖があったので、さほど問題無かったのですが、<br/>
問題は、ブログに紹介やらヘルプやらをやたらと<strong>親切に記載してしまっていた為、これら全てをローカライズする必要ができました</strong>。</p>
<p>いやぁー、これはアプリをリリースするのを辞めるか悩むくらい霹靂しました。<br/>
最後の1ヶ月間は、このローカライズを延々とやっていた気がします。<br/>
実際にアプリ以外のローカライズは以下を対応しました。</p>
<ul>
<li><a href="http://www.project-unknown.jp/timehacker/en/help/index">ヘルプページ - 英語版</a></li>
<li><a href="http://www.project-unknown.jp/timehacker/en/index">プロモーション - 英語版</a></li>
<li><a href="https://docs.google.com/forms/d/e/1FAIpQLSdWrDcpehCMOJ9eYxx02XB2bQRkcS4rrgQQmH_fB8KbR6TR5g/viewform">お問い合わせフォーム - 英語版</a></li>
</ul>
<p>繰り返しますが、、</p>
<p><strong>アプリのローカライズ以上に、このローカライズの方が遥かにしんどかったです</strong>。</p>
<p>しかも、後述しますが、最終的にこの1ヶ月の作業は殆ど無駄になり、申請日にプロジェクトメンバー総掛かりで作り直してます。</p>
<h3 id="最後の追い込み">最後の追い込み</h3>
<p>具体的な日付だと、2018/7/15(土)にプロジェクトメンバーで集まり、<br/>
最終テスト、ヘルプページやプロモーションページの仕上げを行って、<br/>
夜までに申請を上げて打ち上げを想定していたのですが…、</p>
<p><strong>相次ぐ不具合</strong><br/>
<strong>まさかの、申請日にUI変更</strong><br/>
<strong>UI変更に引っ張られて、全ての画像変更</strong></p>
<p>が発生し、気づいたら申請したのは、2018/7/16(日)の夜でした…。<br/>
特に、UI変更が本当にしんどかった…。<br/>
これにより、具体的には、</p>
<ul>
<li><a href="http://www.project-unknown.jp/timehacker/jp/index">日本語版プロモーションページ</a>の画像総取り替え</li>
<li><a href="http://www.project-unknown.jp/timehacker/en/index">英語版プロモーションページ</a>の画像総取り替え</li>
<li><a href="http://www.project-unknown.jp/timehacker/jp/help/index">日本語版ヘルプ</a>の画像総取り替え</li>
<li><a href="http://www.project-unknown.jp/timehacker/en/help/index">英語版ヘルプ</a>の画像総取り替え</li>
</ul>
<p>以下の、ストアキャプチャ作り直し</p>
<p><strong>英語版iPhoneX</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716013728.png" alt="f:id:project-unknown:20180716013728p:plain" title="f:id:project-unknown:20180716013728p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p><strong>英語版5.5 inch</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716013744.png" alt="f:id:project-unknown:20180716013744p:plain" title="f:id:project-unknown:20180716013744p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p><strong>日本語版iPhoneX</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716013811.png" alt="f:id:project-unknown:20180716013811p:plain" title="f:id:project-unknown:20180716013811p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p><strong>日本語版5.5inch</strong></p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716013830.png" alt="f:id:project-unknown:20180716013830p:plain" title="f:id:project-unknown:20180716013830p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>というか、最後の2日間、殆ど画像の差し替えしかやってない気がする…。</p>
<p><br></p>
<h2 id="くらうリジェクト">くらうリジェクト</h2>
<p>今書いているブログ記事もそうですが、<br/>
申請を待っている間に、私含め、ぽぽたもTIME HACKER関連の記事を色々書き溜め、申請が通ったらアプリのリリースも含め、ブログ記事のリリースをするために待ち構えていましたが、<strong>ものの見事にリジェクトをくらいました</strong>。</p>
<p>内容は</p>
<pre class="code" data-lang="" data-unlink>2. 4 Performance: Hardware Compatibility
Guideline 2.4.1 – Performance – Hardware Compatibility</pre>
<p>iPadでの表示が崩れているから、ちゃんとUI整えろよというご指摘です。</p>
<p>実は、リリース前に<strong>iPadで表示が崩れる事を知っていました</strong>し、<strong>Appleの審査する人はiPadで審査する</strong>ということも知っていました。</p>
<p>(iPhoneアプリであっても、iPadに内蔵されているiPhoneシミュレータで動くべきであるという考えなので、私が知る限りでは、iPadで審査を行っていそうです。)</p>
<p>ですが、<strong>まぁ、操作できない事ないじゃん</strong>という謎の過信で、対応を見送っていたところを、案の定指摘されました。</p>
<p>もう、目も当てられない…。</p>
<p>こちら、OSSを使ってUI表現していた所だったので、<a href="http://www.project-unknown.jp/entry/2017/08/27/022747">ライセンス</a>の問題もあり、手を加えるわけにもいきませんから、その場しのぎとして、表示崩れが発生する端末では、一部機能を削除して再申請しました。</p>
<h2 id="初回リリースで諦めた事">初回リリースで諦めた事</h2>
<p>たくさんあります。<br/>
本当にたくさんあります。</p>
<p>何故か申請日にiTunes Connectの調子が悪くて、iTunes Connectにバイナリアップロードするのに数時間かかった為、<br/>
<strong>心が盛大にポッキリ折れて</strong>、気づいていた簡単なバグはそのままにしてしまっていたり、</p>
<p>UXの検討の時に記録忘れ防止のための通知機能も、<br/>
通知タイミングを間違えたらユーザーの不満がものすごく上がりそうという、<strong>チキンハート</strong>がひょっこり顔を出して見送ったり、</p>
<p>本当に色々積み残しています。</p>
<p>ざっくりと列挙すると、以下の機能を初回リリースでは諦めています。</p>
<h3 id="通知機能">通知機能</h3>
<p>上述した通りです</p>
<h3 id="Apple-Watch">Apple Watch</h3>
<p>Twitterに思いっきりApple Watch化すると記載しているのに、工数の兼ね合いという<strong>大人の都合</strong>で諦めてます。</p>
<h3 id="Widget対応">Widget対応</h3>
<p><strong>大人の都合</strong></p>
<h3 id="分析機能の何点か">分析機能の何点か</h3>
<p>最終的に<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>の押しにしていきたいのですが、<br/>
まずは、ユーザーの行動ログを貯めることが何より優先すべき事だったので、ここの機能も結構諦めています。</p>
<h3 id="アプリ内課金">アプリ内課金</h3>
<p>やりたかった…。<br/>
<strong>本当にやりたかった…。</strong><br/>
できなかった…。</p>
<h2 id="KPT">KPT</h2>
<p>締めくくりとして、今回のTIME HACKERのKPTを考えたいと思います。</p>
<h3 id="Keep">Keep</h3>
<p><strong>自分の信念を最後まで貫いた</strong></p>
<p>ライフハック大全を読んで、是非このアプリを使いたい、そして造りたいと言う思いを最後まで貫けたのは良かった。</p>
<p>このアプリは、<strong>万人受けするアプリではない</strong>という事はわかっています。<br/>
しかし、ターゲット層項目に記載した人に長く使ってもらう事だけを想定して造っています。</p>
<p><strong>エターナル化を防いだ</strong></p>
<p>本当はAppleWatch、Widget、分析機能の強化、Push等もっとやりたかったけど、これを全て捨てました。<br/>
初回リリースでは、<strong>ユーザーのログを貯めることを何より重要視し、それ以外の機能は二の次にしました</strong>。<br/>
結果として、この判断を行わなければ、リリースは来年になっていたでしょう。</p>
<h3 id="Problem">Problem</h3>
<p><strong>何でもかんでもやりすぎた</strong></p>
<p>最低限の事だけしかやっていないつもりだったのですが、今考えると色々手を広げすぎたと思います。</p>
<p>特にローカライズはリリース後でも良かったかもしれません…が、iTunes Connectの仕組み上、プライマリー言語を最初から英語に指定していないと、後々更に面倒な事になっていたので、これでよかったのかもしれませんが…。</p>
<p>またヘルプやプロモーションはやりすぎました、これこそリリース後でも良かった。</p>
<p><strong>至る所でチキンハートが顔を出した</strong></p>
<p>RealmSwiftをやめたのも結局の所、チキンハートが問題でしたし、<br/>
チキンハートが顔を出したせいで、ハレーションを生みそうな機能を見送ったり、もっと強気に造っても良かった。</p>
<p><strong>プロジェクトとして、集まって作業する頻度が少なすぎた</strong></p>
<p>最近はリモートワークが流行っていますが、やはり対面でやり取りする作業だと、それぞれのクオリティ・速度が全然違います。</p>
<p>特に申請日に集まって、最後のテストを行った際の不具合の出方がやばかったです。<br/>
もうちょっと早めに集まってテストするなどすれば、問題の早期発見に気づけたでしょう。</p>
<p><strong>明らかにリスクがあるとわかっていた不具合を放置した</strong></p>
<p>最後のリジェクトの所ですね。<br/>
これは本当にもう愚かだったとしか言いようが無い。<br/>
面倒くさがって、対応を先送りにした結果としか言いようが無い。</p>
<h3 id="Try">Try</h3>
<p><strong>プロジェクトとして集まる頻度を増やす</strong></p>
<p>これはProblemの裏返しですね、クオリティ・速度を上げる意味で、無理にでも集まって集中して造る時間を確保したほうが良いです。</p>
<p><strong>チキンハートをどうにかする</strong></p>
<p>なにかする度に、いや契約が…とかいや請求一気にきたら怖いしー…とか、<br/>
出す前にでもでもだってを発揮して、チャンスをことごとく逃しているのがとにかくもったいない。<br/>
<strong>やる前に後悔するなら、やって後悔する</strong>の精神を持とうと思います。</p>
<p><strong>多少面倒でも正しいことをする</strong></p>
<p>これは最後のProblemの不具合と気づいていて目を背けたものに掛かります。<br/>
結局バグの放置や不具合の放置は、問題の先送りでしか無いですし、別な言い方をすれば、<strong>ローンと同じで先送りにすれば金利(より面倒な工数)が増える</strong>ものとし、リスクがあるものに関しては、速攻潰していくマインドは持ち続けなければと痛感しました。</p>
<h2 id="最後に">最後に</h2>
<p>自分たちで言うのも変な話ですが、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>は、自分の時間を生み出す為に有用なツールに仕上がっています。<br/>
また、ユーザーの声を聞いて、機能追加やアイコン追加も行っていこうと思いますので、気になる事などありましたら、いつでも、以下のフォームよりお問い合わせください!</p>
<p><a href="https://docs.google.com/forms/d/e/1FAIpQLSfyoostK-qOEqkA-s3-48UxSFXu6UWDOoXXsHf95bwl38sAsw/viewform">TIME HACKER ご要望・お問い合わせ</a></p>
<p>是非、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>を使って、毎日の生活を少しでも改善していきましょう!</p>
project-unknown
RealmSwift vs CoreData
hatenablog://entry/17391345971647493140
2018-07-22T16:03:57+09:00
2019-08-31T19:39:48+09:00 はじめに iOSのローカルデータベースとして、これまでは、CoreDataが主流でした。 CoreDataの他だと直接SQLiteを弄ったり、ラッパーとなるFMDBなどのOSSがありましたが、Appleが提供している機能として、やはりCoreDataのシェア率は高かったと思います。 実際に、以下はAppleが公開している資料ですが、 CoreDataを採用する事で、純粋にSQLを叩くよりスピードが上がるや、メモリ効率も良い等、夢のようなツールとして紹介されています。 しかし、CoreDataは学習コストが非常に高く、扱えるようになるまで時間が掛かったり、 扱えるようになったとしても、他のDBM…
<h2>はじめに</h2>
<p>iOSのローカルデータベースとして、これまでは、CoreDataが主流でした。<br/>
CoreDataの他だと直接SQLiteを弄ったり、ラッパーとなるFMDBなどのOSSがありましたが、Appleが提供している機能として、やはりCoreDataのシェア率は高かったと思います。</p>
<p>実際に、以下はAppleが公開している資料ですが、<br/>
CoreDataを採用する事で、純粋にSQLを叩くよりスピードが上がるや、メモリ効率も良い等、夢のようなツールとして紹介されています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180717/20180717010316.png" alt="f:id:project-unknown:20180717010316p:plain:w430" title="f:id:project-unknown:20180717010316p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>しかし、CoreDataは学習コストが非常に高く、扱えるようになるまで時間が掛かったり、 扱えるようになったとしても、他のDBMSライクに扱おうとしたらどハマりしたりと、非常にクセが強いです。</p>
<p>iOSアプリの現場で、CoreDataを採用しているPJ等に配属された際に、困った人も多いのではないでしょうか。</p>
<p>そんな中登場したのが、RealmSwiftです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180717/20180717010114.png" alt="f:id:project-unknown:20180717010114p:plain:w430" title="f:id:project-unknown:20180717010114p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>Realmは、公式サイトから紹介を引用すると</p>
<blockquote><p>Realm Swiftはアプリケーションのモデル層を効率的に安全で迅速な方法で記述することができます。</p></blockquote>
<p>実際に使ってみた感想だと、とにかく実装が楽です。<br/>
あまり考えなくてもデータの永続化もでき、UserDefaultsみたいなお手軽感があります。</p>
<p>しかし、</p>
<p>結論から先に言うと、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>アプリを開発中に、私はCoreDataを選択し、RealmSwiftベースで作ってきたアプリの構造をCoreDataに移しました。</p>
<p>他のサイトでも、RealmSwiftとCoreDataの比較がなされているところが多いですが、私の主観でも書いてみようと思います。</p>
<h2>リレーションシップをやろうとすると面倒</h2>
<p>RealmSwiftでリレーションシップライクな事をやろうとすると、以下のように記述します。</p>
<p><strong>FirstEntity</strong></p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> UIKit
<span class="synPreProc">import</span> RealmSwift
<span class="synPreProc">class</span> <span class="synType">FirstEntity</span><span class="synSpecial">:</span> <span class="synType">Object</span> {
<span class="synType">@objc</span> dynamic <span class="synPreProc">var</span> <span class="synIdentifier">id</span> <span class="synIdentifier">=</span> NSUUID().uuidString
<span class="synType">@objc</span> dynamic <span class="synPreProc">var</span> <span class="synIdentifier">second</span><span class="synSpecial">:</span> <span class="synType">SecondEntity</span>? <span class="synIdentifier">=</span> <span class="synConstant">nil</span>
<span class="synStatement">override</span> <span class="synPreProc">static</span> <span class="synPreProc">func</span> <span class="synIdentifier">primaryKey</span>() <span class="synSpecial">-></span> <span class="synType">String</span>? {
<span class="synStatement">return</span> <span class="synConstant">"id"</span>
}
}
</pre>
<p><strong>SecondEntity</strong></p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> UIKit
<span class="synPreProc">import</span> RealmSwift
<span class="synPreProc">class</span> <span class="synType">ActionEntity</span><span class="synSpecial">:</span> <span class="synType">Object</span> {
<span class="synType">@objc</span> dynamic <span class="synPreProc">var</span> <span class="synIdentifier">id</span> <span class="synIdentifier">=</span> NSUUID().uuidString
<span class="synPreProc">let</span> <span class="synIdentifier">firstEntity</span> <span class="synIdentifier">=</span> LinkingObjects(fromType<span class="synSpecial">:</span> <span class="synType">FirstEntity.self</span>, property<span class="synSpecial">:</span> <span class="synConstant">"second"</span>) <span class="synComment">// FirstEntityがぶら下がる</span>
}
</pre>
<p>上記のように書くと、SecondEntityにFirstEntityがぶら下がる形となり、<br/>
SecondEntityにFirstEntityを登録する事で、データを一気に登録・参照する事が可能になります。</p>
<p>これだけなら、非常に便利なのですが、 削除する時(Delete Rule)が問題になります。<br/>
RealmSwiftでは、DeleteRuleを設定することができないため、他のDBMSにあるように、親となるデータを削除した際に、子のデータも一緒に削除することはできません。<br/>
(私が開発していた時に、気づいていなかっただけなら申し訳ありません。)<br/>
ですので、RealmSwiftでデータ削除する際に、親も子も手動で削除していく必要があります。</p>
<p>反面、CoreDataの場合だと、</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716180641.png" alt="f:id:project-unknown:20180716180641p:plain:w430" title="f:id:project-unknown:20180716180641p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>こうする事で、親のデータを消したら、子のデータまで一気に消えます。</p>
<p><br></p>
<h2>トランザクションが分かりにくい</h2>
<p>これは完全に慣れの問題ですが、RealmSwiftの場合、コードブロック内でトランザクションが貼られます。</p>
<p>逆にいうとトランザクションから外れてコードを書くと思わぬところでデータ更新が走って詰まった事がありました。</p>
<p>CoreDataのコンテキスト内に更新ロジックを書いて、最後にsaveする書き方に慣れてしまったので、ここは凄く違和感を感じました。</p>
<p><br></p>
<h2>マイグレーションに違和感がある</h2>
<p>例えば以下の構成があったとします。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> UIKit
<span class="synPreProc">import</span> RealmSwift
<span class="synPreProc">class</span> <span class="synType">FirstEntity</span><span class="synSpecial">:</span> <span class="synType">Object</span> {
<span class="synType">@objc</span> dynamic <span class="synPreProc">var</span> <span class="synIdentifier">id</span> <span class="synIdentifier">=</span> NSUUID().uuidString
<span class="synType">@objc</span> dynamic <span class="synPreProc">var</span> <span class="synIdentifier">updateDate</span>? <span class="synIdentifier">=</span> Date()
}
</pre>
<p>これにデータを突っ込んで、<br/>
アップデートのタイミングで、以下のようにカラム名を修正します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> UIKit
<span class="synPreProc">import</span> RealmSwift
<span class="synPreProc">class</span> <span class="synType">FirstEntity</span><span class="synSpecial">:</span> <span class="synType">Object</span> {
<span class="synType">@objc</span> dynamic <span class="synPreProc">var</span> <span class="synIdentifier">id</span> <span class="synIdentifier">=</span> NSUUID().uuidString
<span class="synType">@objc</span> dynamic <span class="synPreProc">var</span> <span class="synIdentifier">createdDate</span>? <span class="synIdentifier">=</span> Date()
}
</pre>
<p>この時、これまで入っていたDateが初期化されて、1970年が入ってました。</p>
<p>単純にマイグレーションする際の考慮漏れはありますが、CoreDataの場合だとここでクラッシュして気付けます。</p>
<p>RealmSwiftの場合だとデータを登録して結果を見て初めて気付くと言うところが個人的にはしんどいです。</p>
<p>実際に個人で開発しているときは、コアな箇所だとユニットテストを書いたり、ある程度のテストをおこないますが、それでも余暇の時間を使うのもあってテスト漏れは発生しやすいです。</p>
<p>この時に気付けるのか?というのがCoreDataを採用する1番の動機となりました。</p>
<p><br></p>
<h2>締めくくり</h2>
<p>結局のところで言うと、RealmSwiftに慣れていなくて、CoreDataは散々使ってきたので慣れていた。<br/>
が、CoreDataを採用した1番の理由です。</p>
<p>RealmSwiftは触ったから分かる素晴らしいものですが、私の学習が追いつかなかった。<br/>
RealmSwiftはサーバとのデータのやり取りも非常に楽に実装できるよう、サポートされていますし、いずれは使いこなせるにしていきたい限りです。</p>
<h2>最後に</h2>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="TIME HACKER" /></a></p>
<p>記事の途中で記載しましたが、今回の記事は、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>を開発中の記事となります。</p>
<p>是非お使いください!</p>
project-unknown
Firebase A/Bテストを試す - Firebase A/B Testing
hatenablog://entry/10257846132596978916
2018-07-21T17:29:56+09:00
2019-08-31T19:39:48+09:00 Firebase A/Bテスト Firebaseが提供しているA/Bテストを利用した記事となります。 この記事では、A/Bテストとは?から、 実際にTIME HACKERアプリで、インタースティシャルの表示頻度を確認する際に、A/Bテストを導入しているので、実際に実装した際の手順・コードをご紹介します。 Firebase A/Bテスト A/Bテストとは? A/Bテストはどうやるのか? 何のA/Bテストを実施するか? インタースティシャルの頻度を何故測るのか? TIME HACKERでのA/Bテストで見る数値 アプリの定着率 広告のクリック数 AdMobの推定収益 TIME HACKERでのイン…
<h2 id="Firebase-ABテスト">Firebase A/Bテスト</h2>
<p>Firebaseが提供しているA/Bテストを利用した記事となります。</p>
<p>この記事では、A/Bテストとは?から、
実際に<a href="https://timehacker.page.link/page">TIME HACKER</a>アプリで、インタースティシャルの表示頻度を確認する際に、A/Bテストを導入しているので、実際に実装した際の手順・コードをご紹介します。</p>
<ul class="table-of-contents">
<li><a href="#Firebase-ABテスト">Firebase A/Bテスト</a></li>
<li><a href="#ABテストとは">A/Bテストとは?</a></li>
<li><a href="#ABテストはどうやるのか">A/Bテストはどうやるのか?</a></li>
<li><a href="#何のABテストを実施するか">何のA/Bテストを実施するか?</a><ul>
<li><a href="#インタースティシャルの頻度を何故測るのか">インタースティシャルの頻度を何故測るのか?</a></li>
<li><a href="#TIME-HACKERでのABテストで見る数値">TIME HACKERでのA/Bテストで見る数値</a><ul>
<li><a href="#アプリの定着率">アプリの定着率</a></li>
<li><a href="#広告のクリック数">広告のクリック数</a></li>
<li><a href="#AdMobの推定収益">AdMobの推定収益</a></li>
</ul>
</li>
<li><a href="#TIME-HACKERでのインタースティシャル表示制限のABテスト設計">TIME HACKERでのインタースティシャル表示制限のA/Bテスト設計</a></li>
</ul>
</li>
<li><a href="#Firebase-ABテストを用いたABテストを実施する">Firebase A/Bテストを用いたA/Bテストを実施する</a><ul>
<li><a href="#ABテストの準備をコンソール上で行う">A/Bテストの準備をコンソール上で行う。</a></li>
<li><a href="#アプリの設定を行う">アプリの設定を行う。</a></li>
<li><a href="#Firebase-コンソールの準備を続けテスト開始する">Firebase コンソールの準備を続け、テスト開始する</a></li>
<li><a href="#アプリ側でABテスト用のコードを埋め込む">アプリ側でA/Bテスト用のコードを埋め込む</a><ul>
<li><a href="#注意点">注意点</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#締めくくり">締めくくり</a></li>
<li><a href="#最後に">最後に</a></li>
</ul>
<p>最初に、A/Bテストの説明や、<a href="https://timehacker.page.link/page">TIME HACKER</a>でのA/Bテスト設計について論じています。<br/>
実装方法だけを確認したいのであれば、<a href="#Implementation">こちら</a>から御覧ください。</p>
<h2 id="ABテストとは">A/Bテストとは?</h2>
<p>A/Bテストとは、アプリやWebページ等で数パターンの機能・表示を用意して、ユーザー毎に出し分けを行う事で、<br/>
用意した数パターンの機能・表示の内、どのパターンが優れているのかを見極めるのに利用します。</p>
<p>A/Bテストは単純に機能だけの評価だけではなく、マーケティング上だとより高いCTR, CVRを得られるのか検証したり、比較的メジャーなテストです。</p>
<p>特にアプリではこのテストは非常に有用で、iOSアプリ等は、機能検証しようとしたら、毎回申請を挟まないと行けないなど、多大な時間を要するのですが、<br/>
A/Bテストの概念を採用する事で、一度の申請で複数の検証を行うことが出来ます。</p>
<h2 id="ABテストはどうやるのか">A/Bテストはどうやるのか?</h2>
<p>いろんなサービスがありますが、どこもやることは簡単です、<br/>
Webサーバにパターン毎のjsonファイルを設置し、アプリ側でそのjsonファイルを取得し、イベントログ等を送信する事で検証を行います。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716184438.png" alt="f:id:project-unknown:20180716184438p:plain:w430" title="f:id:project-unknown:20180716184438p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span><br/>
(イメージ図)</p>
<p>今回のお題としているのは、FirebaseでもA/Bテストをサポートしている為(まだβですが 2018/7/16段階)、それを採用した実例の紹介となります。</p>
<p>Firebase A/Bテストは、jsonの設置もそうですが、イベントトラッキングも全てサポートしてくれているので、一つのコンソール上でA/Bテストで必要な一連の操作ができるので非常に便利です。</p>
<h2 id="何のABテストを実施するか">何のA/Bテストを実施するか?</h2>
<p>当たり前ですが、何のA/Bテストを行うのか?はちゃんと設計しないといけません。<br/>
<a href="https://timehacker.page.link/page">TIME HACKER</a>では、冒頭でも記載しましたが、インタースティシャルの表示頻度の最適解を求める上で、A/Bテストを実施しています。</p>
<p>ここでは、<a href="https://timehacker.page.link/page">TIME HACKER</a>でのインタースティシャルA/B設計について記載します。</p>
<h3 id="インタースティシャルの頻度を何故測るのか">インタースティシャルの頻度を何故測るのか?</h3>
<p>インタースティシャルは全画面に表示される広告なので、ユーザの行動を大きく阻害します。<br/>
頻度を間違えた場合は、最悪ユーザがアプリから離脱していってしまうでしょう。<br/>
<a href="https://timehacker.page.link/page">TIME HACKER</a>ではアクションの計測を開始したタイミングでインタースティシャル広告を表示するように設計しています。<br/>
ですが、アクションを計測するのは、1番ユーザーが利用する機能です。<br/>
なのに、毎回アクションを計測する際に表示するのでは、あまりにもフラストレーションが溜まるでしょう。</p>
<p>この解決策として、インタースティシャルの表示頻度を調整します。<br/>
まず1度表示したら10分間は表示させません。(これはいたずらに広告をタップする事による、アドセンス狩り対策の意味でも行っています。)</p>
<p>しかし、10分だけで本当に良いのか?<br/>
答えはNOです。<a href="https://timehacker.page.link/page">TIME HACKER</a>では、ユーザがアクションを起こす時に起動するアプリです。<br/>
言うなれば、アクションとアクションのつなぎ目に起動してもらうアプリです。<br/>
そのアクションが、例えば</p>
<ul>
<li>スマホをいじるのであれば、次起動するのは1時間後</li>
<li>映画を見るのであれば、次起動するのは2時間後</li>
</ul>
<p>等、10分だけの制限では、ほぼ毎回記録測定のタイミングで広告が表示されてしまします。</p>
<p>ですので、10分制限は、あくまでアドセンス狩りとして捉え、ユーザーの行動を阻害しない策として、<strong>何回計測実行したのか?</strong>で表示するようにします。</p>
<h3 id="TIME-HACKERでのABテストで見る数値"><a href="https://timehacker.page.link/page">TIME HACKER</a>でのA/Bテストで見る数値</h3>
<p>以下の数値を追います。</p>
<h4 id="アプリの定着率">アプリの定着率</h4>
<p>定着は4-7日で計測します。<br/>
TIME HACKERは毎日複数回利用していただく事を想定したアプリです。<br/>
生活に密着している指標として、1日で見るのではなくて、4-7日の定着数を見ます。<br/>
ここが、インタースティシャルの頻度によって変わるのであれば、見直す必要が出てきます。</p>
<h4 id="広告のクリック数">広告のクリック数</h4>
<p>ユーザーの事を考えないのであれば、そもそもインタースティシャル広告を出さない事が1番であることは明白です。<br/>
ですが、アプリの収益で生活するなどを考えるのであれば、収益を得るのはマストでしょう。</p>
<p>なので、広告のクリック数を見る事は、絶対必要です。</p>
<h4 id="AdMobの推定収益">AdMobの推定収益</h4>
<p>こちらは、広告のクリック数でも似たような指標を取ることができますが、</p>
<h3 id="TIME-HACKERでのインタースティシャル表示制限のABテスト設計">TIME HACKERでのインタースティシャル表示制限のA/Bテスト設計</h3>
<p><a href="https://timehacker.page.link/page">TIME HACKER</a>では、以下の条件でテストを行います。</p>
<p><a name="Implementation"></a></p>
<h2 id="Firebase-ABテストを用いたABテストを実施する">Firebase A/Bテストを用いたA/Bテストを実施する</h2>
<h3 id="ABテストの準備をコンソール上で行う">A/Bテストの準備をコンソール上で行う。</h3>
<p>Firebaseコンソールから、A/B Testingを選択します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716185256.png" alt="f:id:project-unknown:20180716185256p:plain" title="f:id:project-unknown:20180716185256p:plain" class="hatena-fotolife" itemprop="image"></span></p>
<p>テストの名前や、概要、どの割合のユーザをA/Bテストの範囲に含めるのか?を設定します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701142853.png" alt="f:id:project-unknown:20180701142853p:plain:w430" title="f:id:project-unknown:20180701142853p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>今回は100%のユーザとしていますが、規模の大きなアプリ等では、全てをテスト対象にするのではなく、ごく1部のユーザをA/Bテストのターゲットとしても良いかと思います。</p>
<p>次に、バリアントを設定します。<br/>
要はA/Bテストターゲットユーザのうちに、どの割合でどんな値を設定するのか?を指定します。</p>
<p>TIME HACKERでは、インタースティシャル広告の割合を決めたいので、<br/>
25%区切りで、値を設定しています。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701145338.png" alt="f:id:project-unknown:20180701145338p:plain:w430" title="f:id:project-unknown:20180701145338p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>最後に目標を入力します。<br/>
ウォッチしたい数値をここで指定しておくことで、Firebaseが該当する数値を集計してツール上で確認できるようになります。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701145347.png" alt="f:id:project-unknown:20180701145347p:plain:w430" title="f:id:project-unknown:20180701145347p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>ここまで設定したら、次へ進みます。</p>
<p>次に進むと、今設定したA/Bテストが下書きの状態で表示されます。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701150301.png" alt="f:id:project-unknown:20180701150301p:plain:w430" title="f:id:project-unknown:20180701150301p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<h3 id="アプリの設定を行う">アプリの設定を行う。</h3>
<p>次にアプリの設定を行います。</p>
<p>plistを用意する。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701151216.png" alt="f:id:project-unknown:20180701151216p:plain:w430" title="f:id:project-unknown:20180701151216p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>アプリを実行して、tokenを確認しておく。<br/>
↓の箇所。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink>debugPrint(<span class="synConstant">"a/b test token :</span><span class="synSpecial">\(InstanceID.instanceID()</span><span class="synConstant">.token()!)"</span>)
</pre>
<h3 id="Firebase-コンソールの準備を続けテスト開始する">Firebase コンソールの準備を続け、テスト開始する</h3>
<p>テスト確認してみましょう。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701153510.png" alt="f:id:project-unknown:20180701153510p:plain:w430" title="f:id:project-unknown:20180701153510p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>今確認したTokenを入力して、テスト確認したいバリアントを設定します。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701153615.png" alt="f:id:project-unknown:20180701153615p:plain:w430" title="f:id:project-unknown:20180701153615p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>では、テストを開始しましょう。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180701/20180701153703.png" alt="f:id:project-unknown:20180701153703p:plain:w430" title="f:id:project-unknown:20180701153703p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<p>開始して間もないのでデータは無いですが、以下の画面のようにテスト実施の画面になっているはずです。</p>
<p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180716/20180716192307.png" alt="f:id:project-unknown:20180716192307p:plain:w430" title="f:id:project-unknown:20180716192307p:plain:w430" class="hatena-fotolife" style="width:430px" itemprop="image"></span></p>
<h3 id="アプリ側でABテスト用のコードを埋め込む">アプリ側でA/Bテスト用のコードを埋め込む</h3>
<p>以下、実際に<a href="https://timehacker.page.link/page">TIME HACKER</a>アプリで実際に作ったコードの一部です。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">import</span> Firebase
<span class="synPreProc">#if</span> DEBUG
fileprivate <span class="synPreProc">let</span> <span class="synIdentifier">Interval</span> <span class="synIdentifier">=</span> <span class="synConstant">0</span>
<span class="synPreProc">#else</span>
fileprivate <span class="synPreProc">let</span> <span class="synIdentifier">Interval</span> <span class="synIdentifier">=</span> <span class="synConstant">60</span> <span class="synIdentifier">*</span> <span class="synConstant">60</span> <span class="synIdentifier">*</span> <span class="synConstant">24</span>
<span class="synPreProc">#endif</span>
fileprivate <span class="synPreProc">let</span> <span class="synIdentifier">RemoteConfigKeys</span> <span class="synIdentifier">=</span> <span class="synConstant">"interstitialTimesOnTapNum"</span>
fileprivate <span class="synPreProc">let</span> <span class="synIdentifier">RemoteConfigPlist</span> <span class="synIdentifier">=</span> <span class="synConstant">"RemoteConfig-default"</span>
<span class="synPreProc">class</span> <span class="synType">InterstitialTestManager</span><span class="synSpecial">:</span> <span class="synType">NSObject</span> {
<span class="synPreProc">private</span> <span class="synPreProc">let</span> <span class="synIdentifier">remoteConfig</span> <span class="synIdentifier">=</span> RemoteConfig.remoteConfig()
<span class="synPreProc">private</span> <span class="synPreProc">static</span> <span class="synPreProc">let</span> <span class="synIdentifier">instance</span> <span class="synIdentifier">=</span> InterstitialTestManager()
<span class="synPreProc">public</span> <span class="synPreProc">class</span> <span class="synType">var</span> shared<span class="synSpecial">:</span> <span class="synType">InterstitialTestManager</span> {
<span class="synStatement">return</span> instance
}
<span class="synComment">/// ABテストのセットアップ</span>
<span class="synComment">/// 初回AppDelegateなどで1度呼ぶだけで十分</span>
<span class="synPreProc">func</span> <span class="synIdentifier">setup</span>() {
remoteConfig.setDefaults(fromPlist<span class="synSpecial">:</span> <span class="synType">RemoteConfigPlist</span>)
<span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">settings</span> <span class="synIdentifier">=</span> RemoteConfigSettings.<span class="synIdentifier">init</span>(developerModeEnabled<span class="synSpecial">:</span> <span class="synType">false</span>) {
remoteConfig.configSettings <span class="synIdentifier">=</span> settings
}
remoteConfig.fetch(withExpirationDuration<span class="synSpecial">:</span> <span class="synType">TimeInterval</span>(Interval), completionHandler<span class="synSpecial">:</span> { (status, error) <span class="synSpecial">-></span> <span class="synType">Void</span> <span class="synStatement">in</span>
<span class="synStatement">if</span> status <span class="synIdentifier">==</span> RemoteConfigFetchStatus.success {
<span class="synIdentifier">self</span>.remoteConfig.activateFetched()
} <span class="synStatement">else</span> {
print(<span class="synConstant">"fetch error."</span>)
}
})
}
<span class="synComment">/// インタースティシャルの頻度を取得します</span>
<span class="synComment">///</span>
<span class="synComment">/// - Returns:</span>
<span class="synPreProc">func</span> <span class="synIdentifier">interstitialFrequency</span>() <span class="synSpecial">-></span> <span class="synType">Int</span> {
<span class="synPreProc">var</span> <span class="synIdentifier">frequency</span> <span class="synIdentifier">=</span> <span class="synConstant">10</span>
<span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">number</span> <span class="synIdentifier">=</span> remoteConfig[RemoteConfigKeys].numberValue {
frequency <span class="synIdentifier">=</span> number.intValue
}
<span class="synStatement">return</span> frequency
}
<span class="synComment">/// インタースティシャルを表示するかを判定します。</span>
<span class="synComment">///</span>
<span class="synComment">/// - Returns:</span>
<span class="synPreProc">func</span> <span class="synIdentifier">isDisplayInterstitial</span>() <span class="synSpecial">-></span> <span class="synType">Bool</span> {
<span class="synStatement">if</span> actionRunningNumForCount() <span class="synIdentifier">></span> interstitialFrequency() {
<span class="synStatement">return</span> <span class="synConstant">true</span>
}
<span class="synStatement">return</span> <span class="synConstant">false</span>
}
}
</pre>
<p>使い方は、まずAppDelegateでセットアップを呼び出します。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">application</span>(_ application<span class="synSpecial">:</span> <span class="synType">UIApplication</span>, didFinishLaunchingWithOptions launchOptions<span class="synSpecial">:</span> <span class="synPreProc">[UIApplicationLaunchOptionsKey: Any]</span>?) <span class="synSpecial">-></span> <span class="synType">Bool</span> {
InterstitialTestManager.shared.setup()
}
</pre>
<p>後はインタースティシャルの発動条件の条件分に以下のコードで判定を埋め込むだけです。</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">func</span> <span class="synIdentifier">displayInterstitial</span>() {
<span class="synStatement">guard</span> InterstitialTestManager.shared.isDisplayInterstitial() <span class="synStatement">else</span> {
<span class="synStatement">return</span>
}
<span class="synComment">// インタースティシャル表示</span>
}
</pre>
<p>これで、A/Bテストを埋め込む事が出来ました。</p>
<h4 id="注意点">注意点</h4>
<p>載せているSampleコードの以下の部分</p>
<pre class="code lang-swift" data-lang="swift" data-unlink><span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">settings</span> <span class="synIdentifier">=</span> RemoteConfigSettings.<span class="synIdentifier">init</span>(developerModeEnabled<span class="synSpecial">:</span> <span class="synType">false</span>) {
</pre>
<p>ここの、developerModeEnabledを開発時はtrueにすることで、RemoteConfigのキャッシュタイムを好きにいじることができるようになります。<br/>
が、リリース時は、falseにして、Debugモードを解除しておいたほうが無難でしょう。</p>
<p><br></p>
<h2 id="締めくくり">締めくくり</h2>
<p>全て自分でつくろうと思うと、かなり難しいというより、面倒な作業が必要となるため、<br/>
Firebase A/Bテストを採用する事で、簡単にA/Bテストを実行する事ができ、<br/>
またイベントトラッキングはGoogle Analyticsを利用しているので、より詳細な分析も行うことができそうです。</p>
<p>まだリリースして間もないので、Firebase コンソール上でテスト結果を集計出来ていないので、結果については、このタイミングではお話することができません。</p>
<p>ある程度集計できて、新たな気付きがあれば加筆修正を行おうと思います。</p>
<h2 id="最後に">最後に</h2>
<p><a href="http://www.project-unknown.jp/timehacker/jp/index"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/project-unknown/20180710/20180710090601.png" alt="img" /></a></p>
<p>今回の記事で紹介していますが、<a href="http://www.project-unknown.jp/timehacker/jp/index">TIME HACKER</a>で、Firebase A/Bテストを採用しています。<br/>
是非お使いください!</p>
<p>written by ゆう@あんのうん</p>
project-unknown
TIME HACKER 更新情報
hatenablog://entry/10257846132601449886
2018-07-15T21:03:26+09:00
2018-07-17T19:28:26+09:00 TIME HACKER 更新情報 www.project-unknown.jp このカテゴリでは、TIME HACKERの更新情報を掲載していきます。
<h2>TIME HACKER 更新情報</h2>
<p><iframe src="https://hatenablog-parts.com/embed?url=http%3A%2F%2Fwww.project-unknown.jp%2Farchive%2Fcategory%2FTIME%2520HACKER" title="TIME HACKER カテゴリーの記事一覧 - Project Unknown" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="http://www.project-unknown.jp/archive/category/TIME%20HACKER">www.project-unknown.jp</a></cite></p>
<p>このカテゴリでは、TIME HACKERの更新情報を掲載していきます。</p>
project-unknown