前の記事で GitHub Actions のデプロイ環境は整いましたが、macOS ランナーは分単位で課金されます。
個人開発の小さなアプリには正直コストが気になるので、Xcode Cloud を試してみました。
Apple Developer Program に含まれているので、月 25 時間までは追加料金なしで使えます。
Flutter は Xcode Cloud で公式サポートされていないため、ひと手間必要でした。そのあたりをまとめます。
正直キャッシュ周りが謎で、ビルドするたびに結果が変わったりなどしたため、ここに書いたもので落ち着いていますが、よくわかっていないというのが正直なところです。
それをご理解頂けると助かります。
Xcode Cloud とは
Xcode Cloud は Apple が提供する CI/CD サービスで、Xcode から直接設定できます。
macOS・iOS・watchOS・tvOS アプリのビルドに特化しており、App Store Connect との連携がスムーズです。
Apple Developer Program の会員であれば月 25 時間の無料枠があり、個人開発レベルであれば十分まかなえることが多いです。
Flutter を Xcode Cloud で動かすには
Xcode Cloud のビルド環境には Flutter が入っていません。
また、Homebrew の Cask も使えないので brew install --cask flutter という方法も不可です。
解決策は ci_post_clone.sh の中で Flutter SDK を Git から直接クローンすることです。
こちらのFlutter公式にも載っています。
ディレクトリ構成
Xcode Cloud が認識する CI スクリプトは ios/ci_scripts/ 以下に置きます。
詳しいCIスクリプトのドキュメントはこちらを参照ください。
ios/
└── ci_scripts/
├── ci_post_clone.sh # クローン後に実行(Flutter インストール等)
├── ci_pre_xcodebuild.sh # xcodebuild 直前に実行
└── clean_spm_caches.sh # SPM キャッシュのクリーンアップ(共通処理)ci_post_clone.sh
クローン後に Flutter SDK をインストールして flutter pub get まで済ませます。
#!/bin/sh
# Xcode Cloud: install Flutter + dependencies after clone.
# Do NOT use `brew install --cask flutter` — Caskroom is unavailable on CI.
set -e
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
. "$SCRIPT_DIR/clean_spm_caches.sh"
# デフォルトの cwd は ios/ci_scripts/。リポジトリルートに移動する。
cd "$CI_PRIMARY_REPOSITORY_PATH"
echo "=== Installing Flutter SDK ==="
FLUTTER_DIR="$HOME/flutter"
if [ ! -d "$FLUTTER_DIR/bin" ]; then
git clone https://github.com/flutter/flutter.git --depth 1 -b stable "$FLUTTER_DIR"
fi
export PATH="$PATH:$FLUTTER_DIR/bin"
echo "=== Flutter precache (iOS) ==="
flutter precache --ios
echo "=== Flutter pub get ==="
flutter pub get
echo "=== CocoaPods ==="
if ! command -v pod >/dev/null 2>&1; then
if command -v brew >/dev/null 2>&1; then
HOMEBREW_NO_AUTO_UPDATE=1 brew install cocoapods
else
gem install cocoapods --no-document
fi
fi
echo "=== Flutter iOS config-only ==="
flutter build ios --config-only
clean_swiftpm_caches
echo "=== ci_post_clone complete ==="
exit 0flutter build ios --config-only が重要
flutter build ios --config-only を実行することで、FlutterGeneratedPluginSwiftPackage の iOS deployment target が IPHONEOS_DEPLOYMENT_TARGET に合わせて更新されます。
これをやらないと deployment target が 13.0 のままになり、Firebase などの SPM パッケージが要求するバージョンと合わずビルドエラーになります。
Firebase の SPM 問題と clean_spm_caches.sh
Xcode Cloud で Firebase を使っているとき、SPM のキャッシュが残っていると次のようなエラーが出ることがあります。
error: package at '...' already exists in the file systemこれは Xcode Cloud のキャッシュ機構と SwiftPM のキャッシュが干渉することが原因です。
対策として、ビルドごとにキャッシュをクリアしてワークスペース内のディレクトリにリダイレクトしています。
#!/bin/sh
# Xcode Cloud: Firebase/gRPC SPM の "already exists in file system" エラーを避ける
SWIFTPM_WORKSPACE_CACHE="/Volumes/workspace/.swiftpm-cache"
_remove_tree() {
target="$1"
[ ! -e "$target" ] && return 0
chmod -R u+w "$target" 2>/dev/null || true
rm -rf "$target"
}
_redirect_swiftpm_cache() {
link_path="$1"
parent_dir=$(dirname "$link_path")
mkdir -p "$parent_dir" || return 1
_remove_tree "$link_path" || return 1
ln -sfn "$SWIFTPM_WORKSPACE_CACHE" "$link_path"
}
clean_swiftpm_caches() {
echo "=== Cleaning SwiftPM caches ==="
_remove_tree "$SWIFTPM_WORKSPACE_CACHE"
mkdir -p "$SWIFTPM_WORKSPACE_CACHE"
for cache_root in \
"$HOME/Library/Caches/org.swift.swiftpm" \
"/Users/local/Library/Caches/org.swift.swiftpm" \
"$HOME/Library/org.swift.swiftpm" \
"/Users/local/Library/org.swift.swiftpm"; do
_remove_tree "$cache_root"
done
_remove_tree /Volumes/workspace/DerivedData
# xcodebuild は "local" ユーザーで動く — そのキャッシュもリダイレクト
_redirect_swiftpm_cache "/Users/local/Library/Caches/org.swift.swiftpm"
if [ "$HOME" != "/Users/local" ]; then
_redirect_swiftpm_cache "$HOME/Library/Caches/org.swift.swiftpm"
fi
echo "=== SwiftPM cache redirect OK -> $SWIFTPM_WORKSPACE_CACHE ==="
}/Users/local というのは Xcode Cloud が xcodebuild を実行するときのユーザーアカウントです。
自分の $HOME とは別ユーザーなので、両方のキャッシュをリダイレクトする必要があります。
ci_pre_xcodebuild.sh
xcodebuild 直前に実行されるスクリプトです。
ci_post_clone.sh でキャッシュのシンボリックリンクを張ったので、ここではその確認だけ行っています。
#!/bin/sh
set -e
LOCAL_SPM="/Users/local/Library/Caches/org.swift.swiftpm"
if [ -L "$LOCAL_SPM" ]; then
echo "=== ci_pre_xcodebuild: SwiftPM cache -> $(readlink "$LOCAL_SPM") ==="
else
echo "=== ci_pre_xcodebuild: warning: SwiftPM cache is not a workspace symlink ==="
fi
exit 0Xcode Cloud でのワークフロー設定
Product → Xcode Cloud → Create Workflows or Manage Workflows から設定します。
ただし、Archive などを作っておかないと、Create Workflows や Manage Workflows はグレーアウトしていると思います。
僕の環境では作らないと管理も作成も行えませんでした。。。(なんか微妙な。。。)
最初はios以下にあるRunner.xcworkspaceなどを開いて、Archiveなどを作成しておいてください。

基本的な設定はこんな感じです。

| 項目 | 設定値 |
|---|---|
| 開始条件 | main or deploy へのプッシュ、またはタグ |
| アクション | Build & Archive |
| 配布先 | TestFlight(内部テスター) |
証明書とプロビジョニングプロファイルは、App Store Connect と連携しているため自動で管理されます。
fastlane match は不要で、Xcode Cloud 側で証明書を発行・管理してくれます。
GHA と Xcode Cloud の比較
実際に両方使ってみた感想です。
| GitHub Actions | Xcode Cloud | |
|---|---|---|
| コスト | macOS ランナーは従量課金 | 月 25 時間無料 |
| ビルド | subosito/flutter-actionなどのアクションやfastlaneで簡単 | ci_post_clone.sh で手動インストール |
| 証明書管理 | fastlane match が必要 | 自動管理 |
| デバッグのしやすさ | ローカルで act を使える | ログを見るしかない(謎が多いが、セットアップを終えたらこっちがいい) |
コスト面では Xcode Cloud が有利ですが、Flutter アプリの場合は ci_post_clone.sh のメンテナンスが必要なのと、デバッグがやや面倒です。
GHAで回しておいて、仕方なく節約が必要になったら Xcode Cloud に移行してという使い方が個人開発には合っている気がします。
まとめ
個人開発の iOS アプリならGHAで済ませておいて、すごい回す必要があるならxcode cloudというのがいい気がします。
Claude や Cursor などのサービスを活用してスマホから Cloud 上のエージェントに処理させて、ビルド結果を見たいということがあると思うので、そういう時は Xcode Cloud を使ってガンガン回す必要がありそうです。
シリーズ記事
この記事は Flutter iOS ビルド・配布の一連の流れの最終回です。前の 2 本もあわせてどうぞ。
- Flutter × Fastlane でローカルから TestFlight にアップする — Mac 上で fastlane match を使い、初回の署名・TestFlight アップロードまで
- Flutter × GitHub Actions で TestFlight に自動デプロイする — 同じ Fastfile を GHA から呼び出して自動デプロイ