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.

28 Likes

Just to say this is great! Exactly what the doctor ordered for a few of my projects. Thank you for sharing your work in this area.


When using the struct-full-of-functions style of dependencies encouraged by this project (and some of the Point-Free videos), Iā€™m interested to know whether others have come up with any solutions to the following minor ergonomic problems:

  • No argument labels on the functions (as far as I can see?)

For example, the following is a lot clearer with the labels. Am I wrong in thinking you canā€™t do this, or is there a better way? Or is it just the price of doing business with this dependency setup?

// Old:
protocol Translator {
    func translate(_ utterance: Utterance, into locale: Locale) async throws -> Utterance
}

// New:
struct Translator {
   var translate: (Utterance, Locale) async throws -> Utterance
}
  • Simple get properties have to become functions (as far as I can tell?)

For example on the below API I would have found it natural to have a plain property getter for user, but now itā€™s a function which is slightly more cumbersome at use sites. Is that just how it has to be here? I realised you can liberally add computed properties to smooth out the API, so perhaps the solution is to have a getUser: () -> User? method and a var user: User? { getUser() } property.

public struct Authentication {

    public var user: () -> User?
    public var updates: AnyPublisher<User?, Never>

    public var configure: () async throws -> Void

    public var logIn: (Email, Password) async throws -> Void
    public var signUp: (Email, Password) async throws -> Void

    public var logOut: () async throws -> Void
    public var resetPassword: (Email) async throws -> Void

    // MARK: - Helpers

    public var isSignedIn: Bool { user() != nil }
    public var isSignedOut: Bool { user() == nil }
    public var isAnonymous: Bool { user()?.isAnonymous ?? true }

}

None of this is rocket science but I donā€™t see this dependency-struct pattern around very much yet so Iā€™m curious to see how others are organising their code that uses Dependencies.

Thanks again!

Easiest way to get names back is to simply write functions which then call the closures. It's a bit redundant, and could lead to user confusion, but it's perfectly safe and users should naturally gravitate to the function versions due to the better readability. You can also document any closures you don't want directly called to push users towards the function versions. You could also limit the visibility of the closures directly, but that prevents easy setting for tests. I do wish Swift allowed you do private internal(set) or internal private(get) or other relevant combinations, but it's just not allowed at the moment.

For instance, one of my small projects abstracts the networking like this:

struct Networking {
    var handleCallbackURL: @Sendable (_ url: URL) -> Void
    var signInUsingTokens: @Sendable (_ tokens: LoginOutput.Tokens?) async -> Result<LoginOutput, SignInError>
    var signOut: @Sendable () -> Void

    var createExpense: @Sendable (_ output: NewExpenseFlowInput.Output, _ employeeID: String) async -> Result<UploadResponse, APIError>
    var downloadReceipt: @Sendable (_ receipt: Expense.DownloadableReceipt) async -> Result<Data, APIError>
    var fetchEmployee: @Sendable () async -> Result<RawEmployee, APIError>
    var fetchExpenses: @Sendable () async -> Result<IdentifiedArrayOf<Expense>, APIError>

    func handle(callbackURL url: URL) {
        handleCallbackURL(url)
    }

    func signIn(using tokens: LoginOutput.Tokens?) async -> Result<LoginOutput, SignInError> {
        await signInUsingTokens(tokens)
    }

    func createExpense(from output: NewExpenseFlowInput.Output, for employeeID: String) async -> Result<UploadResponse, APIError> {
        await createExpense(output, employeeID)
    }

    func download(_ receipt: Expense.DownloadableReceipt) async -> Result<Data, APIError> {
        await downloadReceipt(receipt)
    }
}
1 Like

Thatā€™s interesting. I donā€™t love the straight duplication but I like the general direction of separating the thing out into two interfaces ā€” itā€™s as if you have a public client interface, and a secondary 'units of implementation' interface. Refining that idea further, and based on the fact that I put basically everything in a library these days, you could use access levels to organise things more like this (example modified from a real recent project):

public struct Translator {
    
    // MARK: - API
    
    public enum Configuration {
        case local
        case remote
        case hybrid
    }
    
    public var configuration: Configuration {
        get { getConfiguration() }
        set { setConfiguration(newValue) }
    }
    
    public func reset() async {
        await performReset() 
    }
    
    public func translate(_ utterance: Utterance, into locale: Locale) async throws -> Utterance {
        try await performTranslate(utterance, locale) 
    }

    // MARK: - Implementation
    
    var performReset: () async -> Void
    var performTranslate: (Utterance, Locale) async throws -> Utterance
    
    var getConfiguration: () -> Configuration
    var setConfiguration: (Configuration) -> Void
    
}

Assuming youā€™re only wanting to mutate the implementation functions for unit tests within the same module where you can easily use @testable import to get at them, this hides the implementation functions from clients of the dependency and exposes the nice clean interface.

I actually donā€™t hate having separate getFoo and setFoo functions, as they are legitimately useful units of implementation to be able to override separately. But as in your example, for the other methods itā€™s not that nice having to come up with an alternate name just to avoid a collision.


In general I love this approach to dependencies but I feel the ergonomics arenā€™t fully baked. I wonder if macros could clean anything up here, or whether that would be putting a hat on a hat ā€” taking an approach thatā€™s already not yet widespread (at least as far as I know?) and too-eagerly adding more abstraction on top.

1 Like

I just tried the setup using some computed properties to wrap up struct functions and it works, but you need to declare the setters as nonmutating, because at the point of use the @Dependency will be a get-only property:

public var configuration: Configuration {
    get { getConfiguration() }
    nonmutating set { setConfiguration(newValue) }
}

The more you try to clean up the interface like this, you definitely end up adding a lot of code and move further and further away from the concept of "itā€™s just a little struct holding a couple of functions".


Another challenge Iā€™m working on is refactoring the following container to a struct-style dependency. Iā€™m not sure how to work around the generic get/set implementation. Iā€™m guessing the answer will end up being to ditch this approach entirely and put each setting in as its own dependency...

@MainActor public final class Settings: ObservableObject {

    public var locale: Locale {
        get { get(.locale) }
        set { set(.locale, to: newValue) }
    }

    public var contentFontSize: FontSize {
        get { get(.contentFontSize) }
        set { set(.contentFontSize, to: newValue) }
    }

    public var autoClearDuration: Duration {
        get { get(.autoClearDuration) }
        set { set(.autoClearDuration, to: newValue) }
    }
    
    // More...

    private func get<T:Codable>(_ setting: Setting) -> T { ... }
    private func set<T:Codable>(_ setting: Setting, to value: T) { ... }

}

On the occasions where Iā€™ve really, really wanted a generic function as a value, what Iā€™ve ended up with is a protocol and struct combo:

protocol ThingConsumer {
    func callAsFunction<T: Thing>(_ value: T)
}

struct ConcreteThingConsumer: ThingConsumer {
    func callAsFunction<T: Thing>(_ value: T) {
        ...
    }
}

In your case, Iā€™d be tempted to go with a single protocol, but using two pseudo-functions also works (and even gives you argument labels back):

protocol SettingsGetter {
    func callAsFunction<T:Codable>(_ setting: Setting) -> T
}

protocol SettingsSetter {
    func callAsFunction<T:Codable>(_ setting: Setting, to value: T)
}

struct Settings {
    var get: SettingsGetter
    var set: SettingsSetter

    public var locale: Locale {
        get { get(.locale) }
        nonmutating set { set(.locale, to: newValue) }
    }
}
2 Likes

Thatā€™s a neat trick! I think in this case you lose some of the appeal of the struct-dependency setup because you need to create a protocol conformer to override the behaviour rather than just assigning a closure. But itā€™s not totally off the table when I consider all the alternatives.

Perhaps thereā€™s some cool approaches to settings that Iā€™ve missed, but last I checked all the @AppStorage derivative packages seem to follow its lead of being very global and inflexible and not allowing you to easily wrap up and control your load and save mechanism (not to mention being heavily based on string keys and multiple definitions spread out all over the app). I could definitely just be behind the curve on this though!

My approach here is to not abstract the abstraction. That is, don't abstract the interface to UserDefaults, like the get and set you define. Simply abstract your properties and leave the concrete implementation to be tested directly. Since you probably want to do that anyway, you get the same coverage without the complexity of having to duplicate the entire interface. I do the same thing for my networking and other complex APIs.

1 Like

Iā€™m still struggling with exactly how to organise this settings system, but I think your thesis here is right on! It boils down (yet again...) to just KISS.

Iā€™ve just come up against a sharp edge in using swift-dependencies which others may find useful to know, if itā€™s not obvious (it wasnā€™t to me at first). Itā€™s not a direct issue with swift-dependencies, but something that happens naturally if youā€™re using the library and using something along the lines of Point-Freeā€™s 'tree of view models' setup for an app.

Given a dependency with a long-running publisher endpoint:

public struct Authentication {
    // [...]
    public var updates: AnyPublisher<User?, Never>
}

Naturally youā€™re going to subscribe to that in your view models to keep any user-related state current:

@MainActor public final class ProfileModel: ObservableObject {
    @Dependency(\.authentication) var authentication
    @Published var isSignedIn = false
    
    public init() {
        Task {
            for await user in authentication.updates.values {
                isSignedIn = user != nil
            }       
        }
    }

    deinit {
        print("Iā€™m called when the profile screen is dismissed, right?")
    }
}

In fact, the profile model never gets released when its parent model changes destination it because thereā€™s a retain cycle in the task here, which implicitly captures self through authentication and isSignedIn.

You can do one of two things. You can start adding capture groups to all your tasks:

Task { [weak self, authentication] in
    for await user in authentication.updates.values {
        self?.isSignedIn = user != nil
    }       
}

Personally I donā€™t like this because itā€™s so easy to miss the self?. on a property inside the task.

The other option which makes a lot more sense is to use structured concurrency by adding SwiftUIā€™s task {} modifier to take care of the lifetime of the task. You can then capture self as much as you like and it wonā€™t matter because thereā€™s a definite point at runtime where the task will be cancelled and all your self references go away safely before deinit.

// ProfileModel.swift
// [...]
func trackUserUpdates() async {
    for await user in authentication.updates.values {
        isSignedIn = user != nil
    }
}

// ProfileScreen.swift
// [...]
var body: some View {
    Text("Profile Screen")
        .task { await model.trackUserUpdates() }
}

This is probably super-obvious to many users, but it took me a while to notice the problem. The one thing Iā€™m not sure about is that not starting the task automatically in init means that during testing creating an instance of the model doesnā€™t immediately set up all the expected state (notably if the underlying update publisher is using a CurrentValueSubject or equivalent so that your model would usually get an immediate initial value). You have to start and stop that task manually rather than it just coming along for the ride as the model is inited and released. Maybe thatā€™s ok? Maybe having long-running task publishers in dependencies is an anti-pattern? But whatā€™s the alternative for external things that change like auth states and data stores? Iā€™d love to hear from anyone else working with this setup.

I think this issue has less to do with the actual method of dependency injection and more about ā€˜never endingā€™ signals.

In Swift there was a design choice to capture self implicitly in a Task. Assuming that most tasks are short lived. This is probably true for many cases, but causes surprises in the remaining cases. :blush:

That, coupled with a never ending signal gives a retain cycle.

But itā€™s actually also a retain cycle even if the signal ends. As long as the signal outlives ā€˜selfā€™, you have a retain cycle for a while.

Iā€™d love to see a solution to this pattern in Swift - that doesnā€™t require having to remember capturing self weakly inside the Task.

1 Like