ObjecTips

Swift & Objective-C で iOS とか macOS とか

Touch IDのエラーまとめ

LAContext の以下のAPI

- (void)evaluatePolicy:(LAPolicy)policy
       localizedReason:(NSString *)localizedReason
                 reply:(void(^)(BOOL success, NSError * __nullable error))reply;

このAPI指紋認証LAPolicyDeviceOwnerAuthenticationWithBiometrics を使ってみて実際に遭遇したエラー集
LocalAuthentication.framework の LAError.h に書かれているエラーの定義も合わせて確認すると良いかも。


Error Domain=com.apple.LocalAuthentication Code=-1 "Application retry limit exceeded." UserInfo={NSLocalizedDescription=Application retry limit exceeded.

Touch IDの認証失敗を繰り返して制限回数に達した時


LAPolicyDeviceOwnerAuthentication でパスコード認証中にキャンセル Error Domain=com.apple.LocalAuthentication Code=-2 "Canceled by user." UserInfo={NSLocalizedDescription=Canceled by user.}

Touch IDのシステムアラートを表示中にホームボタンを押す
またはTouch IDのシステムアラートの「キャンセル」ボタンを押した時


Error Domain=com.apple.LocalAuthentication Code=-3 "Fallback authentication mechanism selected." UserInfo={NSLocalizedDescription=Fallback authentication mechanism selected.}

Touch IDのシステムアラートを表示中に「パスコードを入力」ボタンを押した時


Error Domain=com.apple.LocalAuthentication Code=-4 "Canceled by another authentication." UserInfo={NSLocalizedDescription=Canceled by another authentication.}

Touch IDのシステムアラートを表示中に再度APIでTouch IDを表示しようとした時


Error Domain=com.apple.LocalAuthentication Code=-4 "Caller moved to background." UserInfo={NSLocalizedDescription=Caller moved to background.}

Touch IDのシステムアラートをAPIで表示するのと同時にホームボタンを押してホーム画面を表示した時


Error Domain=com.apple.LocalAuthentication Code=-4 "UI canceled by system." UserInfo={NSLocalizedDescription=UI canceled by system.}

Touch IDのシステムアラートを表示中に端末をロックした時


Error Domain=com.apple.LocalAuthentication Code=-8 "Biometry is locked out." UserInfo={NSLocalizedDescription=Biometry is locked out.}

Touch IDの認証制限回数に達した状態でAPIからTouch IDを呼んだ時


Error Domain=com.apple.LocalAuthentication Code=-1004 "User interaction is required." UserInfo={NSLocalizedDescription=User interaction is required.}

バックグラウンドにいる時にTouch IDのAPIを呼んだ時

これは - (void)applicationDidEnterBackground:(UIApplication *)application のタイミングでTouch IDのAPIを呼ぶとたまに起こる。

このエラーを避けるにはAPIの呼び出しは - (void)applicationDidBecomeActive:(UIApplication *)application で行いつつ、2重にAPIコールをしてしまわないようにAPIの呼び出し時に使った LAContextインスタンスを reply ブロックが呼ばれるまで保持したりして状態管理すると良い。

ターミナルでUUIDを生成する

iOS, macOS でプログラム内でUUIDを使う場合 CFUUIDNSUUID が使える。

プログラム外で識別子としてUUIDが欲しい時は uuidgen というコマンドが用意されているのでこれを使う。
ターミナルで

$uuidgen

結果は以下のように生成されたUUIDが表示される

14B78297-8560-4144-AFA4-41563A4BE71B

-hdr オプションを使うと CFUUID を使ったヘッダ定義用のスニペットが生成される。

$uuidgen -hdr

hdr は header の略らしい。

結果は以下

// 14B78297-8560-4144-AFA4-41563A4BE71B
#warning Change the macro name MYUUID below to something useful!
#define MYUUID CFUUIDGetConstantUUIDWithBytes(kCFAllocatorSystemDefault, 0x14, 0xB7, 0x82, 0x97, 0x85, 0x60, 0x41, 0x44, 0xAF, 0xA4, 0x41, 0x56, 0x3A, 0x4B, 0xE7, 0x1B)

このままコピペしてソースコード内で使用できるように #define が記載されている。

Google Firebase で既存の AdMob アカウントを使う

新たに作成したFirebaseアカウント(GoogleアカウントA)で今まで使っていた既存のAdMobアカウント(別のGoogleアカウントB)を引き続き使いたいというパターンの手順。
*1 *2

  • GoogleアカウントAでFirebaseにログイン
  • 新規プロジェクトを作成
  • 歯車アイコンから「権限」を選択
  • IAMを選択してプロジェクトを選択し「メンバーを追加」からGoogleアカウントBのメールアドレスと権限を入力してメンバーの追加を実行*3
  • GoogleアカウントBに招待メールが届くので承認
  • 承認するとGoogleアカウントBのFirebaseのダッシュボードが開いてGoogleアカウントAで作成したプロジェクトが表示される
  • GoogleアカウントBでAdMobにログインして「分析」タブのアプリ一覧から「Firebase にリンク」を選択
  • iOSの場合バンドルIDを入力して、そのバンドルIDがFirebaseプロジェクトで設定したものと一致していれば「既存の Firebase プロジェクトと既存の Firebase アプリにリンクする」という選択肢にデフォルトでチェックが付くので「続行」を選択

これでリンク完了
GoogleアカウントAとBどちらのFirebaseのダッシュボードでもプロジェクトのEARN AdMobを表示すると「リンク済み」となっている
あとはFirebaseのAdMob, Analytics, その他を使ってアプリへの実装を行う


権限設定やアカウントリンク機能をちゃんと使えばプロジェクト毎に異なるAdMobアカウントを使う事もできる他、プロジェクト毎にログを閲覧できる人、設定を編集できる人、ストレージにファイルをアップできる人などいろいろと制御できそう。
自分がプロジェクトに招待される側としても、複数の参加プロジェクトを1つのGoogleアカウントとダッシュボードで表示できるので便利そう。

*1:社内、社外の権限とか管理の都合でFirebaseとAdMobのアカウントが異なるケースの話。AdMobとFirebaseのGoogleアカウントが同じでいい場合はそのアカウントでそのままFirebaseに登録すればいいはず。この手順は不要

*2:Google Apps環境化で既存のAdMobアカウントとFirebaseに未登録のアカウント2つを使って手順を確認した

*3:権限はとりあえずプロジェクトオーナーにしてみた

Google Firebase で Crash Reporting を使う

まず初めに

後からこの issue で分かった事だけど
https://github.com/firebase/quickstart-ios/issues/13

Google Cloud Platform の利用規約に同意しておかないと下の手順の dSYM アップロード用のアカウントの作成やその権限付与でサーバエラーが起きて進めなくなってしまうので、Firebase にアプリを登録したらまずGCPのダッシュボードをチェックしてアクセスできるか確認しておいた方がいい。(これで結構時間を潰した)

GCPのダッシュボードを開くと以下のようなダイアログが出る場合があるので設定しておく
https://console.cloud.google.com/

f:id:Koze:20160607224715p:plain


セットアップ

Google Firebase の Crash Reporting を使うには dSYM アップロードのための準備を行う必要がある。
設定手順は以下
https://firebase.google.com/docs/crash/ios#upload_symbol_files

1 Download a service account key to authenticate your uploads
  • プロジェクトの歯車アイコン「権限」から「サービスアカウント」を選択
  • 「サービスアカウントを作成」を選択
  • 任意のサービスアカウント名とサービスアカウントIDを入力して「新しい秘密鍵の提供」をチェック、「キーのタイプ」でJSONを選択して「作成」を選択
  • アカウントを作成して鍵の作成に成功するとJSONファイルがダウンロードされる。また、作成したサービスアカウントに自動でプロジェクトの編集者権限も付与される。*1
2 Ensure that the service account has write permission

ここは権限の確認。
先の手順でサービスアカウント作成が正しく行われていれば、鍵が作成された事と必要な権限が付与された事がダイアログ表示されるのでこの確認作業はスキップしても良い。

3 Add an upload script for your symbol files

Xcode の Build Phase に Run Script を設定する。

# Replace this path with the path to the key you just downloaded
JSON_FILE=Path/To/ServiceAccount.json

# Replace this with the GOOGLE_APP_ID from your GoogleService-Info.plist file
GOOGLE_APP_ID=1:my:app:id

defaults write com.google.SymbolUpload version -integer 1   # creates file if it does not exist
JSON=$(cat "${JSON_FILE}")
/usr/bin/plutil -replace "app_${GOOGLE_APP_ID//:/_}" -json "${JSON}" "$HOME/Library/Preferences/com.google.SymbolUpload.plist"
"${PODS_ROOT}"/FirebaseCrash/upload-sym

2行目のJSON ファイルのパス設定は、JSONファイルをプロジェクトディレクトリ内に置いて $SRCROOT や $PROJECT_DIR を使ってパスを指定すると分かりやすい。*2

以下のように指定する
JSON_FILE="$(PROJECT_DIR)"/<リソースフォルダ>/<ファイル名>.json

10行目の ${PODS_ROOT} はCocoapodsを使っていないので
以下のように設定した
"${PROJECT_DIR}"/<外部ライブラリフォルダ>/FirebaseCrash/upload-sym

Cocoapods を使わない場合

Cocoapods を使わない場合、以下からリンク先のページから Firebase SDK を直接ダウンロードして必要な framework をXcodeプロジェクトに手動で入れていく。
Crash Report に必要な framework は FirebaseCrash.framework と Analytics 関連の framework 一式なので、これらを全てプロジェクトに取り込む。
どのサービスにどの framework が必要なのかは README に書いてある。

ダウンロードリンク
https://firebase.google.com/docs/ios/setup#frameworks

このダウンロード版の Firebase SDK には upload-sym など幾つかの必要なファイルが入っていないので、それらのファイルは適当なダミープロジェクトを用意して Cocoapods 経由で Firebase/Crash をインストールしてゲットするか、以下の FirebaseCrash の podspec に書かれている source のURLから直接ファイルを拾ってきてやる必要がある。

https://github.com/CocoaPods/Specs/tree/master/Specs/FirebaseCrash

ダウンロードした source のZIPファイルを解凍するとCLIスクリプトファイルが同梱されているので、それらをまとめて Xcode の Run Script で指定したディレクトリへ移動orコピーする。

最終的にXcodeプロジェクトのディレクトリ内に用意したファイル一式は以下のようになった

f:id:Koze:20160607222928p:plain

試す

実際に試してみる。
ドキュメントでは assert を仕込んで Build&Runして、一旦 Xcode からアプリをStopさせてアプリをホーム画面から起動。
アプリがクラッシュしたのを確認したら assert を取り除いて再度 Xcode からBuild&Runしてログがレポートが送信されたのを確認したらウェブのダッシュボードを確認。
という流れになっている。

実際に試してみると

2016-06-07 21:58:57.160 AppName[76436:] <FIRAnalytics/INFO> Firebase Analytics v.3200000 started
2016-06-07 21:58:57.162 AppName[76436:11233546] Firebase Crash Reporting: Successfully enabled
2016-06-07 21:58:57.166: <FIRInstanceID/WARNING> FIRInstanceID AppDelegate proxy enabled, will swizzle app delegate remote notification handlers. To disable add "FirebaseAppDelegateProxyEnabled" to your Info.plist and set it to NO
2016-06-07 21:58:57.160 AppName[76436:] <FIRAnalytics/INFO> To enable debug logging set the following application argument: -FIRAnalyticsDebugEnabled (see http://goo.gl/Y0Yjwu)
2016-06-07 21:58:57.185 AppName[76436:] <FIRAnalytics/INFO> Successfully created Firebase Analytics App Delegate Proxy automatically. To disable the proxy, set the flag FirebaseAppDelegateProxyEnabled to NO in the Info.plist
2016-06-07 21:58:57.194 AppName[76436:] <FIRAnalytics/INFO> Firebase Analytics enabled
2016-06-07 21:58:57.911 AppName[76436:11233921] Firebase Crash Reporting: Crash successfully uploaded

という感じでログが出て、ドキュメントには15秒内にレポートが送信されると書いてあるところテストした環境ではアプリ起動後すぐにクラッシュレポートが送信されたのが確認できた。
ダッシュボードへの反映はドキュメントには20分かかると書いてあって、これはテスト時には15分弱でダッシュボードに反映された。


Known Issues

iOS の Crash Reporting は現在β版で現時点での Known Issues としてデバイスのモデル名が表示されないと書かれている。
この点はアップデートを待つしかない。

Device models may not be showing in error reports. This will be addressed in an upcoming Pod update.

*1:もしここでアカウント作成のエラーが起きた場合、ブラウザをリロードするとアカウントは存在しているけど鍵が作成されてなくて権限も付与されていないという状態になるので、改めて手動で鍵の作成と権限の付与を行う必要がある。

*2:Xcodeプロジェクトのターゲットのリソースとしてファイルを追加してしまうとビルドしたアプリに内包されて配布されてしまうので注意。

Bitcode対応のアプリでクラッシュレポートを使う際の注意

アプリをBitcode対応してリリースしたら Crashlytics でクラッシュレポートが表示されなくなってしまった。
ダッシュボードには dSYMs が見当たらないとのメッセージが。

f:id:Koze:20160606141934p:plain

Found 1 unsymbolicated crash from missing dSYMs in 1 version in the last 24 hours.

Crashlytics のヘルプによればBitcode対応のアプリがApp StoreからリリースされるとAppleは新しく dSYMs を生成するらしく、この生成された dSYMs をアップロードする必要があるとの事。

手順は以下
https://docs.fabric.io/ios/crashlytics/missing-dsyms.html#bitcode-download

dSYMs は iTunes Connect からダウンロード、もしくは Xcode の Organizer で 「Download dSYMs...」 からダウンロードできる。
Xcode の場合は dSYMs をダウンロードした後ターミナルで

mdfind "com_apple_xcode_dsym_uuids == <UUID>"

と実行して dSYMs のある場所を探さなくてはならないのでちょっと面倒。
dSYMs のダウンロード先は以下のような場所になっていて、この xcarchive の中の dSYMs フォルダにいくつか dSYM ファイルが入っている。

/Users/<UseName>/Library/Developer/Xcode/Archives/yyyy-MM-dd/<AppName> yyyy-MM-dd HH.mm.xcarchive

iTunes Connect からダウンロードする場合はリリース済みアプリのアクティビティから該当のバージョンのビルドを選択して「dSYM をダウンロード」を選択。
dSYMs.zip という名称でZIP形式でダウンロードされる。
ZIPファイルの中身は Xcode からダウンロードした時と比べて増減があって中身が異なっているけど、どちらにしてもとりあえず Crashlytics で missing になっている dSYM と一致する UUID のものが入っていればOK

Crashlytics の設定ページから「Missing DYMs」のタブを選択して dSYM ファイルか dSYM ファイルが入ったZIPファイルをアップロードして、Missing DYMs のリストに表示されている UUID に打ち消し線が入れば成功。ダッシュボードでシンボルを確認できるようになる。


今回は Fabric Crashlytics での方法だったけど、Google Firebase でも同様にBitcode対応のアプリの dSYM をアップロードし直す必要があるはず。


追記

fastlane を使えば Bitcode 対応アプリの dSYM を iTunes Connect から自動で引っ張ってきてくれるとの事

crashlytics.com

Atom のデフォルト syntax を変更する

GitHub製のエディタAtomでplistを開くとデフォルトでは Property List (Old-Style) として開かれるが、今時のplistファイルはOld-Style形式ではなくXML形式なので syntax error になってしまう。

f:id:Koze:20160528134936p:plain

メニューの Edit>Select Grammar かウィンドウ右下のフォーマット名をクリックして Property List (XML) に変更すれば syntax が正しく解釈される。

f:id:Koze:20160528135047p:plain

f:id:Koze:20160528135109p:plain

この設定は新しくplistファイルを開く都度行わなくてはならない。

そこでplistファイルはデフォルトで Property List (XML) で開くように設定する。
設定方法は以下の公式ドキュメントを参照
http://flight-manual.atom.io/using-atom/sections/basic-customization/#_customizing_language_recognition

メニューの Atom>Config... を選択して config.cson ファイルを開く

  core:
    themes: [
      "atom-light-ui"
      "one-light-syntax"
    ]

上記の config.cson に customFileTypes を追加する

  core:
    themes: [
      "atom-light-ui"
      "one-light-syntax"
    ]
    customFileTypes:
      "text.xml.plist": [
        "plist"
      ]

これでAtomでplistファイルを開くときはデフォルトで Property List (XML) で開くようになる。


Property List (XML) の identifier はパッケージから Property List を検索して Property List (XML) の Scope: を参照する。

f:id:Koze:20160528135928p:plain

もしくは以下のソースファイル末尾の scopeName を参照する。 https://github.com/atom/language-property-list/tree/master/grammars

Property List (Old-Style) の scopeName は source.plist
Property List (XML) の scopeName は text.xml.plist
となっている。

watchOS 1からwatchOS 2への置き換え例

watchOSとiOSでデータのやり取りをしている場合のwatchOS 1からwatchOS 2への置き換え例

watchOS

watchOS 1
    [WKInterfaceController openParentApplication:userInfo
                                           reply:^(NSDictionary *replyInfo, NSError *error) {
                                               if (error) {
                                               }
                                               else {
                                               }
                                           }];
watchOS 2

openParentApplication:reply: は使えなくなったので WatchConnectivity.framework の WCSession を使った実装に置き換える。
[WCSession isSupported] はwatchOSでは常にYESを返すので isSupported のチェックは無くても良い。

セットアップ

- (void)awakeWithContext:(id)context {
    [super awakeWithContext:context];

    WCSession *session = [WCSession defaultSession];
    session.delegate = self;
    [session activateSession];
}

データ取得リクエスト

    [[WCSession defaultSession] sendMessage:userInfo
                               replyHandler:^(NSDictionary<NSString *,id> * _Nonnull replyMessage) {
                               }
                               errorHandler:^(NSError * _Nonnull error) {
                               }];

iOS

iOS 8 with watchOS 1
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply
{
    NSDictionary *dict;
    // configure reply data
    reply(dict);
}
iOS 9 with watchOS 2

WCSession をセットアップして delegate でのメッセージの受け取りとデータの返却を行う。
WCSessionDelegate は色々とメソッドのバリエーションがあるけど、watchOS 1の時のデータのやり取りをそのまま置き換える場合は didReceiveMessage:replyHandler: を実装すればOK

セットアップ

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    if ([WCSession isSupported]) {
        WCSession *session = [WCSession defaultSession];
        session.delegate = self;
        [session activateSession];
    }

    return YES;
}

データ取得リクエストへの応答

- (void)session:(WCSession *)session didReceiveMessage:(NSDictionary<NSString *, id> *)message replyHandler:(void(^)(NSDictionary<NSString *, id> *replyMessage))replyHandler
{
    NSDictionary *dict;
    // configure reply data
    replyHandler(dict);
}