軟體設計

[模組化 #4] 使用 SPM 開始 iOS 模組化(本機套件、static、dynamic 完整整理)

在前面的模組化系列中,我們整理了從邊界、相依方向到分層結構的基本原則。現在該實際套用到 iOS 專案了。

閱讀 4 分鐘
[模組化 #4] 使用 SPM 開始 iOS 模組化(本機套件、static、dynamic 完整整理) 封面圖

在前面的模組化系列中,我們整理了從邊界、相依方向到分層結構的基本原則。現在該實際套用到 iOS 專案了。

在 iOS 中拆分模組,實際上有兩種工具:Xcode framework target,以及 Swift Package(SPM,Swift Package Manager)。

先說結論:以 2026 年來看,若要開始新的模組化,SPM 是預設選擇。只要使用資料夾和 Package.swift,就能建立模組,不會產生專案檔(pbxproj)衝突。

本文將整理如何使用 SPM 建立本機模組、它與 framework target 的差異,以及選擇 static 或 dynamic 連結方式的基準。


為什麼不是 framework target,而是 SPM?

也可以在 Xcode 中透過 File → New → Target → Framework 建立模組。長久以來,這一直是標準做法。

問題在於這種方式留下的痕跡。每新增一個 target,project.pbxproj 就會變更數十行;如果兩位團隊成員同時修改 target,就會發生合併衝突。

SPM 本機套件則不同。

比較 Framework target SPM 本機套件
模組定義位置 project.pbxproj Package.swift(宣告式程式碼)
合併衝突 頻繁 少見(檔案是人類可讀的程式碼)
新增模組的成本 操作 Xcode GUI 資料夾+新增幾行程式碼
細緻的建置設定 自由 有限

模組越多,「新增一個模組的成本」越會決定整體結構。因為新增模組若令人畏懼,就不會有人願意拆分。

除非需要非常細緻地控制建置設定,否則本機 SPM 套件在管理成本上更具優勢。


如何建立本機 SPM 套件?

在 App 專案旁建立一個資料夾,再放入 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 專案中,並將程式庫連結到 App target,接著就能使用 import FeatureSearch。

重點是,相依關係會以程式碼明確寫在 dependencies 陣列中。如果 FeatureSearch 不應使用 CoreNetwork,就從陣列移除;偷偷 import 時會產生編譯錯誤。

上一篇文章所說的「由編譯器強制邊界」,就是這樣建立的。

dependencies 陣列會直接成為相依圖
dependencies 陣列會直接成為相依圖

除了像上述方式在一個套件中放入多個 target,也可以為每個模組各放一個套件。模組少時,單一套件多 target 較易管理;達到數十個規模後,許多團隊會依網域拆分套件。


static 和 dynamic,該選哪一個?

宣告 .library 時可以指定連結方式。預設值是由 Xcode 自動決定的 automatic,而且大多會以 static 處理。

  • static:模組程式碼會複製並合併到 App 二進位檔中。App 啟動時不需額外載入
  • dynamic:模組以獨立的 framework 檔案存在,並在執行時載入

選擇基準如下。

情境 選擇
一般功能・核心模組 static(維持預設值)
App、Widget 與 Extension 共用相同模組 dynamic(消除二進位檔重複)
App 啟動時間敏感,但有數十個 dynamic 考慮整合為 static

dynamic framework 越多,App 啟動時的載入成本越高。這也是 Apple 在 WWDC session 中持續建議的做法,因此除非有特殊理由,維持 automatic(實際上是 static)通常最穩妥。


什麼時候只靠 SPM 會不夠?

從 SPM 模組化開始並順利使用後,團隊變大時仍會遇到一些限制。

  • App target 本身的設定(簽署、scheme、建置設定)仍留在 pbxproj 中,因此仍可能發生衝突
  • 模組達到數十個後,管理 Package.swift 與統一模組範本會變得麻煩
  • 希望在組織層級共用建置快取

這時會出現的工具就是 Tuist。這將是系列最後一篇的主題。

面試時會這樣問

Q. 在 iOS 模組化中,為什麼要使用 SPM 本機套件?

模組定義不是由 pbxproj 管理,而是以宣告式 Package.swift 程式碼管理,因此能減少合併衝突,也降低新增模組的成本。此外,target 間的相依關係會明確寫在 dependencies 中,未獲允許的 import 會以編譯錯誤阻擋,讓編譯器強制執行模組邊界。

Q. 請說明 static library 與 dynamic framework 的差異及選擇基準。

static 會在連結時複製到 App 二進位檔,因此執行時沒有載入成本;dynamic 則以獨立檔案存在,並在執行時載入。預設使用 static;當 App、Extension 與 Widget 共用相同程式碼、需要減少二進位檔重複時,選擇 dynamic。

看著套件資料夾一個個增加,多少有點樂趣
看著套件資料夾一個個增加,多少有點樂趣

下一篇將介紹許多團隊開始模組化的真正契機:Xcode 建置速度。

我們會整理建置變慢的原因、模組化能改善哪些地方及改善多少,以及如何用數字衡量改善效果。