ソフトウェア設計

[モジュール化 #4] SPMで始めるiOSモジュール化(ローカルパッケージ・static・dynamic総まとめ)

前回までのモジュール化シリーズでは、境界・依存方向・レイヤー構造まで、基本原則を整理しました。ここからはiOSプロジェクトに実際に適用します。

読了 5 分
[モジュール化 #4] SPMで始めるiOSモジュール化(ローカルパッケージ・static・dynamic総まとめ)のカバー画像

前回までのモジュール化シリーズでは、境界・依存方向・レイヤー構造まで、基本原則を整理しました。ここからは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するとコンパイルエラーになります。

前回の記事で説明した「コンパイラが境界を強制する状態」は、このように作られます。

dependencies配列がそのまま依存グラフになります
dependencies配列がそのまま依存グラフになります

上のように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を選びます。

パッケージフォルダが1つずつ増えていくのは、地味に楽しいものです
パッケージフォルダが1つずつ増えていくのは、地味に楽しいものです

次回は、多くのチームがモジュール化を始める本当のきっかけ、Xcodeのビルド速度を扱います。

ビルドが遅くなる理由、モジュール化でどこがどれだけ改善するのか、そして改善効果を数値で測る方法まで整理します。