Site icon TechHype.io

Swift Dependencies – A dependency management library inspired by SwiftUI’s “environment”

This library was motivated and designed over the course of many episodes on Point-Free, a video series exploring functional programming and the Swift language, hosted by Brandon Williams and Stephen Celis.

Overview

Dependencies are the types and functions in your application that need to interact with outside systems that you do not control. Classic examples of this are API clients that make network requests to servers, but also seemingly innocuous things such as UUID and Date initializers, file access, user defaults, and even clocks and timers, can all be thought of as dependencies.

You can get really far in application development without ever thinking about dependency management (or, as some like to call it, “dependency injection”), but eventually uncontrolled dependencies can cause many problems in your code base and development cycle:

For these reasons, and a lot more, it is highly encouraged for you to take control of your dependencies rather than letting them control you.

But, controlling a dependency is only the beginning. Once you have controlled your dependencies, you are faced with a whole set of new problems:

This library addresses all of the points above, and much, much more.

Quick start

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.

final class FeatureModel: ObservableObject {
  @Dependency(\.continuousClock) var clock  // Controllable way to sleep a task
  @Dependency(\.date.now) var now           // Controllable way to ask for current date
  @Dependency(\.mainQueue) var mainQueue    // Controllable scheduling on main queue
  @Dependency(\.uuid) var uuid              // Controllable UUID creation

  // ...
}

Once your dependencies are declared, rather than reaching out to the Date()UUID(), 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)
      )
    ]
  )
}

Here we controlled the date dependency to always return the same date, and we controlled the uuid dependency to return an auto-incrementing UUID every time it is invoked, and we even controlled the clock dependency using an ImmediateClock to squash all of time into a single instant. If we did not control these dependencies this test would be very difficult to write since there is no way to accurately predict what will be returned by Date() and UUID(), and we’d have to wait for real world time to pass, making the test slow.

But, controllable dependencies aren’t only useful for tests. They can also be used in Xcode previews. Suppose the feature above makes use of a clock to sleep for an amount of time before something happens in the view. If you don’t want to literally wait for time to pass in order to see how the view changes, you can override the clock dependency to be an “immediate” clock using the withDependencies helper:

struct Feature_Previews: PreviewProvider {
  static var previews: some View {
    FeatureView(
      model: withDependencies {
        $0.clock = ImmediateClock()
      } operation: {
        FeatureModel()
      }
    )
  }
}

This will make it so that the preview uses an immediate clock when run, but when running in a simulator or on device it will still use a live ContinuousClock. This makes it possible to override dependencies just for previews without affecting how your app will run in production.

That is the basics to getting started with using the library, but there is still a lot more you can do. You can learn more in depth about the library by exploring the documentation and articles:

Getting started

Essentials

Advanced

Miscellaneous

Examples

We rebuilt Apple’s Scrumdinger demo application using modern, best practices for SwiftUI development, including using this library to control dependencies on file system access, timers and speech recognition APIs. That demo can be found in our SwiftUINavigation library.

Documentation

The latest documentation for the Dependencies APIs is available here.

Installation

You can add Dependencies to an Xcode project by adding it to your project as a package.

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

If you want to use Dependencies in a SwiftPM project, it’s as simple as adding it to your Package.swift:

dependencies: [
  .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "0.1.0")
]

And then adding the product to any target that needs access to the library:

.product(name: "Dependencies", package: "swift-dependencies"),

Extensions

This library controls a number of dependencies out of the box, but is also open to extension. The following projects all build on top of Dependencies:

Alternatives

There are many other dependency injection libraries in the Swift community. Each has its own set of priorities and trade-offs that differ from Dependencies. Here are a few well-known examples:

Swift Dependencies on GitHub: https://github.com/pointfreeco/swift-dependencies
Platform: iOS
⭐️: 558
Exit mobile version