上一篇我们介绍了 SOLID(面向对象五大设计原则)的最后一个字母 DIP(依赖倒置原则)。理解这一原则后,下一个问题自然会出现:“那么,在实际应用中,这些依赖由谁来组装,又该怎么组装?”
项目较小时,通过初始化器直接传入依赖的手动 DI(Dependency Injection,依赖注入)就够用了。但当页面增加到几十个、依赖图变得更深时,组装代码本身也会开始成为负担。今天介绍适合在这个阶段使用的 Swift DI 库 Factory。本文以 2026 年 7 月的最新版本 3.3.1 为准。
手动 DI 能撑到什么程度?
遵循 DIP 的代码,大致是下面这种形态。
protocol NetworkProviding {
func fetch(_ url: URL) async throws -> Data
}
final class OrderViewModel {
private let network: NetworkProviding
init(network: NetworkProviding) {
self.network = network
}
}
代码依赖协议,具体实现从外部注入。到这里还不需要库。问题出在负责组装的一方。
// 某处的组装点(Composition Root)
let network = NetworkProvider()
let repository = OrderRepository(network: network)
let analytics = AnalyticsService(network: network)
let viewModel = OrderViewModel(repository: repository, analytics: analytics)
当依赖深入到三层、四层时,这种初始化代码会在每个页面中重复。一旦给中间层增加一个依赖,就必须修改所有经过它的初始化代码。这正是人们想逃到单例的时刻,但你应该已经知道单例会怎样阻碍测试。DI 容器正是用来接管组装问题的工具。
Factory 的不同之处:编译时安全
提到 Swift DI 库,Swinject 长期以来几乎一直被当作标准方案。Swinject 基于字符串或类型注册,并通过 resolve() 获取,因此遗漏注册时编译仍能通过,运行时才会爆出 nil。也就是说,必须运行应用才能发现错误。
Factory 颠覆了这一点。由于依赖被定义为 Container 的计算属性,引用不存在的依赖时根本无法编译。拼写错误和注册遗漏都会在构建阶段被发现。
此外,它还是一个可执行代码不到 1,000 行的轻量库,只用纯 Swift 运行,无需编译时代码生成脚本。不像 Needle 那样需要把额外工具接入构建流水线。
3 分钟掌握 Factory 基本用法
使用 SPM(Swift Package Manager)安装。包地址是 https://github.com/hmlongco/Factory,从 3.x 开始的导入方式是 FactoryKit。
通过在 Container 扩展中添加计算属性来注册依赖。
import FactoryKit
extension Container {
var networkService: Factory<NetworkProviding> {
self { NetworkProvider() }
}
var orderRepository: Factory<OrderRepositoryType> {
self { OrderRepository(network: self.networkService()) }
}
}
获取依赖有三种方式。
// 1. 属性包装器注入
final class OrderViewModel {
@Injected(\.orderRepository) private var repository
}
// 2. 直接调用
let repository = Container.shared.orderRepository()
// 3. 初始化器注入 — 只把组装交给容器
extension Container {
var orderViewModel: Factory<OrderViewModel> {
self { OrderViewModel(repository: self.orderRepository()) }
}
}
第 3 种方式值得关注。类本身保持完全不知道库存在的纯初始化器注入形式,只有组装代码交给容器负责。这与 DIP 篇中介绍的“具体实现的组装属于外部职责”这一原则完全一致。
在 SwiftUI 中,可以通过 @InjectedObservable 直接获取 Observable 视图模型。
struct OrderView: View {
@InjectedObservable(\.orderViewModel) var viewModel
}
作用域:通过声明定义实例生命周期
使用 DI 容器的另一个原因是管理实例生命周期。在 Factory 中,只需给注册表添加一个修饰符。
extension Container {
var networkService: Factory<NetworkProviding> {
self { NetworkProvider() }.singleton
}
var imageCache: Factory<ImageCaching> {
self { ImageCache() }.cached
}
}
- unique — 默认值。每次请求都会创建新实例。
- singleton — 整个应用共享一个实例。
- cached — 在重置缓存前返回同一个实例。
- shared — 只要有人持有强引用就会保持;没有人使用时释放。
与直接创建全局单例对象的区别在于,生命周期不再像 static let 一样散落在代码各处,而是集中声明在一个注册表中。之后想把 singleton 改成 cached 时,只需修改一个修饰符。
测试和预览才是它真正发挥价值的地方
引入 DI 最实用的理由最终还是测试。Factory 可以直接覆盖原有注册。
import FactoryTesting
@Suite(.container) // 隔离每个测试的容器
struct OrderViewModelTests {
@Test func loadsOrders() async {
Container.shared.orderRepository { MockOrderRepository() }
let viewModel = Container.shared.orderViewModel()
await viewModel.load()
#expect(viewModel.orders.count == 3)
}
}
在 Swift Testing 中,为 FactoryTesting target 添加 .container trait 后,容器状态就不会在测试之间混用。在 SwiftUI 预览中也可以用同样的方式注入 mock。
#Preview {
Container.shared.orderRepository { MockOrderRepository() }
return OrderView()
}
还有一种上下文修饰符,只在特定运行环境中自动替换实现。如果只想在 Debug 构建中使用 stub 分析,可以这样做。
container.analytics.onDebug { StubAnalyticsEngine() }
同样,你还可以在注册表中声明测试、预览和模拟器环境各自的覆盖实现。
Factory 3.x 的变化
下面为从 2.x 升级的用户整理主要变化。
- 导入名称变更 —
import Factory改为import FactoryKit。迁移的大部分工作就是完成这一替换。 - 完整支持 Swift 6 Strict Concurrency — 注册
@MainActor视图模型时,2.x 需要在闭包中重复写入@MainActor in,而 3.x 只需在工厂声明处添加注解即可。 - 仅支持 SPM — 已停止支持 CocoaPods。CocoaPods 项目必须停留在 Factory 2.5.3,或直接嵌入源代码。
- 支持 Swift Testing — 正式加入了测试隔离支持,包括上面提到的
.containertrait。
总结
DIP 告诉我们要依赖抽象而不是具体实现,而 Factory 则降低了在真实代码库中坚持这一方向的成本。编译时安全会将注册遗漏变成构建错误,作用域和 mock 替换也只需几行声明。
没必要重写已经通过 Swinject 正常运行的项目,但如果是新项目,可以把 Factory 作为默认选择。它轻量、引入成本低;如果后来不喜欢,也可以保留初始化器注入结构,只移除容器。
如果手动 DI 已经让你难以为继,组装代码开始拖后腿,不妨试试看。

