🔨

ReactNativeで環境ごとのビルド設定を整える(iOS編)

2021/08/16に公開

ネイティブアプリ開発では、任意の環境に接続されたアプリを手元で動かしたり配布したりできる必要があります。これを構成する要素は2つあると思っています。

1つ目、iOS/Androidアプリのビルド設定は基本的には

  • Debugビルド
  • Releaseビルド

の2種類が存在します。Debugビルドは手元のマシンで実行して手元の端末で利用することが多いです。Releaseビルドだと一般的には配布用に最適化された形でビルドされます。Webで言う NODE_ENV=development, production みたいなものですね。

2つ目として、接続先のバックエンドは

  • ローカル環境(local)
  • 開発環境(dev)
  • 本番環境(prod)

が存在することが多いでしょうか(ネイティブアプリ向けのローカル環境は用意していないこともあるかもしれません)。

この記事では、ReactNativeを活用したiOS/Androidアプリ開発において、上記2要素を組み合わせた

  • Debugビルド × local(手元での開発)
  • Debugビルド × dev(手元での開発)
  • Releaseビルド × dev(社内で開発環境を活用した動作確認)
  • Releaseビルド × prod(ストアでの配布)

のようなことをできるようにすることを目指します。

前提

以下の環境を利用しています。

  • react-native: 0.64.1
  • Xcode: 12.5.1
  • Android Studio: 4.2.1

プロジェクトの初期化は以下のコマンドで行います。

$ npx react-native init SampleApp --template react-native-template-typescript

AnalyticsやCrashlyticsなどの用途でFirebaseも導入することとします。

方針

envファイルは <root>/env ディレクトリ以下に、 .env.local.env.dev.env.prod のような形で作成します。

外部の接続情報(APIのエンドポイントなど)は可能な限り、ビルド時に注入する形にします。これを簡単にしてくれるパッケージとして react-native-config があるのでこれを活用していきます。
https://github.com/luggit/react-native-config

ビルド時に ENVFILE=.env.dev のような環境変数を渡せば、TypeScriptから読み取れるだけでなく、設定次第ではiOS/Androidのビルド時にもこの環境変数を利用できるようになります。

iOSの設定

パッケージの導入

ゴールとしては $ ENVFILE=env/.env.dev npx react-native run-ios のようにコマンドを実行すれば

  • ENVFILE で指定した環境変数に基づいた接続先につながる
  • Google-Service-Info.plist が切り替わる
  • bundleIdentifierが切り替わる
  • (ついでに)アプリ名も切り替わる

ようになることを目指します。Xcodeの Build Configurations や scheme はできるだけ増やさないシンプルな構成でやっていきます(ReactNativeのバージョンがあがったり、Xcodeのバージョンが上がった際にDebug/ReleaseのBuild Configurationであることを前提に設定を書き換えて来ることがあるためです)。

react-native-config のREADMEに結構詳しく書いてくれているので概ねそれに従って設定していきます。まずはパッケージをインストールします。

$ npm i react-native-config
$ cd ios && pod install && cd ..

Google-Service-Info.plist

ネイティブアプリでFirebaseを使う際に面倒なのが Google-Service-Info.plist の存在です。環境ごとに用意する必要があるので Google-Service-Info-dev.plist のような名前でプロジェクトに配置するものの、 Google-Service-Info.plist というファイル名では位置されている前提で実行されるライブラリが存在します(Googleサインイン系のライブラリでそうだったことがある)。そのため、ビルド時に Google-Service-Info-dev.plistGoogle-Service-Info.plist としてコピーする処理を用意する必要があり、すなわちビルド時に ENVFILE=.env.dev のように渡された環境変数を利用できるようにする必要があります。

READMEに書かれている Availability in Build settings and Info.plist の設定に従います。簡単に説明すると

  1. Config.xcconfig を作成して1行目に #include? "tmp.xcconfig" と書く
  2. .gitignoretmp.xcconfig を追記する
  3. Xcodeの PROJECTConfigurationsDebugReleaseNone になっているところを Config にする(1で作成したConfig.xcconfigが読み込まれるようにしている)
  4. SampleApp の Edit scheme を開いて Build -> Pre-actionsに 以下のRun Scriptを追加する
"${SRCROOT}/../node_modules/react-native-config/ios/ReactNativeConfig/BuildXCConfig.rb" "${SRCROOT}/.." "${SRCROOT}/tmp.xcconfig"

です。やっていることは ENVFILE の内容が tmp.xcconfig に転記されるようにBuildのPre-actionに設定しているだけです。これをすることで Info.plist などで環境変数の内容が使えるようになります。$(ENV_KEY) のようにすれば利用できます。これにより、bundleIdentifierやアプリ名にsuffixをつけられるようになります。さらに、先程述べていたFirebaseの Google-Service-Info.plist を動的にコピーできるようになります。

  1. XcodeでTARGETSをSampleAppにして、Build Phasesを開く
  2. +を押してNew Run Script Phaseを選ぶ
  3. Run Script ってなってる部分をダブルクリックすると名前が変えられるので変えて位置を Check Pods Manifest.lock の下にする
  4. 以下のスクリプト書き込む。
rm -rf "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"
cp "$SRCROOT/PrologueApp/GoogleService-Info-${APP_ENV}.plist" "${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/GoogleService-Info.plist"

最終的には 👇 のような感じになりますね。

スクリプトは ENVFILEAPP_ENV という環境変数が設定されている前提になっています。 僕が設定するときには APP_ENV として接続先の環境を書くようにしていて、今回のケースだと local or dev or prod になります。この APP_ENV を 👇 の画像のように Bundle Identifier の設定にも活用すれば動的に変更することができます(もちろん APP_ENV っていう名前じゃなくて、 BUNDLE_ID_SUFFIX とかでもいいと思います)。Bundle IdentifierとGoogle-Service-Info.plistに設定されているBundle Identifierは一致している必要があることにご注意ください。

同様に、 Info.plistBundle display name の値を $(DISPLAY_NAME_SUFFIX) のようなものを使って SampleApp$(DISPLAY_NAME_SUFFIX) のように設定しておけば、 ENVFILE の値に応じたアプリ名になります( $(DISPLAY_NAME)として名前全体を設定できるようにしてもいいかもしれません)。

ここまでくれば、あとは AppDelegate.m にFirebaseの初期化のコードを書けば以下のコマンドで動作します。

$ ENVFILE=env/.env.dev npx react-native run-ios

実機での実行

実機での実行が、ENVFILEを使ったいいやり方をまだわからなくて、本当は $ ENVFILE=env/.env.dev npx react-native run-ios --device=foo みたいな感じで実機実行できるといいんですが、おそらく?現状できないので仕方なく scheme を作って対応しています。

  1. Xcodeで SampleApp の Edit scheme を開く
  2. 左下のほうの Duplicate Scheme を押す
  3. 名前は ENVFILEで指定するenvの名前(devとか)にしつつ、下の方にある Shared のチェックを入れる(git管理されます)
  4. 左側の Build -> Pre-actionsで New Run Script して、cp "${PROJECT_DIR}/../env/.env.dev" "${PROJECT_DIR}/../.env" を書く

これで作成された dev schemeを選択後、転送先を実機してRunすれば実機で実行できます。別のファイル使いたくなったらまたschemeを増やしてcp元のenvファイルを変えれば実行できます。

(ここをもうすこしシンプルにいい感じに設定できるといいんですが…)

iOSアプリの配布

すみません…ここも書こうと思っていたんですが途中で力尽きました…ひとまずキーワードをおいておきます

  • fastlane/fastlane matchでの証明書管理
  • AppStore Connect API
  • ビルド時に ENVFILE を指定する

おわりに

ENVFILE を指定して実行するだけで、接続先やGoogle-Service-Info.plistを動的に切り替えられるようになりました。本当は

  • Android編
  • Firebase App Distributionへの配布編
  • ストアへのリリース編
  • CD編

も書きたかったのですが、iOSの設定だけで力尽きてしまいました…。みなさんの反応がよければ、Zennの本などにまとめることも検討していますのでお気軽にフィードバックいただければ幸いです。

僕自身は Flutter で同様の内容で開発環境やCD環境のセットアップ、ベースコードの実装などを顧問業としてやっていたこともあり、React Nativeでのセットアップもそれほど大変ではありませんでした。クロスプラットフォームでの開発をやっていた経験が別のクロスプラットフォームでも活きるのは少しおもしろかったです。

https://twitter.com/_mogaming/status/1427071358362558470?s=21

Discussion