【Xcode/Swift】今更だけど、SPMを復習してみる

はじめに

SPM使ってる人は多いけど、

  • 実はちゃんと理解してない (よくわからんけど、動いてるからヨシ!)
  • CocoaPodsとごっちゃになってる
  • Xcode任せで触ったことない

ってパターン、よくある。(というか私がそう)

今のSwiftUI/iOS開発では、SPMの理解度がコードの構造やビルド時間、保守性に直結する

最近は:

  • SwiftPM対応のライブラリが主流
  • Apple公式のライブラリも全部SPM(例:swift-algorithms)
  • ローカルモジュール分割やFeatureごとの再利用にもバッチリ使える

ってことで、改めてSPMを復習していこうという試み。

CocoaPodsやCarthageとの違い

全部「iOSアプリに外部ライブラリを導入するツール」だけど、中身の思想・仕組み・使い勝手がところどころ違う。

比較項目SwiftPM (SPM)CocoaPodsCarthage
開発元Apple公式OSS(community)OSS(community)
Xcode統合✅ 完全統合(Xcode 11+)◯(CLI & workspace必要)△(ビルド後に手動設定あり)
設定ファイルPackage.swiftPodfileCartfile
UI or CLIXcode 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.9Plugin機能、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指定)

  1. Xcode > File > Add Packages…
  2. GitHubなどのURLを入力(例: https://github.com/pointfreeco/swift-composable-architecture
  3. バージョンルールを選ぶ(後述)
  4. Add Package → 使いたいTargetを選んで追加完了

↓ このフローの裏で起きていること

  • Package.resolved にバージョン固定情報が書かれる
  • .xcodeproj.swiftpm ディレクトリにキャッシュが溜まる
  • Xcodeが勝手にビルドしてリンクまでやってくれる(神)

ローカルパッケージの使い方

ローカルパッケージ = GitHubとかじゃなく、同じプロジェクト内 or ローカルディレクトリにある Package を参照する方法

手順:

  1. File > Add Packages…
  2. 右下にある「Add Local…」を選ぶ
  3. Package.swift のあるフォルダを選択
  4. 依存先の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プロジェクトでは、ButtonViewRoundedImageView みたいな
再利用可能な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系のライブラリも簡単に導入できる。

手順:

  1. File > Add Packages…
  2. URL: https://github.com/onevcat/Kingfisher
  3. from: 7.0.0 とか指定してAdd
  4. 使用したい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/DerivedDatarm -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 moduleReset 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を完全に理解しら、もう一歩先へ。

  • 自作のViewModifierCustomComponentCommonUIとしてSPM化
  • GitHubにPackage.swift置くだけで公開ライブラリになる
  • README.mdにインストール手順書くだけで誰でも使える!