iOSアプリ開発でFirebaseのGoogleService-Info.plistを環境別に自動的に切り替える

Firebaseを使っているとGoogleService-Info.plistなるファイルをプロジェクト内に配置するよう指示されますよね。
FirebaseのNotificationやAnalytics、CrashReportなどは開発版とリリースされているものとは混ぜたくないので、環境別に自動的にGoogleService-Info.plistを切り替えさせたかったのですが、それがなかなかに面倒だったのでメモっておきます。

前提:Configurationsの分類

私が現在作成しているアプリでは1つのプロジェクト内に複数の環境(Configuration)を存在させています。
以下のような感じにConfigurationsを分類し、Bundle IDを環境別に変化させることでそれぞれ別アプリとして認識させています。

Configuration名 Bundle ID Suffix 用途 運用方針
Staging Debug  .Staging.Debug 普段の開発ビルド用 ステージングサーバへアクセスしつつデバッグ情報などを表示させる
Staging Release .Staging 社内配布やテスト用 ステージングサーバへアクセスしつつデバッグ情報などは表示させない
Production Debug  .Debug 本番環境でデバッグしたい時に 本番サーバへアクセスしつつデバッグ情報などを表示させる
Production Release  なし 本番リリース用 本番サーバへアクセスしつつデバッグ情報などは表示させない

これに合わせてSchemeもStagingProductionの2つ作成しており、Runした場合はそれぞれの環境のDebug版が実行され、Archiveした場合は必ずRelease版がビルドされるようにしています。
あとはアプリ名、アイコンもそれぞれの環境で変更することで事故が起こらないようにしています。

手順

1. Firebase上のプロジェクトにアプリをConfiguration分作成する

開発版とリリース版など環境ごとにFirebaseの管理を分けたいのでその分だけFirebaseのプロジェクトにアプリを作成します。今回の場合だと最終的に以下のような感じになると思います

スクリーンショット 2017-04-20 18.22.02.png

細かい手順は公式リファレンスを見ていただくのが早いです。

2. GoogleService-Info.plistを名前を変えて全部プロジェクトに配置する

次はGoogleService-Info.plistをダウンロードしてプロジェクトに配置します
細かい手順は公式リファレ(ry

スクリーンショット 2017-04-20 18.29.20.png

ですがどのplistをダウンロードしても同じ名前で落ちてくるためそれぞれ環境別に区別がつくように名前を変更します
Xcodeに追加すると以下のような感じになっていると思います(名前は一例です)

スクリーンショット 2017-04-20 18.36.52(3).png

ちなみにですが、実はXcodeのプロジェクトに追加しておく必要は無いです。
後述のRunScriptで参照できる場所にあればいいのでXcode上で見えていなくても問題ないのですが、私は気分的に追加しつつipaには含まれないようにtargetからは外しています。

3. Run Scriptで無理やり環境別のGoogleService-Info.plistを適用する

デフォルトではFirebaseライブラリはルート階層にあるGoogleService-Info.plistという名前のファイルを参照しに行くため、それぞれのビルド環境ごとのplistファイルをコピーします。

PROJECTファイル -> TARGETS -> Build Phases -> + -> New Run Script Phaseをタップして新しいスクリプトを作成して以下のスクリプトを貼り付けて下さい
※パスやらConfiguration名やらが含まれているため、各々の環境に合わせて適宜読み替えて下さい

Copy_GoogleService-Info.plist
if [ "${CONFIGURATION}" = "Staging_Debug" ]; then
    cp "${PROJECT_DIR}/${PROJECT_NAME}/Configurations/GoogleService-Info-Staging-Debug.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
    echo "Staging Debug GoogleService-Info copied."
elif [ "${CONFIGURATION}" = "Staging_Release" ]; then
    cp "${PROJECT_DIR}/${PROJECT_NAME}/Configurations/GoogleService-Info-Staging-Release.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
    echo "Staging Release GoogleService-Info copied."
elif [ "${CONFIGURATION}" = "Production_Debug" ]; then
    cp "${PROJECT_DIR}/${PROJECT_NAME}/Configurations/GoogleService-Info-Production-Debug.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
    echo "Production Debug GoogleService-Info copied."
elif [ "${CONFIGURATION}" = "Production_Release" ]; then
    cp "${PROJECT_DIR}/${PROJECT_NAME}/Configurations/GoogleService-Info-Production-Release.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
    echo "Production Release GoogleService-Info copied."
fi

4. Crash Reportのシンボルファイル自動アップロードに対応する

※Crash Reportが不要であればこの手順は不要です。
Crash Reportを使っている場合は公式リファレンスの手順にある通り人間が読める状態にするためシンボルファイルのアップロードスクリプトを追加する必要があります。
(手動でシンボルファイルをアップロードしてもいいですがかったるくてやってらんないですw)

このスクリプト内にはGoogleAppIdを記述するよう指示があるためここも環境別に分岐出来るようスクリプトを修正します。
GoogleAppIdはGoogleService-Info.plist内に記述されているので各環境のplistから引っ張ってきて下さい。

※パスやらConfiguration名やらが含まれているため、各々の環境に合わせて適宜読み替えて下さい(2度目)
特に最後のjsonファイル名は各々異なるようなので変更はお忘れなく。

Upload_Symbol_File_to_Firebase
# Replace this with the GOOGLE_APP_ID from your GoogleService-Info.plist file
if [ "${CONFIGURATION}" = "Staging_Debug" ]; then
    GOOGLE_APP_ID=1:xxxxxxxxxxxx:ios:xxxxxxxxxxxx
    echo "Configure Staging Debug"
elif [ "${CONFIGURATION}" = "Staging_Release" ]; then
    GOOGLE_APP_ID=1:xxxxxxxxxxxx:ios:xxxxxxxxxxxx
    echo "Configure Staging Release"
elif [ "${CONFIGURATION}" = "Production_Debug" ]; then
    GOOGLE_APP_ID=1:xxxxxxxxxxxx:ios:xxxxxxxxxxxx
    echo "Configure Production Debug"
elif [ "${CONFIGURATION}" = "Production_Release" ]; then
    GOOGLE_APP_ID=1:xxxxxxxxxxxx:ios:xxxxxxxxxxxx
    echo "Configure Production Release"
fi

# Replace the /Path/To/ServiceAccount.json with the path to the key you just downloaded
"${PODS_ROOT}"/FirebaseCrash/upload-sym "${PROJECT_DIR}/${PROJECT_NAME}/Configurations/bigfive-15e55-firebase-crashreporting-8v6br-f8d6143138.json"

2つのスクリプトを追加するとBuild Phasesにはこんな感じになっているはずです

スクリーンショット 2017-04-20 18.36.52(3).png

これで作業は完了です。あとはビルドが通ればおkです

ポエム:FIROptions()じゃダメな理由

ここまでやってといてなんですが、実はFirebaseライブラリには使用するGoogleService-Info.plistを指定できるオプションがあります。
なので以下のようにコード側で環境別にファイル名を明示することで簡単に切り替えることが出来ます。

AppDelegate.swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    let firbaseOptions = FIROptions(contentsOfFile: Bundle.main.path(forResource: "GoogleService-Info-Staging-Debug", ofType: "plist"))!
    FIRApp.configure(with: firbaseOptions)
    return true
}

しかしながら、この方法で問題になるのはCrash Reportのシンボルファイル自動アップロードスクリプトです。
このスクリプトの本体はFirebaseのライブラリに含まれているのですが、このスクリプト内にGoogleService-Info.plistという名前がハードコーディングされているため別の名前にしているとうまくアップロード出来ません。
こっちのほうが正統派っぽいやり方なので、最初この方法でやろうとしたら出来なくてストレスで寿命がマッハになりかけたので皆様もお気をつけ下さい。

ちなみにAndroidでは?

Androidアプリ開発においてはProductFlaverやBuildTypeによって使用するソースフォルダを分けることができるので、上記のiOSのConfigurationと同じようなFlaverを作り、そのフォルダごとにGoogleService-Info.plistをそのまま配置すればいいので楽ちんです

参考

1089contribution
  1. Crash Reportのシンボルファイル自動アップロードに対応する

自分の理解不足だったら申し訳ないのですが、ビルドのたびにアップロードしていたらビルド時間が長くなるなど起きないのでしょうか?

451contribution

@star__hoshi おそらくですが数秒単位では遅くなる可能性はあります
ただFirebase Crash Reportを使う場合、シンボルファイルをアップしていないとクラッシュログの解析が困難になるので極力アップロードはしておいたほうがいいと思います。

Firebase公式リファレンスでもアップロードスクリプトをビルドごとに走らせるよう手順があります
https://firebase.google.com/docs/crash/ios#upload_symbol_files

どうしてもビルド時間が気になる場合は、手動でシンボルファイルを書き出してCrash Reportのダッシュボードにアップロードすることも出来るのでそれでも大丈夫です!