Memoji of Jacob giving a thumbs up

Dependency Injection

For a long time, I had no idea what dependency injection was or why it mattered. When I realized I already knew one flavor of it, Constructor Injection, it suddenly became obvious to me. In this article, I hope to provide a clear explanation of Dependency Injection and how you can use it in Swift.

So what is Dependency Injection?

Dependency injection is the concept of providing things to an object that it relies on, instead of letting the object create the things it needs, itself. Often this is done in a constructor.

No Injection


To illustrate, here's a quick example of an object not using dependency injection:

struct SomeClient {
    private let someService = SomeService()

    func doSomeWork() {
        someService.doSomething()
    }
}

Notice how SomeClient needs (or is "dependent" on) SomeService to do its job? SomeService is a dependency of SomeClient. Dependency injection takes the responsibility of creating a SomeService out of SomeClient. Instead, a dependency is first created and then given to the client.

Constructor Injection


It's super simple, you probably already know how to do it, even if you're not familiar with the terms.

This is what Constructor Injection looks like:

struct SomeClient {
    private let someService: SomeService

    // notice how the dependency is required in the constructor below?
    init(_ service: SomeService) {
        self.someService = service
    }

    func doSomeWork() {
        someService.doSomething()
    }
}

It's a subtle difference, but it's profound. By removing the creation of the service from the client we achieve a couple of important things. First, it becomes easier to make the client more reusable. We could pass it different versions of SomeService, and the client could do different kinds of work! And because we can pass in different versions of the dependency, testing that structure becomes far easier.

It's a subtle difference, but it's profound.

Setter Injection


If you know even a little about Object Oriented programming, you already know how to construct an object and pass in the dependencies. That is super intuitive and commonplace. Then there is Setter Injection which you've probably done before too. Here's a quick example from my UIKit days:

// This example also demonstrates Interface Injection 
class SomeViewController: UIViewController, UITableViewDelegate {
    let tableView = UITableView()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.tableView.delegate = self // Setting the dependency
    }
}

The UITableView relies on a delegate to handle specific tasks and it exposes a setter for us to inject that dependency after the table view was initialized. Again, this was another common sense one for me. It was what dependency injection evolved into that was confusing to me at first. Let's cover that next.

Auto-Wired Dependency Injection

Eventually, the people using Dependency Injection created a new pattern to inject dependencies. One that is similar to the injection patterns we've just discussed, only it happens automagically. The concept is that an application creates a manifest of dependencies inside of a dependency injection framework. Then an object in that applications says it needs one such dependency and the framework provides it.

It would look something like this:

// pseudo code

let provider = DependencyProvider([
    SomeClient(),
    SomeService()
])

let controller = Controller(client: provider.resolveDependency())

// or

struct MyView {
    let service: SomeService = provider.resolveDependency()
}

This auto wiring confused me at first. It's really not much different than what we talked about before, the only true difference is that the dependency provider knows which dependency to provide based on the interface required at the usage site. There are third party frameworks that do this generically for many languages. However, Apple recently released a fully native way to do it.

Swift's Environment Variables

If you've done much with SwiftUI, you've probably seen an environment variable before. The one I use the most is probably the colorScheme:

struct SomeView: View {

    @Environment(\.colorScheme) var colorScheme : ColorScheme

    var body: some view {
        Text("")
            .foregroundColor(colorScheme == .dark ? .white : .black)
    }
}

The line @Environment(\.colorScheme) var colorScheme: ColorScheme is one way to do the autowired dependency injection. Let's break it down.

On the left we use an annotation (or property wrapper) @Environment This particular property wrapper takes a key path. This particular key path was \.colorScheme. That tells Swift's Environment framework that we're looking for an object with that name, please provide it. The next section var colorScheme is the name assignment, which could be any name at all. The last section : ColorScheme is the type assignment, which is actually erroneous. We could simply write: @Environment(\.colorScheme) var colorScheme.

Now to override the default and inject our own value, we can do that like so:

struct ContentView: View {
    
    var body: some View {
        SomeView()
            .environment(\.colorScheme, .dark) // injecting Dark to Some View
    }

}

Creating our own Environment Variable

We can create our own environment variables that we can place into Swift's Environment. To start, let's create a class to hold an access token.

class TokenStore: ObservableObject {
    var accessToken: String? = nil
}

Then, to access it from the @Environment property wrapper, it needs to have an Environment Key associated with it.

struct TokenStoreKey: EnvironmentKey {
    static let defaultValue = TokenStore()
}

This key is used to make the key path on the environment. We can do that by extending Swift's EnvironmentValues:

extension EnvironmentValues {
    var tokenStore: TokenStore {
        get { self[TokenStoreKey.self] }
        set { self[TokenStoreKey.self] = newValue }
    }
}

Now that we've done all that, we can use the token store like:

class SomeHttpClient {
    @Environment(\.tokenStore) var store

    func setToken(_ token: String) {
        store.accessToken = token
    }

    func getToken() -> String? {
        store.accessToken
    }
}

🔥☝️

That's one way to use Swift's native Dependency Injection tooling. There are other options as well. I hope this has been helpful!

Tags: