前回までのモジュール化シリーズでは、境界・依存方向・レイヤー構造まで、基本原則を整理しました。ここからはiOSプロジェクトに実際に適用します。
iOSでモジュールを分割するツールは、実質的に2つです。Xcodeのフレームワークターゲットと、Swift Package(SPM、Swift Package Manager)です。
結論から言えば、2026年時点で新しくモジュール化を始めるなら、SPMが基本の選択肢です。プロジェクトファイル(pbxproj)の衝突なしに、フォルダとPackage.swiftだけでモジュールを作れます。
この記事では、SPMでローカルモジュールを作る方法、フレームワークターゲットとの違い、static・dynamicリンキングの選択基準を整理します。
なぜフレームワークターゲットではなくSPMなのか?
XcodeでFile → New → Target → Frameworkを選んでモジュールを作ることもできます。長い間、これが標準でした。
問題は、この方法が残す変更です。ターゲットを1つ追加するたびにproject.pbxprojが数十行変わり、2人のチームメンバーが同時にターゲットを編集するとマージコンフリクトが起きます。
SPMのローカルパッケージは異なります。
| 比較 | フレームワークターゲット | SPMローカルパッケージ |
|---|---|---|
| モジュール定義の場所 | project.pbxproj | Package.swift(宣言型コード) |
| マージコンフリクト | 多い | 少ない(人が読めるコード) |
| モジュール追加のコスト | Xcode GUI操作 | フォルダ+数行の追加 |
| 細かなビルド設定 | 自由 | 制限あり |
モジュールが増えるほど、「モジュールを1つ追加するコスト」が構造を決めます。追加が怖ければ、誰も分割しなくなるからです。
ビルド設定を非常に細かく制御する必要がなければ、管理コストではローカルSPMパッケージが優位です。
ローカルSPMパッケージはどう作る?
アプリプロジェクトの隣にフォルダを1つ作り、Package.swiftを置けば完了です。前回の記事のレイヤー構造をそのまま移してみましょう。
// Modules/Package.swift
let package = Package(
name: "Modules",
products: [
.library(name: "FeatureSearch", targets: ["FeatureSearch"]),
.library(name: "CoreNetwork", targets: ["CoreNetwork"]),
],
targets: [
.target(name: "FeatureSearch", dependencies: ["CoreNetwork"]),
.target(name: "CoreNetwork"),
.testTarget(name: "FeatureSearchTests", dependencies: ["FeatureSearch"]),
]
)
このパッケージをXcodeプロジェクトにドラッグして追加し、アプリターゲットにライブラリを接続すると、import FeatureSearchが動作します。
重要なのは、依存関係がdependencies配列にコードで明示される点です。FeatureSearchがCoreNetworkを使わないなら配列から削除でき、勝手にimportするとコンパイルエラーになります。
前回の記事で説明した「コンパイラが境界を強制する状態」は、このように作られます。
上のように1つのパッケージに複数のターゲットを置く方法のほか、モジュールごとにパッケージを1つずつ置く方法もあります。モジュールが少ないうちは単一パッケージのマルチターゲットが管理しやすく、数十個規模になるとドメインごとにパッケージを分けるチームが多いです。
staticとdynamic、どちらを選ぶべき?
.libraryを宣言するときにリンキング方式を指定できます。デフォルトはXcodeが決めるautomaticで、ほとんどの場合staticとして処理されます。
- static:モジュールコードがアプリバイナリにコピーされて統合される。アプリ起動時の追加ロードなし
- dynamic:モジュールが独立したフレームワークファイルとして存在し、実行時にロードされる
選択基準は次のとおりです。
| 状況 | 選択 |
|---|---|
| 一般的な機能・コアモジュール | static(デフォルトを維持) |
| アプリ・ウィジェット・エクステンションが同じモジュールを共有 | dynamic(バイナリの重複を削減) |
| アプリ起動時間が重要なのにdynamicが数十個ある | staticへの統合を検討 |
dynamicフレームワークが増えると、アプリ起動時のロードコストが増加します。AppleがWWDCセッションで継続的に推奨してきた内容でもあるため、特別な理由がなければautomatic(実質static)を維持するのが無難です。
SPMだけでは足りなくなるのはいつ?
SPMによるモジュール化から始めてうまく使えていても、チームが大きくなると限界にぶつかります。
- アプリターゲット自体の設定(署名、スキーム、ビルド設定)は引き続きpbxprojに残り、衝突の余地がある
- モジュールが数十個になると、Package.swiftの管理とモジュールテンプレートの統一が面倒になる
- 組織単位でビルドキャッシュを共有したい
ここで登場するツールがTuistです。シリーズ最終回で扱うテーマです。
面接ではこう聞かれます
Q. iOSのモジュール化でSPMローカルパッケージを使う理由は何ですか?
モジュール定義をpbxprojではなく宣言型のPackage.swiftコードで管理するため、マージコンフリクトとモジュール追加のコストを減らせます。また、ターゲット間の依存関係がdependenciesに明示されるので、許可されていないimportはコンパイルエラーで遮断され、コンパイラがモジュール境界を強制します。
Q. staticライブラリとdynamicフレームワークの違い、および選択基準を説明してください。
staticはリンク時にアプリバイナリへコピーされるため、実行時のロードコストがありません。dynamicは独立したファイルとして存在し、実行時にロードされます。基本はstaticとし、アプリ・エクステンション・ウィジェットが同じコードを共有してバイナリの重複を減らす必要がある場合にdynamicを選びます。
次回は、多くのチームがモジュール化を始める本当のきっかけ、Xcodeのビルド速度を扱います。
ビルドが遅くなる理由、モジュール化でどこがどれだけ改善するのか、そして改善効果を数値で測る方法まで整理します。

![[モジュール化 #4] SPMで始めるiOSモジュール化(ローカルパッケージ・static・dynamic総まとめ)のカバー画像](/assets/images/posts/2dfa8b93-209b-47ab-8ad8-2745ec0ab314/1.jpg)