用 Swift Package 拆分 SwiftUI 功能,而不是拆散领域逻辑
把每个 SwiftUI 页面放进一个 Swift Package 并不会自动得到模块化。真正的模块边界应围绕领域能力和变化频率:账户、项目、支付或搜索各自拥有模型、用例与界面适配;应用 target 负责组装。若拆分后所有 package 都依赖 Shared,互相导入 view model,并通过通知传递核心事件,目录增多了,耦合却更难看见。
先画依赖方向
一个实用起点是三层:底部为小而稳定的领域类型与协议,中间为功能 package,顶部为应用组合。功能可以依赖领域合同和必要平台封装,不能反向依赖应用 target,也不应直接导入另一个功能的内部视图。
AppComposition
├─ SearchFeature ─┐
├─ LibraryFeature ├─ DomainContracts
└─ AccountFeature ┘
│
PlatformServices
这不是要求每个方框一个独立仓库。一个本地 package 可以含多个 target;关键是 manifest 明确依赖,编译器阻止越界。若两个功能必须彼此调用,先提取它们共同依赖的领域命令或由应用协调,而不是互相添加 target dependency。
功能入口应很小
Feature 对外可以提供一个构造视图的方法或根 View,以及它所需的依赖协议。路由枚举、内部模型、子页面和具体 service 默认保持 internal。公开符号越少,重构空间越大,增量构建也更容易受益。
public struct SearchFeatureView: View {
private let client: any SearchClient
private let onOpenResult: (SearchResult.ID) -> Void
public init(
client: any SearchClient,
onOpenResult: @escaping (SearchResult.ID) -> Void
) {
self.client = client
self.onOpenResult = onOpenResult
}
public var body: some View {
SearchScreen(model: SearchModel(client: client), onOpen: onOpenResult)
}
}
闭包把导航意图交给组合层,而不是让 SearchFeature 导入 LibraryFeature 的页面。大型流程可以使用定义在领域层的 typed destination,但它仍应表达业务去向而非 UIKit/SwiftUI 具体类型。
Shared 只能包含真正稳定的共同语言
Shared, Core, Common 很容易变成任何人都能放东西的抽屉。只有跨功能语义相同、变化原因相同的类型才共享,例如不可变标识或经过定义的认证合同。一个看似相同的“Status”在订单和同步领域含义不同,不应为了复用 enum 名字合并。
设计 token 与通用控件可位于独立 UI package,但它不能导入业务模型。按钮接收 label、state 和 action,而不是整份 Order。扩展也要放在拥有语义的一侧;给所有 View 添加依赖业务状态的全局 extension 会重新制造隐藏耦合。
依赖通过组合根注入
应用入口创建真实网络、数据库和分析服务,再将满足的小协议传给功能。测试使用内存实现或 fake。避免 service locator 和全局 singleton,因为它们让 package manifest 看起来独立,运行时却共享隐形状态。协议应由需要它的功能定义,而不是由一个巨大基础设施模块预先声明所有可能方法。
Swift 6.2 并发边界也属于合同。跨 target 的值应明确 Sendable 与 actor 隔离;不要用 @unchecked Sendable 让旧 singleton 穿过所有模块。若一个数据库 actor 由组合根所有,功能只看到它需要的异步接口。
用构建和测试验证模块价值
每个功能 target 有自己的逻辑测试和少量预览/组件 fixture。应用 target 保留跨功能导航与启动测试。CI 可以并行测试 package,但仍需完整构建发现资源、签名与组合问题。观察增量构建时间;若拆包显著增加泛型边界或生成大量 public 符号,模块化也可能带来成本。
迁移采用垂直切片:选一个依赖较少的功能,定义入口、移动代码、删除旧路径,再继续下一块。不要先创建十个空 package 后长期维持新旧两套结构。每一步都应保持可构建、可发布。
结论
Swift Package 是执行边界的工具,不是架构本身。按领域能力拆分、保持依赖单向、把导航交给组合层、限制 public API,并让 shared 只承载真正共同语言。模块的价值体现在某个功能能独立理解、测试和替换,而不是项目浏览器里出现了更多文件夹。