STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

​​複数のiOSアプリを1つのリポジトリから動的に生成した話

この記事は STORES Advent Calendar 2022 の13日目の記事です。

はじめに

こんにちは、@marcy731 です。
わたしは STORES に今年の4月に入社して以降、 STORES ブランドアプリ のiOSエンジニアをしています。

STORES ブランドアプリ とは、オーナーさまごとにオリジナルなブランドアプリを作成し、その後の運用や分析をかんたんに行うことのできるサービスになります。

stores.jp

STORES ブランドアプリ では複数のアプリを開発・運用するにあたって、「1つのソースコードからアプリを動的に生成し、各アプリストアへと配信する仕組み」を構築しています。
この仕組みのおかげで、開発・運用をスピーディに行うことができています。

STORES Advent Calendar 2022 の9日目の記事では @tomorrowkey が「動的にCIの定義を生成する」というタイトルでこの仕組みの一部をご紹介しています。
とても面白いことをしていますので、まだご覧になっていない方はぜひ見てみてください。

本記事ではiOSアプリにおいて、「いかにして動的にアプリを生成しているのか」に着目してご紹介していきます。

iOSアプリを新規開発する一般的な手順

まずは一般的な手順でiOSアプリを新規作成することを考えてみましょう。

例として、Xcodeから「SampleApp」というアプリを新規に作成すると仮定します。
すると以下のようなディレクトリ構造のプロジェクトが自動生成されます。

Xcodeから新規プロジェクト作成を行い自動生成されたプロジェクト

iOSエンジニアであれば見慣れたものですが、この<APP_NAME>.xcodeprojXcodeで開くことにより、iOSアプリのバイナリをビルドできます。
Command Lineでもビルドできますが、いずれにせよiOSアプリには<APP_NAME>.xcodeprojが必要不可欠になります。

さて、このままでは何の機能もないアプリになってしまいますので、ここからオーナーさま独自のアプリになるように開発していきます。

とはいえ、実際にやることはそう多くありません。

STORES ブランドアプリ にはアプリ作成の基盤となるAppmakerと呼ばれる社内ライブラリがあります。
このあたらしいプロジェクトでAppmakerをインポートし、少しの初期設定することで、1つのブランドアプリとしてビルドができるようになります。*1

最後にFirebasefastlaneBiriseなど、DevOpsに必要な設定をすることで晴れてアプリ作成が完了します。


では2つ目、3つ目、nつ目、とアプリが増えていくことを想像してみてください。


想像いただいたとおり、このままではすべてのアプリで上記と同じ手順を行う必要があります。
当然ながら、これらのプロジェクトを管理するためのGitリポジトリも個別に用意する必要があります。

今回は省きましたが、ストアへの配信やテストアプリの配信なども個別に対応しないといけません。
さらには新規アプリ開発だけでなく、その後の新機能の開発や保守も個別に対応しないといけません。

アプリ作成からストアへのアップロードの流れ

ここまでをまとめると、アプリごとに以下の作業が必要になります。

  1. Xcodeから新規プロジェクトを生成する。
  2. Appmakerを含む必要なライブラリをインポートする。
  3. Appmakerにアプリ固有の設定値を渡す。
  4. ストアへの配信やテストアプリの配信のために、Firebaseやfastlane、Bitriseを導入する。


…どうでしょう。


…大変そうですよね。


実際に私が入社した4月の段階では上記のような運用でした。
では現在の STORES ブランドアプリ ではどのようにアプリを生成しているか見ていきましょう。

.xcodeproj を動的に生成できないか?

先ほどiOSアプリのビルドには.xcodeprojが必要不可欠だと述べました。
.xcodeproj にはアプリ名、Bundle ID、証明書の情報、ビルド設定、その他さまざまなアプリに必要なメタデータが設定されています。
ともすればこの.xcodeprojを動的に生成もしくは変更することが出来れば、複数アプリの動的生成も実現出来そうです。

しかしiOSエンジニアであればご存じのとおり、この.xcodeprojXcodeが解釈するファイルですので、人間がパッと理解できるようには構成されていません。
.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つあれば良く、あたらしいアプリを導入するときの手順も以下のようになりました。

  1. アプリごとに設定用のディレクトconfig/<APP_NAME>/を用意する。
  2. 上記ディレクトリにenv.ymlやAppIconなどの画像リソース、Firebaseで利用するGoogleService-Info.plistなどを格納する。
  3. スクリプトを実行し、<APP_NAME>.xcodeprojを生成する。

これだけであたらしいアプリを動的に生成し、ビルドできるようになりました。
Firebaseやfastlane、Biriseなどの設定も都度行う必要がなくなり、基本的にはすべて共通化できたことも大きな点です。

結果として、新規開発・保守のコストが大きく下がり、開発体験が大きく向上しました。
なによりも、本来行いたい新機能の開発やリファクタリングに集中でき、プロダクトの品質向上に繋がりました。

さいごに

本記事では、環境変数を用いて複数のアプリを1つのリポジトリから動的に生成する方法を紹介しました。
複数のアプリを動的に生成する必要があるプロダクトは稀ですが、環境変数を用いて.xcodeprojを動的に変更するテクニックは、他のユースケースでも利用できるものになります。
少しでも何かの参考になりましたら幸いです。

STORES ブランドアプリ ではこのような「アプリをつくる仕組み」づくりを行っています。
本記事のような基盤開発や自動化など以外にも、サーバーからのレスポンスに応じた動的レイアウトシステムなど、通常のアプリ開発では経験しないような技術的な挑戦がまだまだ多くあります。
少しでも面白そうと感じていただけましたら、ぜひカジュアルにTwitterまで連絡をいただけますと泣いて喜びます。

また STORES では STORES ブランドアプリ 以外のプロダクトでもエンジニアを絶賛募集中です。
ぜひ採用サイトにも遊びに来てください。

jobs.st.inc

*1:アプリ内部をサーバーからのレスポンスをもとに動的に生成しているお話は別の機会に話せたら嬉しいです。

*2:具体的な.xcodeprojファイルの内容についてはこちらの記事がとても参考になりました。ありがとうございます。https://qiita.com/yokomotod/items/02e395e99bb891d27f67

*3:実際の設定ではもう少し設定する項目はあります。

*4:TestFlightはAppleが提供するβ版アプリの配信を行うためのアプリケーションです。

*5:実行スクリプトはfastlaneで定義し、CIからも呼べるようにしてあります。