Flutter アプリで CocoaPods を完全撤廃して Swift Package Manager に移行した
業務で開発している Flutter アプリで、iOS ビルドから CocoaPods を完全に外しました。今は Swift Package Manager(以下 SPM)だけでビルドが通ります。Podfile も Podfile.lock も pod install も、リポジトリと CI から消えています。
移行は一発では終わらず、Flutter 3.35 での部分移行と Flutter 3.44 での完全撤廃の二段階になりました。この記事では、実際にやった手順と、途中で踏んだ罠をまとめます。
対象読者は、Flutter アプリの iOS ビルドを SPM に寄せたい人です。ネイティブ iOS アプリの移行話ではありません。
CocoaPods の残り時間
そもそもなぜ今やったのかという話です。CocoaPods はすでにメンテナンスモードに入っています。公式ブログでは trunk(podspec を登録する中央リポジトリ)の read-only 化スケジュールが公表されています。
2026 年 12 月 2 日に CocoaPods trunk は恒久的な read-only になります。以降、新しいバージョンの podspec は trunk に上がりません。
| 時期 | 出来事 |
|---|---|
| 2024年 | メンテナンスモード入りを表明。11月に read-only 化計画を発表 |
| 2025年5月 |
prepare_command を使う新規 Pod の受け入れ停止 |
| 2026年11月1〜7日 | read-only モードのテスト実施 |
| 2026年12月2日 | trunk が恒久的に read-only 化 |
read-only 化されても既存の Pod が消えるわけではないので、明日ビルドが壊れるという話ではありません。ただ、新しいバージョンの podspec は trunk に上がらなくなります。Firebase のような大手 SDK は今後 SPM だけで配布を続けるでしょうし、CocoaPods 側に残り続ける理由がもうない。維持コストを払って古い仕組みに留まるより、時間があるうちに移行しておくほうが安い、という判断です。
参考: CocoaPods Trunk Read-only Plan(公式ブログ)
Flutter の SPM 統合の現状
Flutter は 3.24 あたりから SPM 統合をオプトインで提供していて、3.44 以降はデフォルトで有効になっています。3.44 未満での有効化は pubspec.yaml に書くだけです。
flutter:
config:
enable-swift-package-manager: true
flutter config --enable-swift-package-manager でマシン単位で有効化する方法もありますが、チーム開発なら pubspec.yaml に書いてリポジトリで共有するほうが事故がないです。CI で有効化し忘れる、という古典的なミスを仕組みで防げるので。
有効化すると、Flutter は SPM 対応プラグインを FlutterGeneratedPluginSwiftPackage というローカルパッケージにまとめ、Xcode プロジェクトから参照させます。SPM 非対応のプラグインは従来どおり CocoaPods にフォールバックします。この「両対応期間はどちらも動く」設計のおかげで、段階的に移行できます。
注意点がひとつあります。私たちが最初に移行した Flutter 3.35 の時点では、プラグインを全部 SPM 化しても、Flutter framework 本体の組み込みに CocoaPods が使われていました。そのため Podfile は消し切れませんでした。ここが二段階になった理由です(後述)。
参考: Swift Package Manager for app developers(Flutter 公式)
現状把握
移行を始める前に、何が SPM 化されていて何が Pods に残っているかを調べます。見る場所は 2 つです。
SPM 化済みのプラグインは、生成されるローカルパッケージの Package.swift に列挙されています。このパッケージは SPM 有効化後の flutter pub get や flutter build ios --config-only で生成されます。
ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift
Pods に残っているものは ios/Podfile.lock の PODS: セクションを見れば分かります。
今回のアプリでは、firebase_core / firebase_crashlytics / firebase_analytics / webview_flutter_wkwebview / url_launcher_ios など 11 個のプラグインがすでに SPM 解決になっていました。プラグイン由来で Pods に残っていたのはたった 1 つです。
PODS:
- flutter_keyboard_visibility (6.0.0)
ボトルネックの正体と排除
flutter_keyboard_visibility は .podspec しか提供しておらず、Package.swift を持ちません。つまり SPM 非対応です。しかも pubspec.yaml に直接書いた覚えがない。依存グラフを追うと、オンボーディング画面で使っていた introduction_screen の推移依存でした。
pubspec.yaml
└─ introduction_screen (直接依存)
└─ flutter_keyboard_visibility (推移依存 / .podspec のみ)→ CocoaPods に残留
SPM 非対応のパッケージが見つかったときの選択肢は 3 つです。
- パッケージ側の SPM 対応を待つ
- SPM 対応の代替パッケージに乗り換える
- パッケージをやめて自前実装する
flutter_keyboard_visibility はメンテナンスが止まり気味で、待つ選択肢は現実的ではありませんでした。そして introduction_screen の使い方を確認したら、API 呼び出しは 1 ファイルの 3 箇所だけ。中身はページをスワイプしてドットを出すだけの画面です。これのためだけに CocoaPods を維持するのは割に合わないので、3 の自前実装を選びました。PageView とドット表示を素で書いても 200 行弱で収まります。
置き換えで見た目が変わっては困るので、先にゴールデンテストを追加して現状の見た目を固定してから置き換えました。この順序はおすすめです。置き換え後の差分がピクセル単位で検証できるので、レビューが楽になります。
移行手順
全体は 5 つの Phase に分けました。順序が重要で、Pods を先に消すと SPM 非対応パッケージがビルド不能になって詰みます。依存の排除が必ず先です。
| Phase | 内容 | 補足 |
|---|---|---|
| A | SPM 非対応依存の排除 | UI の自前実装への置き換え |
| B |
pubspec.yaml から不要パッケージ削除 |
pubspec.lock 更新 |
| C | iOS の Pods 撤去 |
Podfile.lock / Pods/ / pbxproj の Pods 参照 |
| D | CI の修正 |
pod install 関連処理の削除 |
| E | 検証 | ローカルビルド + 実機/シミュレータ + CI |
各 Phase は 1 コミットに分けました。途中で問題が出たとき git revert で戻れる粒度を保つためです。
なお、Phase C を最後まで完遂できるかは Flutter のバージョン次第です。Flutter framework 本体が SPM 組み込みに対応する前のバージョンでは、Podfile と最小限の Podfile.lock を残す必要があります(詳細は後述の「完全撤廃までの二段階」)。
Phase C で消すもの、書き換えるものは具体的にはこのあたりです。
-
ios/Podfile.lockとPods/ディレクトリの削除 -
ios/Runner.xcodeproj/project.pbxprojから Pods 関連のビルドフェーズ([CP] Check Pods Manifest.lockなど)とPods-Runner.xcconfigへの参照を削除 -
ios/Flutter/Debug.xcconfigなどから Pods の include 行を削除
-#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
-
ios/Runner.xcworkspace/contents.xcworkspacedataからPods.xcodeprojの参照を削除
- <FileRef
- location = "group:Pods/Pods.xcodeproj">
- </FileRef>
Phase D は使っている CI 次第ですが、やることは単純です。CI のスクリプトに pod install や CocoaPods 自体のインストール(brew install cocoapods や gem install cocoapods)が書いてあれば消します。消し忘れると、pod install が Podfile を見つけられずエラー終了して、そこで CI が止まります。
ハマりポイント
移行そのものより、こっちが本編かもしれません。
Crashlytics の dSYM アップロード
一番のハマりどころでした。
CocoaPods 時代の dSYM アップロードは、Run Script フェーズで ${PODS_ROOT}/FirebaseCrashlytics/upload-symbols を呼ぶ構成が定番でした。SPM 移行で ${PODS_ROOT} が消えるので、当然これは動かなくなります。
代わりに SPM の checkouts ディレクトリにある upload-symbols を直接参照する手もありますが、Firebase の公式ドキュメントが SPM 向けに案内しているのは run スクリプトを呼ぶ方式です。run は同じディレクトリの upload-symbols を呼び出しつつ dSYM の場所も自動で探索してくれるので、素直にこちらに従います。
本当の罠は run スクリプトへのパスです。Xcode が普通にビルドするときの checkouts は標準の DerivedData 配下にありますが、flutter build ios は BUILD_DIR を build/ios/ に差し替えるため、checkouts も build/ios/SourcePackages 配下に置かれます。標準 DerivedData 起点のパスを書くと、Flutter 経由のビルドで届きません。
最終的に落ち着いた Run Script はこれです。
# flutter build ios は BUILD_DIR を build/ios/ に差し替えるため、SPM の
# checkouts も build/ios/SourcePackages 配下に置かれる。
# SPM では upload-symbols 単体ではなく run スクリプトを呼ぶ(Firebase 公式仕様)。
# run が dSYM を自動探索するため DWARF_DSYM_* の手動指定は不要。
"${BUILD_DIR%/Build/*}/SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/run" \
-gsp "${PROJECT_DIR}/Runner/GoogleService-Info.plist" -p ios
${BUILD_DIR%/Build/*} で Build/ より上の階層に戻ってから SourcePackages を掘る形にすると、Xcode 直接ビルドでも flutter build ios でも同じスクリプトで通ります。
スクリプト本文だけでなく、Build Phase の Input Files も設定します。dSYM の生成後にスクリプトが走る依存関係を Xcode に伝えるためのもので、Firebase 公式ドキュメントに記載があります。
${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}
$(SRCROOT)/$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)
${PROJECT_DIR}/Runner/GoogleService-Info.plist
${TARGET_BUILD_DIR}/${EXECUTABLE_PATH}
Package.resolved のコミット方針
SPM を有効化すると Package.resolved が 2 箇所に生成されます。
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolvedios/Runner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
中身は同じものが二重に出るので片方を .gitignore したくなりますが、両方コミットしています。Xcode がどちらを読むかはビルドの起点(workspace か xcodeproj か)で変わるうえ、Apple も CI の再現性のためにコミットを推奨しているためです。ネイティブ SDK のバージョンを lock する役割は Podfile.lock からこのファイルに引き継がれるので、.gitignore に入れてはいけません。
完全撤廃までの二段階
冒頭に書いたとおり、移行は二段階になりました。
| 第一段階 | 第二段階 | |
|---|---|---|
| Flutter | 3.35 | 3.44 |
| プラグインの解決 | 全て SPM | 全て SPM |
| Flutter framework の組み込み | CocoaPods | SPM |
Podfile / Podfile.lock
|
残る(Flutter 本体用) | 削除 |
| CI の cocoapods | 必要(pod バイナリ) |
不要 |
第一段階(Flutter 3.35)でプラグインは全部 SPM になりました。ただ、Flutter framework 本体の組み込みがまだ CocoaPods 経由だったため、Podfile と最小限の Podfile.lock は残さざるを得ませんでした。「ほぼ移行したのに pod が消えない」という中途半端な期間が 1 か月ちょっとあります。
この時点の Podfile.lock の全文がこれです。Firebase の依存ツリーで 250 行あったファイルが、Flutter 本体 1 つだけになりました。
PODS:
- Flutter (1.0.0)
DEPENDENCIES:
- Flutter (from `Flutter`)
EXTERNAL SOURCES:
Flutter:
:path: Flutter
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
PODFILE CHECKSUM: 4c438addb11b6da45ed7ae408823d68256222460
COCOAPODS: 1.16.2
その後 Flutter を 3.44 に上げたタイミングで、Flutter 本体も SPM で組み込めるようになりました。Podfile の削除、xcconfig の Pods include 削除、workspace からの Pods.xcodeproj 参照削除まで一気にやって完全撤廃です。最後の削除だけで差分は 7 ファイル 153 行、すべて削除行です。削除だけの差分は気持ちがいい。
Flutter 本体が SPM 化されたことは、生成される Package.swift からも確認できます。3.44 では依存リストの最後に FlutterFramework が現れます。これが Podfile を消し切れるようになった証拠です。
let package = Package(
name: "FlutterGeneratedPluginSwiftPackage",
dependencies: [
.package(name: "webview_flutter_wkwebview", path: "../.packages/webview_flutter_wkwebview-3.26.0"),
.package(name: "firebase_crashlytics", path: "../.packages/firebase_crashlytics-5.2.4"),
.package(name: "firebase_core", path: "../.packages/firebase_core-4.11.0"),
// ...(中略)...
.package(name: "FlutterFramework", path: "../.packages/FlutterFramework") // ← 3.44 で登場
],
// ...
)
これから移行する人は、使っている Flutter のバージョンで Flutter framework 本体が SPM 組み込みに対応しているかを最初に確認してください。対応前のバージョンだと、プラグインを全部 SPM 化しても Podfile は消せません。それでも第一段階まで進めておく価値はあります。SPM 非対応依存の排除という一番重いところが終わっていれば、残りは Flutter のバージョンアップ後の削除作業だけなので。
まとめ
- CocoaPods trunk は 2026 年 12 月 2 日に read-only 化。移行するなら今がちょうどいい時期です
- 移行のボトルネックは SPM 非対応の推移依存。
Podfile.lockを見て、依存グラフを遡って出所を特定する - 「依存排除 → SPM 有効化 → Pods 撤去 → CI 修正」の順序を守る。Pods を先に消すと詰む
- Crashlytics の dSYM アップロードは
runスクリプト方式 +BUILD_DIR起点のパスに直す - Flutter framework 本体の SPM 組み込み対応前のバージョンでは
Podfileを消し切れない。それでも依存排除まで先にやっておく価値はある
pod install の待ち時間と Podfile.lock のコンフリクト解消から解放されただけでも、やった価値はありました。