Swift Package ManagerをTuist経由で導入し、CocoaPodsと併用しながら移行する
株式会社DIGITALIOで開発しているiOS版 ECナビアンケートアプリ は、依存関係の管理にCocoaPodsを使っていました。本記事では、依存関係の管理をCocoaPodsからSPM(Swift Package Manager)に移行した経緯と手法について書きます。
TL;DR
想定読者: Tuistでプロジェクトを生成していて、CocoaPodsからSPMへの移行を検討している方
依存関係の宣言をTuistの定義ファイルに集約でき、移行は「定義ファイルへの追加と Podfile からの削除」の繰り返しで済んだ
広告メディエーションのSDK本体とアダプターだけCocoaPodsに残し、SPMと併用で運用できている
前提
本記事は、TuistでXcodeプロジェクトを生成している環境を前提にしています。前回の記事では、 project.pbxproj のコンフリクトをなくすためにTuistを導入し、ビルドターゲットを Project.swift に定義して tuist generate で生成する運用に切り替えました。
zenn.dev
生成物はGit管理から除外しているため、ブランチの切り替えやファイルの追加のたびに make sync-develop でプロジェクトを再生成しています。
SPM移行のきっかけ
きっかけはFirebaseから届いたメールでした。Firebase SDKは2026年10月にCocoaPodsへの新バージョン公開を停止し、以降はpodspecからのインストールが公式サポートの対象外となり、SPMまたは手動インストールへの移行が案内される、という内容です。手順は公式の移行ガイドにまとまっています。
firebase.google.com
これはFirebase固有の話ではありません。CocoaPods Trunk自体も、2026年12月2日にread-onlyモードへ移行することが以前から告知されており、同じ流れにあります1。
blog.cocoapods.org
CocoaPods経由でFirebaseを使い続けても公開済みのバージョンは動きますが、新機能やバグ修正は受け取れなくなります2。この告知を機に、依存関係管理をCocoaPodsからSPMへ移行することにしました。
依存関係を種類ごとに分けて移行
まず、Tuist経由でSPMを利用するために、依存関係を記述する Tuist/Package.swift を作成しました。
// swift-tools-version: 5.9
import PackageDescription
#if TUIST
import ProjectDescription
let packageSettings = PackageSettings(
productTypes: [:]
)
#endif
let package = Package(
name: "ECNaviEnqueteApp",
dependencies: [
// ここに追記していく
]
)
次に、 Podfile に書かれていた依存関係を、ネットワーク、認証、UI、その他の依存関係といった種類ごとに分類して順番に移行しました。種類ごとに分けたのは、1つのPRで動作確認すべき範囲を絞り、問題が起きたときに原因を特定しやすくするためです。
どのタスクも手順自体は共通で、次の3ステップです。実際にFirebaseを移行した例で見てみます。
Tuist/Package.swift に対象の依存関係を追加する
// ...
- .package(
- url: "firebase/firebase-ios-sdk",
- exact: "12.14.0"
- ),
.package(
url: "kaishin/Gifu",
exact: "3.4.0"
),
Project.swift で .external(name:) として依存関係を宣言する
// ...
- .external(name: "FirebaseAnalytics"),
- .external(name: "FirebaseMessaging"),
- .external(name: "FirebaseRemoteConfig"),
- .external(name: "FirebaseInAppMessaging-Beta"),
- .external(name: "FirebaseCrashlytics"),
.external(name: "Gifu"),
CocoaPodsではsubspec( Firebase/InAppMessaging など)で選んでいた機能が、SPMでは1つのパッケージが公開する複数のプロダクトになります。
.external(name:) にはSPM側のプロダクト名を指定するため、Pod側の名前と異なるものもあります。
Podfile から該当のPodを削除する
- # Firebase
- pod 'Firebase/Core', '>10.22.0'
- pod 'Firebase/Analytics'
- pod 'Firebase/Messaging'
- pod 'Firebase/RemoteConfig'
- pod 'Firebase/InAppMessaging'
- pod 'Firebase/Crashlytics'
-
...
依存関係の解決はTuistが担うようになるため、 Makefile に tuist install を追加しました。依存関係の解決はプロジェクト生成時に済んでいる必要があるため、 tuist generate の前に置いています。
sync-develop:
cp ECNaviEnqueteApp/Resource/GoogleService-Info-Develop.plist ECNaviEnqueteApp/Resource/GoogleService-Info.plist
- tuist install
tuist generate --no-open
bundle exec pod install
変更を反映するには make sync-develop を実行します。次の順にコマンドが動き、SPMとCocoaPodsのロックファイルも更新されます。
移行でつまずいた点と残った課題
SPMへの移行では、単に依存関係の宣言先を変えるだけでなく、依存関係の利用方法によって個別対応が必要になったものもありました。
スクリプトのパスが変わった
移行した依存関係の1つであるFirebase Crashlyticsは、ビルド後にdSYMをアップロードするビルドスクリプト(Run Script)を実行します。
このスクリプトは、CocoaPodsでは Pods/ 配下にありましたが、SPMでは取得したパッケージのディレクトリ内に移ります。Tuistはこれを Tuist/.build/checkouts/ 配下に置くため、移行のタイミングで定義ファイルのスクリプトが参照するパスも変更しました。
- "${PODS_ROOT}/FirebaseCrashlytics/run"
- "${SRCROOT}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run"
コードの追加対応を伴ったもの
依存関係によっては、コードの追加が必要になった箇所もありました。例えば、ネットワークレイヤーを抽象化するMoyaをCocoaPodsからSPMに移行してビルドし直すと、次のようなエラーが出ました。
FooBarClass.swift:xx:yy Cannot find 'UIDevice' in scope
原因は、CocoaPodsが各Podに生成するumbrella header( Moya-umbrella.h )とmodulemapでした。新規のプロジェクトで Podfile に use_frameworks! を指定してMoyaを導入すると、次のようなファイルが生成されます(抜粋)。
// Moya-umbrella.h
#ifdef OBJC
#import <UIKit/UIKit.h>
#else
// ...
// Moya.modulemap
framework module Moya {
umbrella header "Moya-umbrella.h"
export *
module * { export * }
}
umbrella headerがUIKitを、UIKitがFoundationを読み込み、modulemapの export * がそれらを再エクスポートします。そのため、 import Moya と書くだけでFoundationとUIKit双方のシンボルが使えていました。
import Moya
│
├─ CocoaPods時代: FoundationもUIKitもついてきた ٩(ˊᗜˋ*)و
│
└─ SPM: Moyaしか来ない
(;゚Д゚) 「UIDeviceって誰…?」
SPM経由ではこの経路が失われます。その結果、実際にはFoundationやUIKitのAPIを直接使っているのに、Moyaのimportだけに依存していた数ファイルがコンパイルエラーになりました。
対象ファイルに import Foundation / import UIKit を追加して解決しました。暗黙の再エクスポートは他のPodでも起きうるため、移行対象を分けておくと、コンパイルエラーの原因を直前に移したPodに絞り込めます。
一部の依存関係はSPM対応待ち
すべての依存関係をSPMへ移行できたわけではありません。広告配信のために利用している広告メディエーションでは、SDK本体こそSPM対応が完了していました。しかし、私たちが利用している複数の広告ネットワークに対応するアダプターは、CocoaPods版のSDK本体に依存する構成でした。
アダプターのPodはSDK本体のPodを依存関係として引き込むため、SDK本体だけSPMへ移すと、同じSDKがCocoaPods版とSPM版の2系統で宣言された状態になります。検証したところ、ビルドは通りました。
ただし版が異なる場合、どちらが採用されるかを示す警告は出ず、手元の検証ではSPM版が採用されました3。意図しない版が混ざる状態を避けるため、アダプターのSPM対応が済むまでは、SDK本体もCocoaPodsに残すこととしました。
開発ツールはmiseで管理
本アプリでは、Swiftの静的解析ツールであるSwiftLintと、アプリで使用しているオープンソースライブラリのライセンス一覧を自動生成するLicensePlistを使用しています。
これらは、アプリ本体にリンクされるわけではない開発ツールです。CocoaPods管理から切り出すにあたって、前回Tuistのバージョン固定用に導入したmiseへ寄せ、ツールのバージョン管理を一元化しました。
[tools]
- license-plist = "3.27.9"
- swiftlint = "0.63.3"
tuist = "4.195.9"
これらを呼ぶスクリプトは、 Pods/ 配下のパスからmiseのshimを指すように変更しました。
- "${PODS_ROOT}/SwiftLint/swiftlint"
- "${MISE_DATA_DIR:-${HOME}/.local/share/mise}/shims/swiftlint"
なお、LicensePlistは、SPM管理のパッケージを検出するための設定が必要でした。LicensePlistは、SPMのパッケージを Package.resolved から探します。しかし本アプリでは、Tuistが Tuist/Package.resolved を管理しており、LicensePlistの既定の探索先には存在せず、アプリ内のライセンス一覧に含まれませんでした4。
そこで、 license_plist.yml の packagePaths に Tuist/Package.swift を指定すると、同じディレクトリの Package.resolved が参照されるようになりました。
- options:
- packagePaths:
- - Tuist/Package.swift
移行後の状況
一連の移行の結果、依存関係の状態は次のようになりました。
状態
対象
SPMへ移行済み
ネットワーク、認証、UI、その他の依存関係
miseへ移行済み
SwiftLint、LicensePlist
CocoaPodsのまま維持
広告メディエーションSDK本体とアダプター
Podfile に書かれていたSDK本体とアダプター以外の21件のPod宣言は、すべてなくなりました。残りの依存関係は、提供元のリリースノートでSPM対応の有無を確認しながら追従していきます。
まとめ
Tuistの .external(name:) で依存関係を宣言できるため、移行作業の多くは定義ファイルへの追加と Podfile からの削除という小さな差分の繰り返しになりました。種類ごとに分けて、独立してレビュー・マージできる単位で進めたことで、各PRも小さく保てました。同様の移行を控えている場合は、以下の手順で実施するとスムーズに移行が進められると思います。
前回の記事 を参考にXcodeプロジェクトの管理方式を決める。
依存関係ごとに、SPMへの対応状況を調べる。
種類ごとに分け、動作確認しやすい粒度で順番に移行する。
CocoaPodsに残る依存関係は、12月2日以降は新バージョンがTrunkに公開されなくなります。提供元のSPM対応状況を確認し、未対応のものはアダプターごと移行できる時期を見極める必要があります。
脚注
read-only化後も既存のPodは取得できるが、Trunkに新しいPodやバージョンは公開できなくなる。独自のspecリポジトリで配布されているPodは影響を受けない。 ↩︎
Firebase公式の移行ガイドによれば、CocoaPodsで最後に公開されるメジャーバージョンは12とされている。 ↩︎
CocoaPodsとSPMのバージョンを入れ替えて2通りで検証した。いずれも、ビルドしたバイナリに含まれていたのはSPM版で、新旧は関係なかった。どのパターンでも同じかまでは検証できていない。 ↩︎
既定の探索先は、プロジェクトルートの Package.swift と、 *.xcodeproj および *.xcworkspace 配下の xcshareddata/swiftpm/Package.resolved である。詳細は LicensePlistのREADME を参照。 ↩︎
Mentions
Swift Package ManagerをTuist経由で導入し、CocoaPodsと併用しながら移行する 株式会社DIGITALIOで開発しているiOS版 ECナビアンケートアプリ は、依存関係の管理にCocoaPodsを使っていました。本記事では、依存関係の管理をCocoaPodsからSPM(Swift Package Manager)に移行した経緯と手法について書きます。 TL;DR 想定読者: Tuistでプロジェクトを生成していて、CocoaPodsからSPMへの移行を検討している方 依存関係の宣言をTuistの定義ファイルに集約でき、移行は「定義ファイルへの追加と Podfile からの削除」の繰り返しで済んだ 広告メディエーションのSDK本体とアダプターだけCocoaPodsに残し、SPMと併用で運用できている 前提 本記事は、TuistでXcodeプロジェクトを生成している環境を前提にしています。前回の記事では、 project.pbxproj のコンフリクトをなくすためにTuistを導入し、ビルドターゲットを Project.swift に定義して tuist generate で生成する運用に切り替えました。 zenn.dev 生成物はGit管理から除外しているため、ブランチの切り替えやファイルの追加のたびに make sync-develop でプロジェクトを再生成しています。 SPM移行のきっかけ きっかけはFirebaseから届いたメールでした。Firebase SDKは2026年10月にCocoaPodsへの新バージョン公開を停止し、以降はpodspecからのインストールが公式サポートの対象外となり、SPMまたは手動インストールへの移行が案内される、という内容です。手順は公式の移行ガイドにまとまっています。 firebase.google.com これはFirebase固有の話ではありません。CocoaPods Trunk自体も、2026年12月2日にread-onlyモードへ移行することが以前から告知されており、同じ流れにあります1。 blog.cocoapods.org CocoaPods経由でFirebaseを使い続けても公開済みのバージョンは動きますが、新機能やバグ修正は受け取れなくなります2。この告知を機に、依存関係管理をCocoaPodsからSPMへ移行することにしました。 依存関係を種類ごとに分けて移行 まず、Tuist経由でSPMを利用するために、依存関係を記述する Tuist/Package.swift を作成しました。 // swift-tools-version: 5.9 import PackageDescription #if TUIST import ProjectDescription let packageSettings = PackageSettings( productTypes: [:] ) #endif let package = Package( name: "ECNaviEnqueteApp", dependencies: [ // ここに追記していく ] ) 次に、 Podfile に書かれていた依存関係を、ネットワーク、認証、UI、その他の依存関係といった種類ごとに分類して順番に移行しました。種類ごとに分けたのは、1つのPRで動作確認すべき範囲を絞り、問題が起きたときに原因を特定しやすくするためです。 どのタスクも手順自体は共通で、次の3ステップです。実際にFirebaseを移行した例で見てみます。 Tuist/Package.swift に対象の依存関係を追加する // ... - .package( - url: "firebase/firebase-ios-sdk", - exact: "12.14.0" - ), .package( url: "kaishin/Gifu", exact: "3.4.0" ), Project.swift で .external(name:) として依存関係を宣言する // ... - .external(name: "FirebaseAnalytics"), - .external(name: "FirebaseMessaging"), - .external(name: "FirebaseRemoteConfig"), - .external(name: "FirebaseInAppMessaging-Beta"), - .external(name: "FirebaseCrashlytics"), .external(name: "Gifu"), CocoaPodsではsubspec( Firebase/InAppMessaging など)で選んでいた機能が、SPMでは1つのパッケージが公開する複数のプロダクトになります。 .external(name:) にはSPM側のプロダクト名を指定するため、Pod側の名前と異なるものもあります。 Podfile から該当のPodを削除する - # Firebase - pod 'Firebase/Core', '>10.22.0' - pod 'Firebase/Analytics' - pod 'Firebase/Messaging' - pod 'Firebase/RemoteConfig' - pod 'Firebase/InAppMessaging' - pod 'Firebase/Crashlytics' - ... 依存関係の解決はTuistが担うようになるため、 Makefile に tuist install を追加しました。依存関係の解決はプロジェクト生成時に済んでいる必要があるため、 tuist generate の前に置いています。 sync-develop: cp ECNaviEnqueteApp/Resource/GoogleService-Info-Develop.plist ECNaviEnqueteApp/Resource/GoogleService-Info.plist - tuist install tuist generate --no-open bundle exec pod install 変更を反映するには make sync-develop を実行します。次の順にコマンドが動き、SPMとCocoaPodsのロックファイルも更新されます。 移行でつまずいた点と残った課題 SPMへの移行では、単に依存関係の宣言先を変えるだけでなく、依存関係の利用方法によって個別対応が必要になったものもありました。 スクリプトのパスが変わった 移行した依存関係の1つであるFirebase Crashlyticsは、ビルド後にdSYMをアップロードするビルドスクリプト(Run Script)を実行します。 このスクリプトは、CocoaPodsでは Pods/ 配下にありましたが、SPMでは取得したパッケージのディレクトリ内に移ります。Tuistはこれを Tuist/.build/checkouts/ 配下に置くため、移行のタイミングで定義ファイルのスクリプトが参照するパスも変更しました。 - "${PODS_ROOT}/FirebaseCrashlytics/run" - "${SRCROOT}/Tuist/.build/checkouts/firebase-ios-sdk/Crashlytics/run" コードの追加対応を伴ったもの 依存関係によっては、コードの追加が必要になった箇所もありました。例えば、ネットワークレイヤーを抽象化するMoyaをCocoaPodsからSPMに移行してビルドし直すと、次のようなエラーが出ました。 FooBarClass.swift:xx:yy Cannot find 'UIDevice' in scope 原因は、CocoaPodsが各Podに生成するumbrella header( Moya-umbrella.h )とmodulemapでした。新規のプロジェクトで Podfile に use_frameworks! を指定してMoyaを導入すると、次のようなファイルが生成されます(抜粋)。 // Moya-umbrella.h #ifdef OBJC #import <UIKit/UIKit.h> #else // ... // Moya.modulemap framework module Moya { umbrella header "Moya-umbrella.h" export * module * { export * } } umbrella headerがUIKitを、UIKitがFoundationを読み込み、modulemapの export * がそれらを再エクスポートします。そのため、 import Moya と書くだけでFoundationとUIKit双方のシンボルが使えていました。 import Moya │ ├─ CocoaPods時代: FoundationもUIKitもついてきた ٩(ˊᗜˋ*)و │ └─ SPM: Moyaしか来ない (;゚Д゚) 「UIDeviceって誰…?」 SPM経由ではこの経路が失われます。その結果、実際にはFoundationやUIKitのAPIを直接使っているのに、Moyaのimportだけに依存していた数ファイルがコンパイルエラーになりました。 対象ファイルに import Foundation / import UIKit を追加して解決しました。暗黙の再エクスポートは他のPodでも起きうるため、移行対象を分けておくと、コンパイルエラーの原因を直前に移したPodに絞り込めます。 一部の依存関係はSPM対応待ち すべての依存関係をSPMへ移行できたわけではありません。広告配信のために利用している広告メディエーションでは、SDK本体こそSPM対応が完了していました。しかし、私たちが利用している複数の広告ネットワークに対応するアダプターは、CocoaPods版のSDK本体に依存する構成でした。 アダプターのPodはSDK本体のPodを依存関係として引き込むため、SDK本体だけSPMへ移すと、同じSDKがCocoaPods版とSPM版の2系統で宣言された状態になります。検証したところ、ビルドは通りました。 ただし版が異なる場合、どちらが採用されるかを示す警告は出ず、手元の検証ではSPM版が採用されました3。意図しない版が混ざる状態を避けるため、アダプターのSPM対応が済むまでは、SDK本体もCocoaPodsに残すこととしました。 開発ツールはmiseで管理 本アプリでは、Swiftの静的解析ツールであるSwiftLintと、アプリで使用しているオープンソースライブラリのライセンス一覧を自動生成するLicensePlistを使用しています。 これらは、アプリ本体にリンクされるわけではない開発ツールです。CocoaPods管理から切り出すにあたって、前回Tuistのバージョン固定用に導入したmiseへ寄せ、ツールのバージョン管理を一元化しました。 [tools] - license-plist = "3.27.9" - swiftlint = "0.63.3" tuist = "4.195.9" これらを呼ぶスクリプトは、 Pods/ 配下のパスからmiseのshimを指すように変更しました。 - "${PODS_ROOT}/SwiftLint/swiftlint" - "${MISE_DATA_DIR:-${HOME}/.local/share/mise}/shims/swiftlint" なお、LicensePlistは、SPM管理のパッケージを検出するための設定が必要でした。LicensePlistは、SPMのパッケージを Package.resolved から探します。しかし本アプリでは、Tuistが Tuist/Package.resolved を管理しており、LicensePlistの既定の探索先には存在せず、アプリ内のライセンス一覧に含まれませんでした4。 そこで、 license_plist.yml の packagePaths に Tuist/Package.swift を指定すると、同じディレクトリの Package.resolved が参照されるようになりました。 - options: - packagePaths: - - Tuist/Package.swift 移行後の状況 一連の移行の結果、依存関係の状態は次のようになりました。 状態 対象 SPMへ移行済み ネットワーク、認証、UI、その他の依存関係 miseへ移行済み SwiftLint、LicensePlist CocoaPodsのまま維持 広告メディエーションSDK本体とアダプター Podfile に書かれていたSDK本体とアダプター以外の21件のPod宣言は、すべてなくなりました。残りの依存関係は、提供元のリリースノートでSPM対応の有無を確認しながら追従していきます。 まとめ Tuistの .external(name:) で依存関係を宣言できるため、移行作業の多くは定義ファイルへの追加と Podfile からの削除という小さな差分の繰り返しになりました。種類ごとに分けて、独立してレビュー・マージできる単位で進めたことで、各PRも小さく保てました。同様の移行を控えている場合は、以下の手順で実施するとスムーズに移行が進められると思います。 前回の記事 を参考にXcodeプロジェクトの管理方式を決める。 依存関係ごとに、SPMへの対応状況を調べる。 種類ごとに分け、動作確認しやすい粒度で順番に移行する。 CocoaPodsに残る依存関係は、12月2日以降は新バージョンがTrunkに公開されなくなります。提供元のSPM対応状況を確認し、未対応のものはアダプターごと移行できる時期を見極める必要があります。 脚注 read-only化後も既存のPodは取得できるが、Trunkに新しいPodやバージョンは公開できなくなる。独自のspecリポジトリで配布されているPodは影響を受けない。 ↩︎ Firebase公式の移行ガイドによれば、CocoaPodsで最後に公開されるメジャーバージョンは12とされている。 ↩︎ CocoaPodsとSPMのバージョンを入れ替えて2通りで検証した。いずれも、ビルドしたバイナリに含まれていたのはSPM版で、新旧は関係なかった。どのパターンでも同じかまでは検証できていない。 ↩︎ 既定の探索先は、プロジェクトルートの Package.swift と、 *.xcodeproj および *.xcworkspace 配下の xcshareddata/swiftpm/Package.resolved である。詳細は LicensePlistのREADME を参照。 ↩︎
ネイティブライブラリの管理をCocoaPodsからSPM (Swift Package Manager) へ移行したけど、標準機能でライセンス情報の集約をやってくれないので結構不便。 mono0926/LicensePlist を使えばいいのだけれど、注意点として。"--package-sources-path"を指定しないとgithubからライセンス情報を取得しようとするものの、あっというまにAPI上限に達して403エラーでライセンス情報を取得できなくなる。このときもし"--fail-if-missing-license"を指定していない場合は、不完全な情報しか集計されず正常終了