一个受 SwiftUI “环境” 启发的依赖管理库。
这个库的动机和设计来源于 Point-Free 上的许多剧集,这是一个探索函数式编程和 Swift 语言的视频系列,由 Brandon Williams 和 Stephen Celis 主持。
依赖项是应用程序中需要与您无法控制的外部系统交互的类型和函数。 典型的例子是向服务器发出网络请求的 API 客户端,但看似无害的东西,例如 UUID
和 Date
初始化器、文件访问、用户默认设置,甚至时钟和计时器,都可以被认为是依赖项。
在应用程序开发中,即使不考虑依赖项管理(或有些人喜欢称之为“依赖注入”),您也可以取得很大的进展,但最终不受控制的依赖项可能会在您的代码库和开发周期中引起许多问题
不受控制的依赖项使编写快速、确定性的测试变得困难,因为您容易受到外部世界变化无常的影响,例如文件系统、网络连接、互联网速度、服务器正常运行时间等等。
许多依赖项在 SwiftUI 预览中不能很好地工作,例如位置管理器和语音识别器,有些甚至在模拟器中都不能工作,例如运动管理器等等。 如果您使用这些框架,这将阻止您轻松迭代功能的设计。
与第三方、非 Apple 库(例如 Firebase、web socket 库、网络库等)交互的依赖项往往是重量级的,并且需要很长的编译时间。 这可能会减慢您的开发周期。
由于这些以及更多原因,强烈建议您控制您的依赖项,而不是让它们控制您。
但是,控制依赖项仅仅是开始。 一旦您控制了您的依赖项,您将面临一系列新的问题
如何以比在任何地方显式传递它们更符合人体工程学,但比拥有全局依赖项更安全的方式,在整个应用程序中传播依赖项?
如何仅为应用程序的某一部分覆盖依赖项? 这对于覆盖测试和 SwiftUI 预览的依赖项,以及特定的用户流程(例如 onboarding 体验)非常方便。
如何确保您覆盖了功能在测试中使用的所有依赖项? 测试模拟某些依赖项但将其他依赖项留作与外部世界交互是错误的。
这个库解决了上述所有问题,以及更多、更多的问题。
该库允许您注册自己的依赖项,但它也附带了许多可控制的开箱即用的依赖项(有关完整列表,请参阅 DependencyValues
),并且很有可能您可以立即使用其中一个。 如果您在功能逻辑中直接使用 Date()
、UUID()
、Task.sleep
或 Combine 调度器,您已经可以开始使用这个库。
@Observable
final class FeatureModel {
var items: [Item] = []
@ObservationIgnored
@Dependency(\.continuousClock) var clock // Controllable way to sleep a task
@ObservationIgnored
@Dependency(\.date.now) var now // Controllable way to ask for current date
@ObservationIgnored
@Dependency(\.mainQueue) var mainQueue // Controllable scheduling on main queue
@ObservationIgnored
@Dependency(\.uuid) var uuid // Controllable UUID creation
// ...
}
一旦声明了依赖项,您可以使用在功能模型上定义的依赖项,而不是直接访问 Date()
、UUID()
等。
@Observable
final class FeatureModel {
// ...
func addButtonTapped() async throws {
try await clock.sleep(for: .seconds(1)) // 👈 Don't use 'Task.sleep'
items.append(
Item(
id: uuid(), // 👈 Don't use 'UUID()'
name: "",
createdAt: now // 👈 Don't use 'Date()'
)
)
}
}
这就是开始在您的功能中使用可控制依赖项所需的全部内容。 完成了这一点前期工作后,您就可以开始利用该库的功能。
例如,您可以轻松地在测试中控制这些依赖项。 如果您想测试 addButtonTapped
方法内部的逻辑,您可以使用 withDependencies
函数来覆盖单个测试范围内的任何依赖项。 这就像 1-2-3 一样简单
@Test
func add() async throws {
let model = withDependencies {
// 1️⃣ Override any dependencies that your feature uses.
$0.clock = .immediate
$0.date.now = Date(timeIntervalSinceReferenceDate: 1234567890)
$0.uuid = .incrementing
} operation: {
// 2️⃣ Construct the feature's model
FeatureModel()
}
// 3️⃣ The model now executes in a controlled environment of dependencies,
// and so we can make assertions against its behavior.
try await model.addButtonTapped()
#expect(
model.items == [
Item(
id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
name: "",
createdAt: Date(timeIntervalSinceReferenceDate: 1234567890)
)
]
)
}
在这里,我们控制了 date
依赖项始终返回相同的日期,我们控制了 uuid
依赖项每次调用时都返回自动递增的 UUID,我们甚至使用 ImmediateClock
控制了 clock
依赖项,将所有时间压缩到一个瞬间。 如果我们不控制这些依赖项,这个测试将很难编写,因为无法准确预测 Date()
和 UUID()
将返回什么,并且我们必须等待真实世界的时间流逝,从而使测试变慢。
但是,可控制的依赖项不仅在测试中很有用。 它们也可以在 Xcode 预览中使用。 假设上面的功能使用时钟来休眠一段时间,然后在视图中发生某些事情。 如果您不想真正等待时间流逝才能查看视图如何变化,您可以覆盖时钟依赖项,使其成为使用 prepareDependencies
的“即时”时钟
#Preview {
let _ = prepareDependencies {
$0.continuousClock = .immediate
}
// All access of '@Dependency(\.continuousClock)' in this preview will
// use an immediate clock.
FeatureView(model: FeatureModel())
}
这将使预览在运行时使用即时时钟,但在模拟器或设备上运行时,它仍将使用实时 ContinuousClock
。 这使得仅为预览覆盖依赖项成为可能,而不会影响您的应用程序在生产环境中的运行方式。
这就是开始使用该库的基础知识,但您仍然可以做更多的事情。 您可以通过浏览 文档 和文章来更深入地了解该库
使用依赖项:了解如何使用在该库中注册的依赖项。
注册依赖项:了解如何在该库中注册您自己的依赖项,以便它们可以立即从代码库的任何部分获得。
实时、预览和测试依赖项:了解如何为实时应用程序、Xcode 预览甚至测试提供依赖项的不同实现。
测试:控制依赖项的主要原因之一是为了更容易进行测试。 了解一些使用该库编写更好测试的技巧和窍门。
设计依赖项:学习设计依赖项的技术,以便它们在注入功能和覆盖测试时最灵活。
覆盖依赖项:了解如何在运行时更改依赖项,以便应用程序的某些部分可以使用不同的依赖项。
依赖项生命周期:了解依赖项的生命周期、如何延长依赖项的生命周期以及依赖项如何继承。
单入口点系统:了解“单入口点”系统,以及为什么它们最适合这个依赖项库,尽管可以将该库与非单入口点系统一起使用。
我们使用现代、最佳的 SwiftUI 开发实践重建了 Apple 的 Scrumdinger 演示应用程序,包括使用这个库来控制文件系统访问、计时器和语音识别 API 的依赖项。 该演示可以在这里找到。
Dependencies API 的最新文档可在此处获得 here。
您可以通过将 Dependencies 添加到您的项目中作为包,将其添加到 Xcode 项目中。
如果您想在 SwiftPM 项目中使用 Dependencies,只需将其添加到您的 Package.swift
中即可
dependencies: [
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0")
]
然后将产品添加到任何需要访问该库的目标
.product(name: "Dependencies", package: "swift-dependencies"),
如果您想讨论这个库或对如何使用它来解决特定问题有疑问,您可以与 Point-Free 爱好者们在以下几个地方进行讨论
这个库开箱即用地控制了许多依赖项,但也开放扩展。 以下项目都建立在 Dependencies 之上
Swift 社区中还有许多其他依赖注入库。 每个库都有自己的一组优先级和权衡,这些优先级和权衡与 Dependencies 不同。 以下是一些著名的例子
本库在 MIT 许可证下发布。 有关详细信息,请参阅 LICENSE。