はじめに
SPM使ってる人は多いけど、
- 実はちゃんと理解してない (よくわからんけど、動いてるからヨシ!)
- CocoaPodsとごっちゃになってる
- Xcode任せで触ったことない
ってパターン、よくある。(というか私がそう)
今のSwiftUI/iOS開発では、SPMの理解度がコードの構造やビルド時間、保守性に直結する。
最近は:
- SwiftPM対応のライブラリが主流
- Apple公式のライブラリも全部SPM(例:swift-algorithms)
- ローカルモジュール分割やFeatureごとの再利用にもバッチリ使える
ってことで、改めてSPMを復習していこうという試み。
CocoaPodsやCarthageとの違い
全部「iOSアプリに外部ライブラリを導入するツール」だけど、中身の思想・仕組み・使い勝手がところどころ違う。
比較項目 | SwiftPM (SPM) | CocoaPods | Carthage |
---|---|---|---|
開発元 | Apple公式 | OSS(community) | OSS(community) |
Xcode統合 | ✅ 完全統合(Xcode 11+) | ◯(CLI & workspace必要) | △(ビルド後に手動設定あり) |
設定ファイル | Package.swift | Podfile | Cartfile |
UI or CLI | Xcode UI or CLIどちらもOK | 基本はCLI(Pod install) | 完全CLI |
モジュール分割 | ✅ 対応(Target構造) | ❌(基本はApp単位) | △(Framework構造なら可能) |
ローカルパッケージ | ✅ 標準対応 | ❌ | △ |
自動ビルド | ✅ | ✅ | ❌(ビルドはするがリンク手動) |
公式サポート状況 | ✅ Apple自ら利用&推奨 | ❌ 廃れつつある | ❌ メンテ停止気味 |
現在の主流 | ◎ SwiftPM一択 | △ 古いプロジェクトではまだあり | ✕ ほぼ使用されていない |
- 新規プロジェクトは100% SPMでOK (SPM対応してないライブラリをどうしても使いたい場合以外)
- CocoaPodsは古い資産 or Xcode外で開発したいとき限定
- Carthageは正直もう使う理由がないレベル
SwiftUI × SPMはベストマッチだから、「Feature単位で分けて再利用する構成」に進化させるとアーキテクチャが一段上がる。
SPMの基本構造
Package.swiftとは? 〜できた歴史と進化〜
Package.swift
= Swift Package Manager (SPM) の“心臓部”。
簡単に言えば「このパッケージの構成・依存・ビルド方法が全部書いてある設計書」。
時期 | 進化ポイント |
---|---|
Swift 3.0(2016) | SPM初登場(macOS, Linux専用) |
Swift 5.1(2019) | iOS/Xcode対応(Xcode 11)で一気に普及 |
Swift 5.3(2020) | Xcode UI統合、binary target追加 |
Swift 5.6〜5.9 | Plugin機能、TestTarget改善、よりモジュール構成しやすく進化 |
Apple自身もCocoaPods → SPMへ完全移行し始めたのがこの頃から。
Package.swift の中身
基本構成はこんな感じ
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "AwesomeKit",
platforms: [
.iOS(.v15)
],
products: [
.library(name: "AwesomeKit", targets: ["Core", "UI"])
],
dependencies: [
.package(url: "https://github.com/pointfreeco/composable-architecture", from: "1.0.0")
],
targets: [
.target(name: "Core"),
.target(name: "UI", dependencies: ["Core"]),
.testTarget(name: "CoreTests", dependencies: ["Core"])
]
)
Product / Target / Dependency の関係:
概念 | 説明 |
---|---|
Product | 他のプロジェクトが使える公開物(Library or Executable) |
Target | 実際のソースコード単位。Productの中身になる |
Dependency | 外部のSPMライブラリへの依存関係 |
よくあるターゲット構成パターン:
├─ Sources
│ ├─ Core ← ロジック層(モデル、API、UseCaseなど)
│ └─ UI ← SwiftUI View、Component
├─ Tests
│ └─ CoreTests ← ユニットテスト
こんなふうにTargetでFeatureや責務ごとに分割することで、アプリの再利用性&ビルド時間が爆上がりする。
BinaryTarget / Plugin(Swift 5.3+ / 5.6+)
BinaryTarget: ビルド済みの.xcframework
などをSPMで扱えるやつ。
.target(
name: "VisionWrapper",
dependencies: [],
path: "Sources/VisionWrapper",
linkerSettings: [
.linkedFramework("Vision")
]
)
// or
.binaryTarget(
name: "AwesomeBinary",
path: "./Frameworks/AwesomeBinary.xcframework"
)
XCFrameworkの配布とかで便利(Carthage完全不要)
Plugin(Swift 5.6〜5.9で進化中らしい?): SPMでコード生成・Lint・コマンド実行などができるようになる超注目機能。
// 例:SwiftFormatの自動実行Plugin
.plugin(
name: "SwiftFormatPlugin",
capability: .command(
intent: .sourceCodeFormatting(),
permissions: []
)
)
まだ実務では浸透してないけど、今後はFastlane的なことをSPM内で完結できるようになる可能性あり?
SPMの使い方(Xcodeベース)
依存ライブラリの追加方法(URL指定)
- Xcode > File > Add Packages…
- GitHubなどのURLを入力(例:
https://github.com/pointfreeco/swift-composable-architecture
) - バージョンルールを選ぶ(後述)
- Add Package → 使いたいTargetを選んで追加完了
↓ このフローの裏で起きていること
Package.resolved
にバージョン固定情報が書かれる.xcodeproj
や.swiftpm
ディレクトリにキャッシュが溜まる- Xcodeが勝手にビルドしてリンクまでやってくれる(神)
ローカルパッケージの使い方
ローカルパッケージ = GitHubとかじゃなく、同じプロジェクト内 or ローカルディレクトリにある Package を参照する方法
手順:
- File > Add Packages…
- 右下にある「Add Local…」を選ぶ
Package.swift
のあるフォルダを選択- 依存先のTargetに追加して完了!
Package.swift から手動で参照
.package(path: "../SharedModule")
この方式だと、複数プロジェクト間でローカル共有できて便利。モノレポ構成にも強い
バージョン指定のルール(from / exact / branch)
Xcode UI or Package.swift
の中で使えるバージョン指定方法は↓の通り
指定方法 | 書き方 | 意味 |
---|---|---|
from(推奨) | .package(url: "URL", from: "1.2.3") | 1.2.3 以上、2.0.0 未満を許容(セマンティックVer前提) |
exact | .package(url: "URL", exact: "1.2.3") | 完全一致のみ |
range | .package(url: "URL", "1.0.0" ..< "2.0.0") | この範囲内のみ許容 |
branch | .package(url: "URL", branch: "main") | 任意のブランチで取得(非推奨) |
revision | .package(url: "URL", revision: "commitHash") | 任意のGitコミットで固定(Pinningしたい時) |
from:
が一番安全でメンテしやすい(API互換性保証あり)branch:
は開発中のパッケージを試したい時だけにしとくexact:
を使いすぎると、パッケージ更新が地獄になるから注意
SwiftUIプロジェクトでのSPM活用例
CommonパッケージでView共有
SwiftUIプロジェクトでは、ButtonView
や RoundedImageView
みたいな
再利用可能なComponentを共有することがある。
これを、SPMでCommonモジュール化すると爆速で再利用できる。
// パッケージ構成例
MyApp/
├─ App/
│ └─ ContentView.swift
├─ CommonUI/ ← SPMパッケージ
│ └─ Sources/
│ └─ CommonUI/
│ ├─ RoundedImageView.swift
│ └─ CustomButton.swift
├─ Package.swift
// 使い方イメージ
import CommonUI
struct ProfileView: View {
var body: some View {
VStack {
RoundedImageView(imageName: "profile")
CustomButton(title: "ログイン", action: { ... })
}
}
}
サードパーティのUIライブラリ導入(例: Kingfisher)
SPM経由でUI系のライブラリも簡単に導入できる。
手順:
- File > Add Packages…
- URL:
https://github.com/onevcat/Kingfisher
from: 7.0.0
とか指定してAdd- 使用したいTargetに追加
使用例:
import Kingfisher
struct AvatarView: View {
var url: URL
var body: some View {
KFImage(url)
.resizable()
.scaledToFit()
.frame(width: 80, height: 80)
.clipShape(Circle())
}
}
モジュール分割との相性◎(MVVM+Feature毎)
SPMは「再利用」だけじゃなくて、大規模アプリのモジュール分割にも激強
// Feature単位の構成イメージ
MyApp/
├─ Features/
│ ├─ HomeFeature/ ← SwiftPMパッケージ
│ ├─ ProfileFeature/← SwiftPMパッケージ
│ └─ LoginFeature/ ← SwiftPMパッケージ
├─ Core/
│ ├─ APIClient/
│ └─ Model/
├─ App/
│ └─ AppEntry.swift
メリット | 解説 |
---|---|
✅ ビルド時間短縮 | 変更があったモジュールだけ再ビルドされる |
✅ コンパイル最適化 | モジュール境界が明確で依存が減る |
✅ 機能ごとの責務分離 | MVVM構成でも管理がラク |
✅ 他プロジェクト流用しやすい | Feature単位で他アプリに持っていける |
よくあるトラブル&Tips
SPM導入後ビルドが異常に重い問題
現象:
- 初回ビルドが5分以上かかる
- ライブラリ追加後、編集してないファイルも毎回ビルド
原因:
- SPMはターゲットごとの依存管理がシビア
App
ターゲットに全部入れてると、変更検知が効かなくなる.product()
を無差別に依存させてると全部ビルド対象に
対策:
- 不要なTargetへの依存を外す(Feature単位で整理)
- ローカルパッケージならpathを固定してバラバラに依存分離
Whole Module Optimization
有効化(Releaseビルド)- 必要なら 手動で
DerivedData
や.swiftpm
削除してキャッシュリセット
"No such module"
エラー対処法
現象:
- ビルドできてたのに突然No such module ‘XXXX’
- SwiftUI Previewだけクラッシュ
原因:
Package.resolved
と.xcodeproj
の依存ズレ- SPMのキャッシュ or Xcodeの謎挙動(ぶっちゃけこれが8割かも)
対策:
- 一度
File > Packages > Reset Package Caches
- それでも治らなければ、
rm -rf ~/Library/Developer/Xcode/DerivedData
、rm -rf .build/
を試す - 最終手段:
Add Package
から再追加 .product(name: "X", package: "Y")
のパッケージ名のスペル間違いでもエラー出る、Package.swift
側でtargets:
にちゃんと名前が一致してるか確認
キャッシュクリア・DerivedData掃除テク
基本のキャッシュ掃除:
rm -rf ~/Library/Developer/Xcode/DerivedData
パッケージキャッシュだけ削除(軽め)
xcodebuild -resolvePackageDependencies
xcodebuild -clonedSourcePackagesDirPath ""
完全リセットコンボ
rm -rf .build
rm -rf .swiftpm
rm -rf ~/Library/Developer/Xcode/DerivedData
特に**.build/と.swiftpm/**はXcodeが気づかない依存ズレの温床になる
トラブル | 回避策 |
---|---|
ビルド爆遅 | モジュール単位で依存分離せよ |
No such module | Reset Package Cache はXcodeメニューでやる |
SwiftUI Preview死ぬ | Preview target に依存追加されてるかチェック |
謎クラッシュ | DerivedData は定期的に爆破しとけ |
SPMを使った設計のベストプラクティス
ドメインごとにTarget分割する理由
❌ BADな構成:
App/
├─ APIClient.swift
├─ ViewModel.swift
├─ View.swift
├─ User.swift
└─ 全部1つのTargetにブチ込まれてる
💀 結果:
- 変更→フルビルド
- テストしづらい
- 依存の整理が地獄
- 再利用もできない
✅ ベスト構成:ドメイン分割+Target化:
MyApp/
├─ Features/
│ ├─ AuthFeature/ ← ログイン/認証周り
│ ├─ HomeFeature/ ← ホーム画面
├─ Core/
│ ├─ APIClient/
│ ├─ Models/
│ └─ Utilities/
効果 | 内容 |
---|---|
✅ ビルド高速化 | 変更箇所だけビルドされる |
✅ 再利用性 | 別アプリにも移せる |
✅ テストしやすさ | 独立してユニットテスト可能 |
✅ チーム開発分担 | モジュールごとに責任範囲を分けやすい |
Interfaceと実装の分離で依存逆転を実現
Core/
├─ APIClientInterface/ ← Protocol定義
├─ APIClientLive/ ← 実装(URLSessionベース)
→ ViewModelやUseCaseは Interfaceだけ参照
→ 実装はSPMでInjection or Swift Concurrencyで渡すだけ
- 実装を差し替えても他に影響ゼロ
- モックでのテストも容易
- 非同期ライブラリ(e.g., Combine / async/await)ごとの切り替えも可能
まとめ
🔹 SPMがiOS開発のデフォになった今
CocoaPods全盛期を知ってる場合、
SPMは構成がシンプル・高速・柔軟で未来を感じる。(多分)
かつApple公式だから、今後のiOSエコシステムでも間違いなく中心になる。
🔹 次は自作ライブラリ公開にもチャレンジ?
SPMを完全に理解しら、もう一歩先へ。
- 自作の
ViewModifier
やCustomComponent
をCommonUI
としてSPM化 - GitHubに
Package.swift
置くだけで公開ライブラリになる README.md
にインストール手順書くだけで誰でも使える!