こんにちは、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 の目次が多くてどこから読めばいいのか自分は結構困ったりしました笑)
と言っても、上に示した図のように 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 のファイルツリーをシステムのファイルツリーと一致させるようにしました。
この 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
relativePaths
を false
にしておくと各 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 作成の際には参考にさせて頂きました 🙇♂️ )
Sansan では Sourcery や Mockolo でコードジェネレーションを行っているため、XcodeGen では optional
を利用して yml ファイルを記述する必要がありました。
postGenCommand の活用
XcodeGen にはプロジェクトファイルが生成された後に実行したいコマンドを記載できる postGenCommand
というものがあります。Sansan では CocoaPods を利用しているため、↓ のように postGenCommand
に bundle 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 の preBuildScripts
や postBuildScripts
ではそこまでの順序を制御することはできません。
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
にそのバージョンが挿入されます。(Project の attributes
の説明にその記載があります)
そのため、私も xcodeVersion
を指定しようと思い当初は ↓ のように定義していました。
name: Sansan options: xcodeVersion: 12.2
しかし、これでは上手くいきませんでした。原因は簡単で、Project Spec にもしっかりと記載されていますが xcodeVersion
は String で指定する必要がありました 😇 (ドキュメントはしっかり読むようにしたいですね😢 )
そのため、↓ のように定義すればしっかり反映されるようになります。
name: Sansan options: xcodeVersion: "12.2"
ハマる人はそんなにいないかもしれないのですが、おまけでした。
Sansan における XcodeGen の運用方法
XcodeGen をスムーズにチームで利用できるようにするために、いくつか工夫を行ったのでその紹介も軽くですがしようと思います。
Mint を利用した XcodeGen のバージョン統一
チーム内で利用する XcodeGen のバージョンを統一するためにパッケージ管理ツールの Mint を利用しています。
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 が作られている状態だったりします。
また Xcode12 から利用できる機能のいくつかも対応されていなかったりするので、XcodeGen を使用する上ではこのような一定のリスクがあることにも注意しておきたいですね。
XcodeGen はコントリビュータも多く、結構活発にコントリビュートが行われている印象があります(日本人のコントリビュータも多い)。Xcode のアップデートが絡む内容はかなりポジティブに捉えるとコントリビュートチャンスとも言えるかもしれません。(XcodeGen が依存している XcodeProj という OSS からまずは追従する必要がある場合もあったり、XcodeGen 自体も深く理解する必要があるため非常に難易度は高そうなイメージですが...)
XcodeGen 参考リンク集
XcodeGen を導入するにあたって参考にさせて頂いた記事などを以下に列挙しようと思います🙏
どの記事も非常に参考になるものばかりなので、XcodeGen を導入する際には目を通すと作業をスムーズに行うことができるかと思います。
- Project Spec
- 何度か参照していますが、XcodeGen 公式 yml の書き方のようなものになります。一番参照することが多いと思います。
- XcodeGen 超入門
- タイトルの通り最初の方に読んでおくと超入門できます。
- XcodeGenによる新時代のiOSプロジェクト管理
- XcodeGen の概要・課題、yml の書き方など非常に丁寧にまとめられています。
- d-date さんの yml Example
- 自分で作成していた
project.yml
を綺麗に整理していくために参考にさせて頂きました。
- 自分で作成していた
- プロジェクトファイルからXcodeGenのproject.ymlを生成する
- 既存のプロジェクトから XcodeGen の
project.yml
を生成する方法についての記事です。これがなかったら導入できていなかった気がします...
- 既存のプロジェクトから XcodeGen の
- Xcodeプロジェクトの生成ツール「XcodeGen」のセットアップ&操作方法
- yml の記載方法、導入をスムーズに行うための Tips などが細かくまとめられています。
おわりに
XcodeGen は導入するのは中々大変かもしれませんが、それに見合った恩恵はあると思うので規模の大きいプロジェクトであれば積極的に導入したいですね。
チームでも運用を始めたばかりなので、XcodeGen の恩恵をチーム全体として感じられているかという実感はまだそこまでありませんが、改善を続けてチームでの開発をより効率的にしていきたいと思っています。