依赖项

一个受 SwiftUI “环境” 启发的依赖管理库。

CI Slack

了解更多

这个库的动机和设计来源于 Point-Free 上的许多剧集,这是一个探索函数式编程和 Swift 语言的视频系列,由 Brandon WilliamsStephen Celis 主持。

video poster image

概览

依赖项是应用程序中需要与您无法控制的外部系统交互的类型和函数。 典型的例子是向服务器发出网络请求的 API 客户端,但看似无害的东西,例如 UUIDDate 初始化器、文件访问、用户默认设置,甚至时钟和计时器,都可以被认为是依赖项。

在应用程序开发中,即使不考虑依赖项管理(或有些人喜欢称之为“依赖注入”),您也可以取得很大的进展,但最终不受控制的依赖项可能会在您的代码库和开发周期中引起许多问题

由于这些以及更多原因,强烈建议您控制您的依赖项,而不是让它们控制您。

但是,控制依赖项仅仅是开始。 一旦您控制了您的依赖项,您将面临一系列新的问题

这个库解决了上述所有问题,以及更多、更多的问题。

快速开始

该库允许您注册自己的依赖项,但它也附带了许多可控制的开箱即用的依赖项(有关完整列表,请参阅 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。 这使得仅为预览覆盖依赖项成为可能,而不会影响您的应用程序在生产环境中的运行方式。

这就是开始使用该库的基础知识,但您仍然可以做更多的事情。 您可以通过浏览 文档 和文章来更深入地了解该库

开始使用

要点

高级

示例

我们使用现代、最佳的 SwiftUI 开发实践重建了 Apple 的 Scrumdinger 演示应用程序,包括使用这个库来控制文件系统访问、计时器和语音识别 API 的依赖项。 该演示可以在这里找到。

文档

Dependencies API 的最新文档可在此处获得 here

安装

您可以通过将 Dependencies 添加到您的项目中作为包,将其添加到 Xcode 项目中。

https://github.com/pointfreeco/swift-dependencies

如果您想在 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