SwiftUI Dependency Injection in 20 Lines
01.05.2020Swift 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:
Register your dependency with the framework
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 🚀!