ObjecTips

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

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);
}

Playground で delegate を使う

Playground で delegate メソッドを扱うにはどうする?
delegate が呼ばれるクラスの実装が必要、Playground でクラス定義できるの?

というあたりが疑問だったけど Playground でも普通にクラス定義が可能で delegate も試せるらしい。

下記の前回のコードに

クラス定義とクラス生成、delegate の設定のコードを追加してみる。

これで delegate メソッド内で出力している print の内容がコンソールに順次表示されていく。
クラスを定義する箇所はコードの先頭でなくても良くて、delegate を生成する直前にクラス定義のコードを挟むなんて事も出来る。

Playground でオーディオ再生(音声読みあげ)をする

Playground を使って簡単なオーディオ再生のテストを行う。
スニペットを実行確認するのにいちいちダミーのアプリを作ったりしなくていいのが Playground の便利なところ。
Playground なので言語は Swift

まずババッと以下のコードを書いてみる。
オーディオファイルを用意したりするのも面倒なので簡単に AVSpeechSynthesizer を使ってテキスト読み上げを行う。

import UIKit
import AVFoundation

let string = "あのイーハトーヴォのすきとおった風、夏でも底に冷たさをもつ青いそら、うつくしい森で飾られたモーリオ市、郊外のぎらぎらひかる草の波。またそのなかでいっしょになったたくさんのひとたち、ファゼーロとロザーロ、羊飼のミーロや、顔の赤いこどもたち、地主のテーモ、山猫博士のボーガント・テストゥパーゴなど、いまこの暗い巨きな石の建物のなかで考えていると、みんなむかし風のなつかしい青い幻燈のように思われます。"
let synthesizer = AVSpeechSynthesizer()
let voice = AVSpeechSynthesisVoice(language: "ja-JP")
let utterance = AVSpeechUtterance(string: string)
utterance.voice = voice
synthesizer.speakUtterance(utterance)

読み上げするのが日本語テキストのため AVSpeechSynthesisVoice を使って日本語の声を設定してやる必要があるのに注意。
(Playground では Mac のシステム環境に設定に関わらず en_US の Locale で動作するようで、en_US の声で日本語テキストを読み上げさせてもできない。)

と、このコードを実際に Playground で実行してみると一番最後の行を実行した時にすぐ処理が終了してしまい、音声読み上げがされないという事態に直面する。
ここが今回の肝で、Playground でオーディオ再生をするには XCPlayground module の XCPlaygroundPage クラスを使う。

import XCPlayground

XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

これでOK

以下ドキュメントより

Set needsIndefiniteExecution to true to continue execution after the end of top-level code.


最終的な実装コードは以下
これで Playground 上でオーディオ再生(音声読みあげ)を実行確認する事ができる *1

*1:うまく再生されない場合は Playground のウィンドウ下部にある再生停止ボタンを押して処理を再実行させてみると良い

中国語のローカライズ表記

iOS の言語設定の言語一覧画面と同じような画面を作ろうと思って実装してみたら中国語の場合に思い通りにいかなかったメモ

iOS の言語設定の言語一覧画面

f:id:Koze:20160302105041p:plain

cell.textLabel.title には現地語でのローカライズ表記
cell.detailTextLabel.title にはシステム言語でのローカライズ表記で言語が表示されている。

これらをとりあえず見えてる範囲で同じ表記になるよう実装してみる。
ロケールの identifier は以下のように場合によっては言語コード+国コードの形になっている。

コメント部分に記載した様におおよそ設定画面と同じように表示されている。*1
問題は中国語で、まず日本語表記では「簡体中文」「繁体中文」と表記されて欲しいところが「中文」のみが表示されている。
また現地語表記では「香港」が「中華人民共和国香港特別行政区」と表示されている。

試しにコードから取得できる中国語の全リストを洗ってみる。

香港とマカオは 中華人民共和国XXX特別行政区 と付与されるらしい。
これをシステム設定の表記に合わせるには、直接 displayNameForKey: メソッドを呼ばずに1つクッションをかまして特定の NSLocaleIdentifier の時には自前で用意したローカライズ文字列を返す様にしてやる事で対応できる。
また「中文」の表記の部分についても同様に、システム設定の表記に合わせるには特定の NSLocaleLanguageCode の時には自前のローカライズ文字列を返す様にする事で対応できる。 *2

*1:フランス語とスペイン語の文頭の大文字小文字が異なっているのと、フランス語(カナダ)の括弧の表記が無い点が異なっている

*2:労力的にシステム設定と同じ表記になる様に何語までチェックし実装対応するかという問題はある。なるべくなら例外処理なしでAPIローカライズできるのがベスト