Introducing swift-dependencies, a dependency injection library inspired by SwiftUI's "environment"

We're excited to announce Dependencies, which is a general-purpose dependency management library with an API inspired by SwiftUI's "environment" and powered by Swift's task local machinery.

From our Quick start guide:

The library allows you to register your own dependencies, but it also comes with many controllable dependencies out of the box (see DependencyValues for a full list), and there is a good chance you can immediately make use of one. If you are using Date(), UUID(), Task.sleep, or Combine schedulers directly in your feature’s logic, you can already start to use this library.

Any place you are using one of those dependencies directly in feature logic without passing it explicitly to the feature can be updated to first declare your dependency in the feature using the @Dependency property wrapper:

final class FeatureModel: ObservableObject {
  @Dependency(\.continuousClock) var clock  // Controllable async sleep
  @Dependency(\.date.now) var now           // Controllable current date
  @Dependency(\.mainQueue) var mainQueue    // Controllable main queue scheduling
  @Dependency(\.uuid) var uuid              // Controllable UUID creation

  // ...
}

Once your dependencies are declared, rather than reaching out to the Date(), UUID(), Task, > etc., directly, you can use the dependency that is defined on your feature’s model:

final class FeatureModel: ObservableObject {
  // ...

  func addButtonTapped() async throws {
    try await self.clock.sleep(for: .seconds(1))  // 👈 Don't use 'Task.sleep'
    self.items.append(
      Item(
        id: self.uuid(),  // 👈 Don't use 'UUID()'
        name: "",
        createdAt: self.now  // 👈 Don't use 'Date()'
      )
    )
  }
}

That is all it takes to start using controllable dependencies in your features. With that little bit of upfront work done you can start to take advantage of the library’s powers.

For example, you can easily control these dependencies in tests. If you want to test the logic inside the addButtonTapped method, you can use the withDependencies function to override any dependencies for the scope of one single test. It’s as easy as 1-2-3:

func testAdd() async throws {
  let model = withDependencies {
    // 1️⃣ Override any dependencies that your feature uses.
    $0.clock = ImmediateClock()
    $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()
  XCTAssertEqual(
    model.items,
    [
      Item(
        id: UUID(uuidString: "00000000-0000-0000-0000-000000000000")!,
        name: "",
        createdAt: Date(timeIntervalSinceReferenceDate: 1234567890)
      )
    ]
  )
}

The library can be used in SwiftUI, UIKit, server-side Swift, and anywhere, really.

To learn more, please check out the documentation and GitHub repo.

26 Likes