Sansan Builders Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Sansan iOS アプリに XcodeGen を導入しました

こんにちは、Sansan プロダクト開発部 iOS アプリエンジニアの相川です。
本記事は Sansan Advent Calendar 2020 - Adventar の 24 日目の記事になります。

Sansan の iOS アプリに XcodeGen を導入することができたので、今回は XcodeGen 導入について以下の話を紹介できたらと思っています。

  • XcodeGen 導入の流れ
  • XcodeGen Tips
  • Sansan における XcodeGen の運用方法
  • XcodeGen を利用するにあたって気をつけるべきこと
  • XcodeGen 参考リンク集

XcodeGen 導入の流れ

まずは Sansan の iOS アプリにどのような流れで XcodeGen を導入することができたのかということについて話していこうと思います。

iOS チームでは、徐々に開発メンバーが増えていることもあり、iOS エンジニアであれば避けては通ることができない project.pbxproj ファイルのコンフリクトにも遭遇する場面が増えてきているという状況がありました。
また、既存のディレクトリ構成を整理しつつ、Embedded Framework 化を行うことによって、新規メンバーの学習コスト減や各メンバーの開発にかかるビルド時間の削減なども行いたいという思いがありました。
そのため、私が新卒として今年入社する前から既に XcodeGen を導入したいという思いはチーム内で徐々に膨らみつつありました。

私が iOS チームに配属されてから数日経ち、「何か現状の開発における大きな改善をやってみたい」という思いが徐々に湧き始め、XcodeGen を個人的に勉強することにしました。
幸い XcodeGen の記事も増え始めており、XcodeGen の概要はある程度すぐにインプットすることができました。

しかし、公式の Project Spec(XcodeGen でプロジェクトファイルを作成するための project.yml という yml ファイルの作成マニュアル)を見つつ、XcodeGen についての記事も見つつ大規模なアプリである Sansan の project.yml をゼロから作成する作業は、XcodeGen についての知識がない自分にとっては想像以上に苦しいものでした。

手探りで勉強しながら地道に project.yml を作成 -> ビルド -> 失敗 -> yml を編集 -> ビルド -> 失敗 -> ・・・ という流れを何度も繰り返して疲弊していた時に、プロジェクトファイルからXcodeGenのproject.ymlを生成するという記事を見つけました。

記事で紹介されている方法としては、XcodeGen の PR として作成されている Spec Generation(Draft でまだ未完成) というものを使用して、既存のプロジェクトファイルから XcodeGen の project.yml を生成することができるというものでした。

藁にもすがる思いで早速 Sansan の iOS アプリでも使用してみたところ、1000 行近くの読みやすいとは言えない yml ファイルが生成されました。
そのままではビルドすることができなかったのですが、yml の不要な部分を削除したり必要なものを追加したりすることによってビルドするところまで漕ぎ着けることができました 🙌
これまで地道な yml 作成作業をこなしていたおかげで、この時点ではある程度 yml のどこを弄れば良いかが掴めてきていたことは良かったと思います。

ビルドできるようになってから master にマージされるまでの流れは以下のような形でした。

  • Project Spec や Web 上の参考になる記事を見ながら地道に既存のプロジェクトと差異がなくなるような project.yml を作成
    • XcodeGen はファイル上のシステムツリーを Xcode 上のファイルツリーに強制させるため、事前に synx というツールを利用し Xcode 上のファイルツリーをシステム上のファイルツリーに強制させるようにしました
    • 二人のチームメンバーに project.yml をレビューして頂く(汚い時の yml から見ていただいていたレビュワーの負担は計り知れないものだったと思います...)
  • Sansan では CI ツールとして Bitrise を利用しているため、Bitrise の workflow も XcodeGen 用にカスタマイズ
  • 上記の作業を行いながら、チームメンバーに共有するための Sansan 用 XcodeGen ドキュメント・開発環境構築用のスクリプトを作成
  • Sansan では同時期に iPad 対応や、fastlane 移行などのプロジェクトが走っていたため、その差分を取り込む対応
  • リグレッションテストの実施
  • 無事 master にマージ

非常に大変な作業ではありましたが、最初は 1000 行近くあった yml も徐々に整理され、総行数 600 行近くまで抑えることができました。(現在は Embedded Framework 化対応も行っており、その中で yml の改善も続けています)

次節では、XcodeGen 対応を今後行う方の参考になるかもしれない「XcodeGen Tips」について書いていこうと思います。(もし間違ってる・こうした方が良いという部分があればご指摘いただけますと幸いです 🙇‍♂️ )

XcodeGen Tips

Project Spec と Xcode における Project Editor の対応関係

XcodeGen の yml を作成する際は Project Spec を確実に参照することになるかと思うのですが、Project Spec の各部分が Xcode における Project Editor のどこに対応しているかを知っているだけでもある程度 Project Spec は読みやすくなりそうだと個人的には思っています。(Project Spec の目次が多くてどこから読めばいいのか自分は結構困ったりしました笑)

f:id:kalupas:20201208124022p:plain:w400
Project Spec の対応関係

と言っても、上に示した図のように Project 自体に関する記述方法は Project Spec の Project 階層に記載されており、Target ごとの記述方法は Project Spec の Target に記載されているだけになります。

他の項目についても補足しておくとそれぞれざっくり以下について記載されています。

  • Settings: Project にも Target にも使用できる設定値の書き方
  • Aggregate Target: 特定の Target を override する方法
  • Target Template: Target 用の Template の作成方法(Target ごとの記述量を削減できる)
  • Scheme: Scheme の作成方法(Xcode の Scheme に対応)
  • Scheme Template: Target Template の Scheme バージョンのようなもの
  • Swift Package: Swift Package の導入方法
  • Project Reference: Scheme で Project を参照する方法

Sansan では今のところ、目次に書いてある中では主に「Project, Target, Settings, Scheme」を利用して yml ファイルを作成しています。

synx を利用した Xcode 上のファイルツリーの強制

XcodeGen は基本的にファイルシステムのファイルツリーを Xcode のファイルツリーに強制させます。
Sansan は割と歴史があるプロダクトであることもあり、Xcode のファイルツリーとシステムのファイルツリーがだいぶバラバラになってしまっていました 😇

そのままの状態で XcodeGen の作業を始めると非常に効率が悪いと感じたため、synx というツールを利用して Xcode のファイルツリーをシステムのファイルツリーと一致させるようにしました。

github.com

この OSS は最近はメンテナンスされていないようですが、幸い Sansan では特にエラーも起きずに利用することができたため、ファイルツリーを一致させることが楽にできました。

include を活用した yml ファイルの分割

XcodeGen には include という機能があり、ある yml から他の yml ファイルを参照することができます。

Sansan では、Project の設定値を記載している project.yml はルートディレクトリに配置し、Target ごとの設定値を記載している {targetName}.yml は XcodeGen ディレクトリ配下に配置しているため、↓ のように project.yml 内で include して yml ファイルを分割しています。

イメージのために、XcodeGen に関わるファイルツリーと include の利用方法を ↓ に示します。

  • ファイルツリー
.
|--- Sansan
|--- project.yml
|--- XcodeGen
       |--- Scripts
              |--- {script1}.sh
              |--- {script2}.sh
              |--- {script3}.sh
       |--- {target1}.yml
       |--- {target2}.yml
       |--- {target3}.yml
  • include の利用方法
include:
  - path: XcodeGen/{target1}.yml
    relativePaths: false
  - path: XcodeGen/{target2}.yml
    relativePaths: false
  - path: XcodeGen/{target3}.yml
    relativePaths: false

relativePathsfalse にしておくと各 Target で記載するファイルの path は project.yml 階層からの path で認識されるようになります。 relativePaths を Target ごとに書くのはイケていない気がしており、もしスマートな書き方を知っている方がいらっしゃれば教えていただけますと幸いです。

Build Script におけるスクリプトファイルの参照

XcodeGen では各 Target において Build Script を設定することができます。基本的に Sansan では、各 Target の yml 内で preBuildScripts (Build Phase で最初の方に実行するスクリプト)と postBuildScripts(Build Phase でファイルのコンパイル後に実行するスクリプト)を定義する形で利用しています。
その際、スクリプトを yml 内に生で記述することもできるのですが、可読性・メンテナンス性の観点から Sansan では XcodeGen/Scripts ディレクトリ内にスクリプトファイルを配置し、yml ではそのスクリプトファイルを参照するようにしています。

イメージのために yml の例を示すと ↓ のようになります。

targets:
  Sansan:
    platform: iOS
    type: application
    sources: ~~~
    dependencies: ~~~
    preBuildScripts:
      - name: Script name
        runOnlyWhenInstalling: false
        path: XcodeGen/Scripts/script-name.sh
        shell: /bin/sh
    postBuildScripts:
      - name: Script name2
        runOnlyWhenInstalling: false
        path: XcodeGen/Scripts/script-name2.sh
        shell: /bin/sh
    settings: ~~~

このようにすると、yml の可読性が高まり、スクリプトファイルのメンテナンス性も高まって個人的には良さそうかなと思っています。

自動生成系ファイルにおける optional の利用

こちらはクックパッドさんの「XcodeGenによる新時代のiOSプロジェクト管理」の「ソースコード生成」という章で紹介されているものなので、本記事での説明は省略しようと思います。(yml 作成の際には参考にさせて頂きました 🙇‍♂️ )

techlife.cookpad.com

Sansan では Sourcery や Mockolo でコードジェネレーションを行っているため、XcodeGen では optional を利用して yml ファイルを記述する必要がありました。

postGenCommand の活用

XcodeGen にはプロジェクトファイルが生成された後に実行したいコマンドを記載できる postGenCommand というものがあります。Sansan では CocoaPods を利用しているため、↓ のように postGenCommandbundle exec pod install を記載することによって、プロジェクト生成後毎回 pod install が行われるようにしています。

name: Sansan
options:
  xcodeVersion: ...
  postGenCommand: bundle exec pod install
  ...

Setting Groups(Target Template) の活用

Sansan では現在 Embedded Framework 化を行っているのですが、Embedded Framework のように Settings の内容はほぼ共通だけど個別に書きたいものもあるような状況の時は XcodeGen の Setting Groups や Target Template を利用するとメンテナンス性を向上させることができます。

Sansan では SettingGroups を利用しているため、そちらの紹介を軽くしようと思います。
Setting Groups は Project で定義し、各 Target から定義したものを利用する形になります。
Sansan では前述したように Project に関する yml は project.yml 、各 Target は XcodeGen/{targetName}.yml という形で作成しているため、以下のように Setting Groups を利用しています。

  • project.yml
name: Sansan
options: ...
include: ...
settingGroups: # Project で settingGroups という形で定義し
  EMBEDDED_FRAMEWORK_SETTING:
    base:
      CONFIG1: xxx
      CONFIG2: yyy
    configs
      ...
  • XcodeGen/target1.yml
targets:
  target1:
    platform: iOS
    type: framework
    sources: ...
    settings:
      groups: # 各 Target からは groups 経由で利用する
        - EMBEDDED_FRAMEWORK_SETTING
      base:
        LOCAL_CONFIG1: zzz
        LOCAL_CONFIG2: vvv
      configs:
        ...

このように利用すると、target1 では EMBEDDED_FRAMEWORK_SETTING で定義した設定値が適用されるようになるため、yml の可読性も高まります。

Firebase Crashlytics を CocoaPods 経由で利用している場合の対応

CocoaPods を利用している場合、基本的には yml に CocoaPods 関連の設定を書くことなく pod install を行うだけで CocoaPods 関連の依存関係は整理されるのですが、Firebase Crashlytics は注意が必要です。

XcodeGen のリポジトリにある FAQ の Can I use Crashlytics にも記載されていますが、Crashlytics の初期化は Target の Build Phase において [CP] Embed Pods Frameworks. の後に行われる必要があります。

XcodeGen の preBuildScriptspostBuildScripts ではそこまでの順序を制御することはできません。
Podfile に以下のように記載すると Crashlytics の初期化が [CP] Embed Pods Frameworks. の後に行われるようになるため、Crashlytics を利用している場合は記載するようにしましょう。

// Your dependencies
pod 'Firebase/Crashlytics'

script_phase name: 'Run Firebase Crashlytics',
             shell_path: '/bin/sh',
             script: '"${PODS_ROOT}/FirebaseCrashlytics/run"',
             input_files: ['$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)']

微妙だけどハマったもの

あまりハマる人はいないかと思うのですが、私がハマってしまったものがあるのでおまけ程度に紹介しようと思います。
XcodeGen の Project 内の options では xcodeVersion を指定することができます。 xcodeVersion を指定すると、xcscheme 内の LastUpgradeVersion にそのバージョンが挿入されます。(Projectattributes の説明にその記載があります)

そのため、私も xcodeVersion を指定しようと思い当初は ↓ のように定義していました。

name: Sansan
options:
  xcodeVersion: 12.2

しかし、これでは上手くいきませんでした。原因は簡単で、Project Spec にもしっかりと記載されていますが xcodeVersion は String で指定する必要がありました 😇 (ドキュメントはしっかり読むようにしたいですね😢 )

そのため、↓ のように定義すればしっかり反映されるようになります。

name: Sansan
options:
  xcodeVersion: "12.2"

ハマる人はそんなにいないかもしれないのですが、おまけでした。

Sansan における XcodeGen の運用方法

XcodeGen をスムーズにチームで利用できるようにするために、いくつか工夫を行ったのでその紹介も軽くですがしようと思います。

Mint を利用した XcodeGen のバージョン統一

チーム内で利用する XcodeGen のバージョンを統一するためにパッケージ管理ツールの Mint を利用しています。

github.com

brew で Mint をインストールした上で Mintfile を以下のように作成し、

yonaskolb/xcodegen@2.18.0

XcodeGen の導入時には mint bootstrap をコマンドで実行すれば Mintfile に定義されたバージョンの XcodeGen がインストールできるようにしています。

チーム共有用の git hooks の作成

XcodeGen を導入した後は今までの開発スタイルとは若干異なり、ブランチを checkout する度に mint run xcodegen コマンドを実行する必要があったため、git hooks を利用して効率化しておきたいという気持ちがありました。 個人で hooks の post-checkout にコマンドを記載して実行できるようにしても良かったのですが、Sansan のリポジトリ配下でのみ動作する .githooks をチームで共有することによって、個々人が記載することなく導入を進めることができるようにしました。

もっと具体的に説明すると、リポジトリ配下に .githooks というディレクトリを作成し、その中に post-checkout ファイルを以下の内容で作成しました。

which mint >/dev/null 2>&1 || brew install mint # 一応、Mint が入っていなければインストールするようにしている
mint run xcodegen

その上で git config --local core.hooksPath .githooks コマンドを実行すると、Sansan リポジトリ配下においてのみ hooks の参照先を .githooks ディレクトリに切り替えることができるようにし、チームに導入しました。

Sansan 用 XcodeGen ドキュメントの作成

もちろん上で紹介したような仕組みだけだと XcodeGen に慣れていない人にとっては不十分ですし、yml ファイルをチームで運用していくためには情報が不足しているため Sansan 用のドキュメントも作成しました。

ドキュメントの構成は以下のようになっています。

  • XcodeGen 導入のモチベーション・経緯について
  • XcodeGen を使用する上で知っておいて欲しいこと
    • XcodeGen が関係するファイル群
    • XcodeGen の利用方法
  • XcodeGen(主に yml ファイル) の運用方法について
    • yml ファイルの編集が発生しそうなタイミング(発覚次第、徐々に追記していく)

特に yml ファイルの編集が発生しそうなタイミングみたいなものはチームで運用する際には重要かなと思っており、それを明文化しておくことによって yml をチームで運用する際の一助となればと思い記載しています。

まだ XcodeGen を導入したばかりですが、それほど大きな問題は起きることなく運用することができていると感じています。
運用の中で見えてくる課題などももちろんあるため、その辺りは徐々に改善していければと思っています。

XcodeGen を利用するにあたって気をつけるべきこと

学習コストはそこそこ高い

XcodeGen についての日本語の記事もだいぶ増えてきていて、情報を集めることは容易にはなったものの、Project Spec をある程度理解した上で yml ファイルを作成するに至るまでの学習コストはそこそこあったかなと個人的には思っています。 もちろんそれは私に XcodeGen についての知識が全くない状態だったことと、いきなり大規模プロジェクトに XcodeGen を適用させたことも要因だったとは感じています。

そのため、もし XcodeGen についての知識がない状態で導入を進めるのであれば、手元の小さめのプロジェクトに適用してみることから始めるのが良さそうかなと個人的には思っています🙏 (その際は project.yml 自動生成から始めてみるのも良いと思います)

ある程度 yml ファイルの作成方法が掴めたら規模の大きめなプロジェクトに導入していくと効率は良さそうだと思います。

Xcode のアップデート内容が即座に反映されるわけではない

XcodeGen は非常に便利なツールではありますが、Xcode に新たな機能が追加された場合、OSS という性質上、即座にその対応が行われるわけではないことには注意しておきたいと思いました。

実際、Sansan では Xcode11 から使用できるようになっている Test Plans を使ってみたいねみたいな話も上がったのですが、現時点(2020年12月8日)ではまだ PR が作られている状態だったりします。

github.com

また Xcode12 から利用できる機能のいくつかも対応されていなかったりするので、XcodeGen を使用する上ではこのような一定のリスクがあることにも注意しておきたいですね。

XcodeGen はコントリビュータも多く、結構活発にコントリビュートが行われている印象があります(日本人のコントリビュータも多い)。Xcode のアップデートが絡む内容はかなりポジティブに捉えるとコントリビュートチャンスとも言えるかもしれません。(XcodeGen が依存している XcodeProj という OSS からまずは追従する必要がある場合もあったり、XcodeGen 自体も深く理解する必要があるため非常に難易度は高そうなイメージですが...)

github.com

XcodeGen 参考リンク集

XcodeGen を導入するにあたって参考にさせて頂いた記事などを以下に列挙しようと思います🙏
どの記事も非常に参考になるものばかりなので、XcodeGen を導入する際には目を通すと作業をスムーズに行うことができるかと思います。

おわりに

XcodeGen は導入するのは中々大変かもしれませんが、それに見合った恩恵はあると思うので規模の大きいプロジェクトであれば積極的に導入したいですね。
チームでも運用を始めたばかりなので、XcodeGen の恩恵をチーム全体として感じられているかという実感はまだそこまでありませんが、改善を続けてチームでの開発をより効率的にしていきたいと思っています。


buildersbox.corp-sansan.com

© Sansan, Inc.