この記事は STORES Advent Calendar 2022 の13日目の記事です。
はじめに
こんにちは、@marcy731 です。
わたしは STORES に今年の4月に入社して以降、 STORES ブランドアプリ のiOSエンジニアをしています。
STORES ブランドアプリ とは、オーナーさまごとにオリジナルなブランドアプリを作成し、その後の運用や分析をかんたんに行うことのできるサービスになります。
STORES ブランドアプリ では複数のアプリを開発・運用するにあたって、「1つのソースコードからアプリを動的に生成し、各アプリストアへと配信する仕組み」を構築しています。
この仕組みのおかげで、開発・運用をスピーディに行うことができています。
STORES Advent Calendar 2022 の9日目の記事では @tomorrowkey が「動的にCIの定義を生成する」というタイトルでこの仕組みの一部をご紹介しています。
とても面白いことをしていますので、まだご覧になっていない方はぜひ見てみてください。
本記事ではiOSアプリにおいて、「いかにして動的にアプリを生成しているのか」に着目してご紹介していきます。
iOSアプリを新規開発する一般的な手順
まずは一般的な手順でiOSアプリを新規作成することを考えてみましょう。
例として、Xcodeから「SampleApp」というアプリを新規に作成すると仮定します。
すると以下のようなディレクトリ構造のプロジェクトが自動生成されます。
iOSエンジニアであれば見慣れたものですが、この<APP_NAME>.xcodeproj
をXcodeで開くことにより、iOSアプリのバイナリをビルドできます。
Command Lineでもビルドできますが、いずれにせよiOSアプリには<APP_NAME>.xcodeproj
が必要不可欠になります。
さて、このままでは何の機能もないアプリになってしまいますので、ここからオーナーさま独自のアプリになるように開発していきます。
とはいえ、実際にやることはそう多くありません。
STORES ブランドアプリ にはアプリ作成の基盤となるAppmaker
と呼ばれる社内ライブラリがあります。
このあたらしいプロジェクトでAppmaker
をインポートし、少しの初期設定することで、1つのブランドアプリとしてビルドができるようになります。*1
最後にFirebaseやfastlane、Biriseなど、DevOpsに必要な設定をすることで晴れてアプリ作成が完了します。
では2つ目、3つ目、nつ目、とアプリが増えていくことを想像してみてください。
想像いただいたとおり、このままではすべてのアプリで上記と同じ手順を行う必要があります。
当然ながら、これらのプロジェクトを管理するためのGitリポジトリも個別に用意する必要があります。
今回は省きましたが、ストアへの配信やテストアプリの配信なども個別に対応しないといけません。
さらには新規アプリ開発だけでなく、その後の新機能の開発や保守も個別に対応しないといけません。
ここまでをまとめると、アプリごとに以下の作業が必要になります。
- Xcodeから新規プロジェクトを生成する。
Appmaker
を含む必要なライブラリをインポートする。Appmaker
にアプリ固有の設定値を渡す。- ストアへの配信やテストアプリの配信のために、Firebaseやfastlane、Bitriseを導入する。
…どうでしょう。
…大変そうですよね。
実際に私が入社した4月の段階では上記のような運用でした。
では現在の STORES ブランドアプリ ではどのようにアプリを生成しているか見ていきましょう。
.xcodeproj
を動的に生成できないか?
先ほどiOSアプリのビルドには.xcodeproj
が必要不可欠だと述べました。
.xcodeproj
にはアプリ名、Bundle ID、証明書の情報、ビルド設定、その他さまざまなアプリに必要なメタデータが設定されています。
ともすればこの.xcodeproj
を動的に生成もしくは変更することが出来れば、複数アプリの動的生成も実現出来そうです。
しかしiOSエンジニアであればご存じのとおり、この.xcodeproj
はXcodeが解釈するファイルですので、人間がパッと理解できるようには構成されていません。
.xcodeproj
のコンフリクト解消に悩まされたiOSエンジニアもきっと多いはずです。
もちろんファイル構成自体はJSONに近いkey-value形式のフォーマットですので、全く読めないわけというわけではありません。*2
それでも.xcodeproj
を動的に生成する独自ジェネレータを作成するのは、現状では学習コストや保守コストも大きく、現実的な解決策ではありませんでした。
そこで外部OSSを用いて.xcodeproj
を生成することを考えます。
XcodeGen を用いてyamlから.xcodeproj
を生成する
結論としては、外部OSSに依存するリスクも考慮した上で、XcodeGenというOSSコマンドラインツールを利用することにしました。
XcodeGenはproject.yml
というyamlファイルに定義した設定値から.xcodeproj
を生成できるSwift製コマンドラインツールです。
複雑な.xcodeproj
のコンフリクト対策として導入している企業も多いのではないでしょうか。
他の候補としてはCocoaPodsが提供しているXcodeprojがありました。
こちらは.xcodeproj
を生成するのではなく、編集するRuby製のツールです。
今回は以下の観点からXcodeGenを利用することに決めました。
project.yml
で設定値を管理できるため可読性や保守性が高いこと。- 現在でも活発に開発が行われていること。
- 日本のiOS界隈ではメジャーで参考になる記事も多いこと。
- Contributorに著名な日本人の開発者がいること。
- Swiftで書かれていること。
これらのメリットのうち、特にyamlから生成されるというところに着目しました。
yamlは簡易なテキストファイルですので、環境変数を用いることでyamlの値も動的に変更できます。
つまり、環境変数という形で個別アプリの設定値をproject.yml
に渡すことで、アプリごとの.xcodeproj
を動的に生成できるということです。
具体的なproject.yml
は以下のようになっています。
なお環境変数を利用している箇所に注目し、一部を抜粋して記述していますのでこのままでは動作しません。
雰囲気を感じていいただけると幸いです。
name: ${APP_NAME} configs: Debug: debug Release: release options: deploymentTarget: iOS: ${DEPLOYMENT_TARGET} schemes: ${APP_NAME}: build: targets: ${APP_NAME}: all run: config: Debug test: config: Debug profile: config: Release analyze: config: Debug archive: config: Release settings: base: MARKETING_VERSION: ${MARKETING_VERSION} DEVELOPMENT_TEAM: ${TEAM_ID} targets: ${APP_NAME}: type: application platform: iOS settings: base: ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor CODE_SIGN_ENTITLEMENTS: App/App.entitlements PRODUCT_BUNDLE_IDENTIFIER: ${BUNDLE_ID} configs: Release: CODE_SIGN_IDENTITY: iPhone Distribution CODE_SIGN_STYLE: Manual PROVISIONING_PROFILE_SPECIFIER: match AppStore ${BUNDLE_ID} info: path: App/Info.plist properties: CFBundleDisplayName: ${DISPLAY_NAME} SampleAppID: ${SAMPLE_APP_ID} ENVIRONMENT: ${ENVIRONMENT} sources: - path: App
アプリごとの環境変数の設定はconfig/<APP_NAME>/
という個別設定をまとめたディレクトリ内に、env.yml
というyamlファイルを作成し、以下のように定義しています。*3
APP_NAME: "SampleApp" DEPLOYMENT_TARGET: "14.0" TEAM_ID: "XXXXXXXX" ITC_TEAM_ID: "12345678" BUNDLE_ID: "com.sample.app" DISPLAY_NAME: "SampleApp" MARKETING_VERSION: "1.0.0" SAMPLE_APP_ID: "xxxxxxxxxxx"
また、ブランドアプリ全体で統一しておきたい設定に関してはcommon_env.yml
という共通の設定ファイルを用意して定義しました。
こちらにはAppmaker
をはじめとするライブラリのバージョン情報やアプリのバージョン情報などを記述しています。
このenv.yml
およびcommon_env.yml
をXcodeGenを実行する前にパースし、環境変数へと格納しています。
ここまでできれば、あとはこれらの仕組みを1つのスクリプトにまとめ、.xcodeproj
の生成からビルド、TestFlight*4への配布までをコマンド1つで実行できるようにすればOKです。*5
STORES ブランドアプリ のアプリ新規開発の手順
上記の仕組みを利用することで、ブランドアプリ用のリポジトリは1つあれば良く、あたらしいアプリを導入するときの手順も以下のようになりました。
- アプリごとに設定用のディレクトリ
config/<APP_NAME>/
を用意する。 - 上記ディレクトリに
env.yml
やAppIconなどの画像リソース、Firebaseで利用するGoogleService-Info.plist
などを格納する。 - スクリプトを実行し、
<APP_NAME>.xcodeproj
を生成する。
これだけであたらしいアプリを動的に生成し、ビルドできるようになりました。
Firebaseやfastlane、Biriseなどの設定も都度行う必要がなくなり、基本的にはすべて共通化できたことも大きな点です。
結果として、新規開発・保守のコストが大きく下がり、開発体験が大きく向上しました。
なによりも、本来行いたい新機能の開発やリファクタリングに集中でき、プロダクトの品質向上に繋がりました。
さいごに
本記事では、環境変数を用いて複数のアプリを1つのリポジトリから動的に生成する方法を紹介しました。
複数のアプリを動的に生成する必要があるプロダクトは稀ですが、環境変数を用いて.xcodeproj
を動的に変更するテクニックは、他のユースケースでも利用できるものになります。
少しでも何かの参考になりましたら幸いです。
STORES ブランドアプリ ではこのような「アプリをつくる仕組み」づくりを行っています。
本記事のような基盤開発や自動化など以外にも、サーバーからのレスポンスに応じた動的レイアウトシステムなど、通常のアプリ開発では経験しないような技術的な挑戦がまだまだ多くあります。
少しでも面白そうと感じていただけましたら、ぜひカジュアルにTwitterまで連絡をいただけますと泣いて喜びます。
また STORES では STORES ブランドアプリ 以外のプロダクトでもエンジニアを絶賛募集中です。
ぜひ採用サイトにも遊びに来てください。