Blog

SwiftUI Dependency Injection in 20 Lines

01.05.2020

Swift has recently introduced property wrappers for variables. This makes dependency injection using only annotations possible the way we know and love from other languages, like Java.

How will I use it?

Here’s what it will look like in your code:

DependencyInjector.register(dependency: Foo("Hey there!") as FooService)

class Bar {
    @Inject var foo: FooService

    func useFoo() {
        foo.printMessage()
    }
}

Pretty neat, right? Using the framework is extremely simple:

  1. Register your dependency with the framework

  2. Annotate your variables with @Inject

And that’s it! So how do we do it?

Writing the framework

Writing the framework is almost as simple as using it. All we need is a struct to hold the dependencies, and a struct to define the @Inject property wrapper.

Here is the full code:

struct DependencyInjector {
    private static var dependencyList: [String:Any] = [:]

    static func resolve<T>() -> T {
        guard let t = dependencyList[String(describing: T.self)] as? T else {
            fatalError("No povider registered for type \(T.self)")
        }
        return t
    }

    static func register<T>(dependency: T) {
        dependencyList[String(describing: T.self)] = dependency
    }
}

@propertyWrapper struct Inject<T> {
    var wrappedValue: T

    init() {
        self.wrappedValue = DependencyInjector.resolve()
    }
}

Let’s see how this works:

The DependencyInjector struct has a dictionary which will store all of your dependencies. They keys are the class names and the values the instances. The register method uses its generic type information to generate a string key for the dictionary, and places the instance you provided into the dictionary.

The resolve method does the same but in reverse: It uses the type information to look up whether an instance exists in the dictionary, and returns this to you.

Note that if you are providing a Protocol, you must cast down first: If you wish to register a class Foo: FooService and later fetch a FooService , you must register Foo using register(dependency: foo as FooService) for the Injector to properly generate the type names.

Full usage example

So now that you know how it works, here is a full example of how you might use the framework to provide a service (FooService) with an instance Foo to a class Bar :

protocol FooService {
    func printMessage()
}

class Foo: FooService {
    private let message: String

    init(_ message: String) {
        self.message = message
    }

    func printMessage() {
        print(message)
    }
}

class Bar {
    @Inject var foo: FooService

    func useFoo() {
        foo.printMessage()
    }
}

DependencyInjector.register(dependency: Foo("Hey there!") as FooService)

let bar = Bar()

bar.useFoo() // Prints "Hey there!"

Bonus: Adding a provider property wrapper

Right now, we still need to register our dependencies using the dependency manager through DependencyInjector.register. What if we could also use a simple property wrapper for this? We can define a property wrapper as follows:

@propertyWrapper struct Provider<T> {
    var wrappedValue: T

    init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
        DependencyInjector.register(dependency: wrappedValue)
    }
}

Now, as long as we have a variable declared using the @Provider wrapper, we can simply use the variable again later using @Inject. Neat 🚀!

You can find the whole Swift playground here.

By Hannes Hertach