この記事はYUMEMI.grow Mobile #2で発表した内容を記事にしたものです!
最近Xcode Cloudを触る機会があったので、その特徴や設定方法、使い方などを簡単にまとめてみました!
Xcode Cloudとは?
Xcode CloudとはいわゆるCIで、GitHub ActionsやBitrise, CircleCI, Azure Pipelinesなどと同じく、継続的インテグレーション・デリバリーサービスで、その中でテストやビルドの実行、ビルドしたipaの配信準備などを行うことができるものです。
特徴はなんといっても、Apple純正のファーストパーティーのもので、iOSアプリ含めて、Appleプラットフォーム上でのアプリ開発の中で密に連携が取れるようになっており、XcodeやAppStoreConnectの中で使うことができます。
筆者は現時点でGitHubActionsやBitrise, AzurePipelinesのCIを使ったことがあります
それ比較すると以下のような特徴や長所があると思います
- 証明書やProvisionningProfileの管理を自動で行ってくれる
- TestFlightへのipaのアップロード設定が簡単
- API Keyの設定やFastlaneのツールを準備する必要もない!
- XcodeやAppStoreConnectに組み込まれているので、普段の開発導線上からCIの設定や実行状況、履歴についても確認できる
- アプリ開発のツールなどのアップデートへの準拠が早い
- CIの実行トリガーの設定もシンプル
- AzurePipelinesはPR作成時のトリガーの設定などはかなり癖があるので、直感的で分かりやすいと思います
これらは主にファーストパーティーだからこそのメリットですが、特に①・②はCIを設定する上ではいつも面倒に感じていたところなので、ここが自動化されるだけでも、取り入れる価値はあるかなと思います
逆に感じたデメリットは以下のとおりです
- 新しいサービスなので、情報が少ない
- 設定をミスった時のエラーの内容が分かりにくい
- カスタマイズ性が低い
- BuildやTest、Archiveなどのアクションの実行が必須で、例えばSwiftLintだけ実行などはできない
- カスタムのスクリプトの実行タイミングに制約がある
- SPMを除くキャッシュの制御は基本できない
- 設定ファイルをGit管理することができない
- ブランチごとに異なる設定を適用することが難しい
- ブランチごとに別のXcodeのバージョンを適用したい場合はどうするのがいいのか?
- ThirdPartyのツールのセットアップが難しい
- 例えばBitriseなどだとCocoaPodsのインストールなどはプリセットされた機能をぽちぽちすればできますが、Xcode Cloudでは現時点では難しいです
- CocoaPodsやCarthageのキャッシュの管理が難しいので、ライブラリ管理はSPMに寄せるのがよいです
- Fastlaneなどもできる限り使わないようにした方がよさそうです
まだ情報が少ないのは新しいサービスなので、しかたないですね
現状はAppleDeveloperに登録している人は、月25時間まで無料で実行することができます(参考)

今回は上記の特徴を踏まえて、以下の方針で使用していきたいと思います
- TestFlightアプリの配信に利用する
- TriggerはdevelopなどのブランチへのPush(PRのトリガーは利用しない)
- Featureブランチなどでは実行しないようにする
- UnitTestの実行は別のCIを利用する
理由としては、無料の実行時間に限りがあるので、XcodeCloudの長所である証明書の自動管理を生かすために、TestFlightへの配信に限定して利用するためです
Featureブランチなどで実行しないのも、実行時間を抑えるためです
UnitTestは基本シミュレーターで実行するため、特に証明書管理なども不要なため、他のCIサービスでも設定が非常にしやすいです
Xcode Cloudの設定方法
次は実際にXcodeCloudを既存のアプリで使えるようにしましょう
今回のアプリはXcodeCloudSampleにアップしています。
このアプリは既存の記事で書いたSPMベース構成のアプリです。

Xcode Cloudの初期設定
XcodeCloudはXcodeとAppStoreConnectで設定できますが、普段はXcodeを使うことの方が多いと思うので、Xcodeで設定を進めます
実際の設定などはAppStoreConnectの方が詳細に行うことができます
まずはXcodeの左のメニューの一番右のタブのCloudのタブを開き、Get Startedをクリックします

次にどのアプリをビルドするかを選択します。
今回はDebug用とRelease用の2つのアプリのうち、TestFlightで配信するRelease用のアプリを選択します

Teamが選択されていない場合、以下のようなアラートが出ます

✎アイコンのところを選択することで、どのTeamを使うかを選択できます

問題なければ、Nextをクリックします

次に以下の画面が出るので、そのままNextをクリックします

次にGitHubの設定画面への導線が出るので、Grant Accessをクリックします

クリックすると、ブラウザでAppStoreConnectの画面が自動で表示されます
そのままGitHubでステップ1を完了をクリックします

次にGitHubの画面が表示されるので、その中でXcodeCloudのinstall先のアカウントを選択します
今回は筆者の個人アカウントにインストールします

次にどのリポジトリにinstallするかを確認します
今回はサンプルで作ったリポジトリXcodeCloudSampleに限定します
問題なければ、Installをクリックします

これでXcodeCloudとGitHubの接続は完了です
Xcodeで続けるをクリックします

Xcodeのアプリに戻り、以下のようにGitHubとの接続が完了したら、Nextをクリックします

Completeをクリックします

試しに次の画面でStart Buildをクリックして、XcodeCloudを動かしてみます!

動かすと、以下のようにXcode上で実行状況を確認できます!

完了すると、以下のように結果が表示されます!

Xcode CloudのWorkflowの作成・編集
次にXcodeCloudのWorkflowを作成・編集して、使えるようにしましょう
XcodeCloudのメニューのManage Workflowsをクリックします
もしくはメニュー > Product > Xcode Cloudからも設定することもできます

クリックすると、以下のような画面が表示されます
新規作成の場合は、左下の+をクリックします

編集の場合は、編集したいWorkflowを選択して、Editをクリックします

そうすると、以下のような画面が表示されます
Generalのメニューは、Workflowの名前や詳細、リポジトリのURLなどを確認・設定できます

EnvironmentのメニューはXcodeやmacOSのバージョン、環境変数を設定できます

Start ConditionsメニューはWorkflowを自動で起動する場合のトリガーを設定できます
トリガーとしては、①Branch Changes, ②Pull Request Changes, ③Tag Changesを指定できます

例えばBranch Changesのトリガーの場合は、Source BranchやFiles and Foldersなどで特定のブランチへのPushやファイルの変更だけを指定することもできます

Actionsメニューでは+をクリックすると、以下のようなアクションを追加することがわかります
今回はTestFlightへipaをアップロードすることが目的なので、Archiveのアクションを追加します

最終的には以下のような設定になりました
Deployment PreparationはTestFlight(Internal Testing Only)を選択しています

Post-Actionsメニューは、上記のアクションに実行するアクションを選択できます
現時点では①TestFlight External Testing, ②TestFlight Internal Testing, ③Notifyを選択できます

今回は内部テスター向けなので、TestFlight Internal Testingを追加します
Artifactに上記のActionのArchive-iOSアクションを指定します
GroupsにはAppStoreConnectのTestFlightグループを選択して、指定します

ちなみにNotifyを追加すると、以下のようなメニューが表示されます。
どのタイミングで通知するかも選択でき、Slackへの通知も標準でサポートされています

Slackへの通知はこんな感じです

この設定で動かします

正常に完了すると、以下のようになります

Xcode Cloudの使い方
環境変数
XcodeではBuildPhaseで利用できる環境変数が色々ありますが、XcodeCloudでも利用できる環境変数も用意されています
Environment variable referenceに記載がありますが、例えば$CI_PROJECT_FILE_PATHを今回のサンプルアプリで実行すると以下のように出力されます
/Volumes/workspace/repository/XcodeCloudSample.xcworkspace
カスタムスクリプト
CIではセットアップなど色々カスタムのスクリプトを実行したい場面があると思いますが、XcodeCloudでもカスタムスクリプトの実行がサポートされています
公式ドキュメントは、Writing custom build scriptsです
以下の公式ページのイメージの通り、XcodeCloudでは、①clone後、②xcodebuild前、③xcodebuild後の3つのタイミングで実行することができます

そしてそれぞれ以下のファイルをルートディレクトリに作成したci_scriptsディレクトリの直下に作成することで、その処理が自動で実行されます
- ci_post_clone.sh
- ci_pre_xcodebuild.sh
- ci_post_xcodebuild.sh
後述しますが、今回は①ci_post_clone.sh, ②ci_pre_xcodebuild.shを使用しています
ちなみに実行パスは/Volumes/workspace/repository/ci_scriptsなので、cd ..を実行することで、ルートディレクトリに移動することができます
その他注意点・確認点
Swift Package ManagerのPluginを使用する
SPMでプラグインを使用する場合、許可をすることが必要です
Xcodeで開発するときはアラートが表示され、そこで処理をすればOKです
しかしXcodeCloudではUI上のアラートを操作することができないので、XcodeBuildToolPluginをCI上でも使うを参考に以下のコードをci_pre_xcodebuild.shに記載しました
defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES
SwiftGenを使う
SwiftGenを使っているプロジェクトでは、例えばLocalizeやColorやImageのアセットファイルを元に、それらのリソースファイルへアクセスするためのswiftファイルを自動生成して、そのenumなどを使用するようにすることが多いと思います
しかしSwiftGenPluginを使ったプロジェクトの場合、生成したファイルが見つかりませんというエラーがXcodeCloudで発生する事象が確認しました
ググってみると、他の方にも同じ事象が起きているようでした(参考: Xcode Cloud is not able to access swift-gen generated file)
初めはどうやって解決しようかと思いましたが、試行錯誤した結果、ci_scripts/ci_post_clone.shで自動生成するファイルをtouchコマンドで作成することで回避することができました
touch $CI_PROJECT_FILE_PATH/../XcodeCloudSamplePackage/Sources/XcodeCloudSamplePackage/Generated/StringsGenerated.swift
touch $CI_PROJECT_FILE_PATH/../XcodeCloudSamplePackage/Sources/XcodeCloudSamplePackage/Generated/CommonAssets.swift
ビルド番号
TestFlightへアップロードするipaはアプリバージョンとビルド番号の組み合わせが一意になっている必要があります
XcodeCloudでは自動でビルド番号をインクリメントしてくれるので、ビルド番号を開発者が管理する必要はないです
逆にいうと、以前書いたようなビルド番号のカスタマイズはできないです

ビルド番号についてはAppStoreConnectから確認することができます
ビルド番号の変更もここからできます

輸出コンプライアンス情報の設定
TestFlightのアプリの配信まで設定している場合、輸出コンプライアンスの設定がされてないと、XcodeCloudのTestFlightの配信のステップでエラーになります
AppStoreConnectのTestFlightの画面で、コンプライアンスがありませんとなっている状態です(参考: Complying with Encryption Export Regulations)


手動でTestFlightアプリを配信する場合は問題ないですが、このままでは自動配信ができないので、アプリの設定を変更しましょう。
今回は標準的な暗号化アルゴリズムしか使用していないので、
具体的には以下のように、Info.plistにApp Uses Non-Exempt Encryption = NOの設定を追加します

これで以下のように、コンプライアンスについて確認されることもなくなり、TestFlightアプリも配信されました!


Webhookの設定
AppStoreConnectの設定画面からは、WebhookのURLを発行することもできます

まとめ
今回初めてXcodeCloudを使ってみました
Apple純正ということもあり、証明書の管理が不要だったり、TestFlightへのアップロードが簡単だったことはかなり大きいメリットかなと感じました
一方まだ制約も大きく、アーキテクチャ構成によっては利用が難しい場合もかなりあるかなと感じました
ただ今後どんどんXcodeCloudも便利になっていくかなと思うので、やはりAppleのプラットフォームで開発をしていく私たちはXcodeCloudを実行しやすいようなアプリ構成、つまりライブラリはSwiftPackageManagerで管理する、できる限りThirdParty製のツールを使わないなど、シンプルな構成のアプリを目指すのがいいのかなと感じました!
また今回はXcodeでの操作を中心に行いましたが、AppStoreConnectの方が機能が豊富で、ログも見やすいので、詳しく設定や分析をしたいときはAppStoreConnectで確認するとよさそうです!