在前面的模块化系列中,我们梳理了边界、依赖方向和分层结构等基本原理。现在该把它们应用到实际的 iOS 项目中了。
在 iOS 中拆分模块,实际上有两种工具:Xcode framework target,以及 Swift Package(SPM,Swift Package Manager)。
先说结论:以 2026 年为准,重新开始模块化时,SPM 是默认选择。只需使用文件夹和 Package.swift,就能创建模块,不会产生项目文件(pbxproj)冲突。
本文将介绍如何使用 SPM 创建本地模块、它与 framework target 的区别,以及选择 static 或 dynamic 链接方式的标准。
为什么选择 SPM,而不是 framework target?
也可以在 Xcode 中通过 File → New → Target → Framework 创建模块。很长一段时间里,这都是标准做法。
问题在于这种方式留下的痕迹。每添加一个 target,project.pbxproj 就会改动几十行;如果两名团队成员同时操作 target,就会出现合并冲突。
SPM 本地包则不同。
| 对比 | Framework target | SPM 本地包 |
|---|---|---|
| 模块定义位置 | project.pbxproj | Package.swift(声明式代码) |
| 合并冲突 | 频繁 | 少见(文件是人类可读的代码) |
| 添加模块的成本 | 操作 Xcode GUI | 文件夹 + 添加几行代码 |
| 精细的构建设置 | 自由 | 有限 |
模块越多,“添加一个模块的成本”越会决定整体结构。添加模块如果令人畏惧,就不会有人愿意拆分。
除非必须非常精细地控制构建设置,否则本地 SPM 包在维护成本方面更有优势。
如何创建本地 SPM 包?
在应用项目旁创建一个文件夹,再放入 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 项目,并把库连接到应用 target,之后 import FeatureSearch 就能正常工作。
关键在于,依赖关系会以代码形式明确写在 dependencies 数组中。如果 FeatureSearch 不应使用 CoreNetwork,就从数组中删除;偷偷 import 会产生编译错误。
上一篇文章所说的“由编译器强制边界”,就是这样建立的。
除了像上面这样在一个包中放置多个 target,也可以为每个模块分别使用一个包。模块较少时,单包多 target 更易管理;达到几十个规模后,许多团队会按领域拆分包。
static 和 dynamic,该选哪个?
声明 .library 时可以指定链接方式。默认值是由 Xcode 自动决定的 automatic,而且大多数情况下会按 static 处理。
- static:模块代码会复制并合并到应用二进制文件中。应用启动时无需额外加载
- dynamic:模块以独立的 framework 文件存在,并在运行时加载
选择标准如下。
| 情况 | 选择 |
|---|---|
| 一般功能或核心模块 | static(保持默认值) |
| 应用、组件和扩展共享同一个模块 | dynamic(消除二进制重复) |
| 应用启动时间敏感,但有几十个 dynamic 模块 | 考虑整合为 static |
dynamic framework 越多,应用启动时的加载成本越高。Apple 在 WWDC session 中一直建议这样做,因此除非有特殊理由,保持 automatic(实际上是 static)通常更稳妥。
什么时候只用 SPM 会不够?
从 SPM 模块化开始并顺利使用后,团队变大时仍会遇到一些限制。
- 应用 target 本身的设置(签名、scheme 和构建设置)仍保留在 pbxproj 中,因此仍可能发生冲突
- 模块达到几十个后,管理 Package.swift 和统一模块模板会变得麻烦
- 希望在组织层面共享构建缓存
这时会出现的工具就是 Tuist。这将是系列最后一篇的主题。
面试时会这样问
Q. iOS 模块化中为什么要使用 SPM 本地包?
模块定义由声明式的 Package.swift 代码而不是 pbxproj 管理,因此可以减少合并冲突并降低添加模块的成本。此外,target 之间的依赖关系会明确写在 dependencies 中,未获允许的 import 会被编译错误拦截,从而由编译器强制执行模块边界。
Q. 请说明 static 库和 dynamic framework 的区别,以及选择标准。
static 会在链接时复制到应用二进制文件中,因此运行时没有加载成本;dynamic 作为独立文件存在,并在运行时加载。默认使用 static;当应用、扩展和组件共享代码、需要减少二进制重复时,选择 dynamic。
下一篇将介绍许多团队开始模块化的真正契机:Xcode 构建速度。
我们将整理构建变慢的原因、模块化能改善哪些地方以及改善多少,还会介绍如何用数字衡量改善效果。

![[模块化 #4] 使用 SPM 开始 iOS 模块化(本地包、static、dynamic 全面总结) 封面图](/assets/images/posts/2dfa8b93-209b-47ab-8ad8-2745ec0ab314/1.jpg)