Update: Xcode 11.2 and Swift 5.1.1 produced a bug that causes Property Wrappers to crash 💥 https://bugs.swift.org/browse/SR-11564

There are so many diverse use cases for Property Wrappers, but dependency injection in particular seems like one of those natural fits. In this post, we’ll explore how we can leverage this newly exposed feature of the language to achieve native dependency injection in Swift.

The End Game

Although the implementation of the dependency injection is important, how its consumed is just as important. We want to use Property Wrappers for the consumption of dependencies and end up with something like this:

class ViewController: UIViewController { @Inject private var widgetService: WidgetServiceType @Inject private var sampleService: SampleServiceType override func viewDidLoad() { super.viewDidLoad() print(widgetService.test()) print(sampleService.test()) } }

The properties WidgetServiceType and SampleServiceType are protocols. Notice they are not being initialized with their concrete types. Instead, they are resolved elsewhere and later provided by the @Inject property wrapper:

@propertyWrapper public struct Inject { public var wrappedValue: Value { Dependencies.root.resolve() } public init() {} }

We’re using a bit of generics to allow any type to fill the wrappedValue requirement of the @propertyWrapper . The generic Value type is resolved from the dependencies root using type inference, but where does Dependencies.root come from?

I’ll get to that shortly, but first let’s take a minute and appreciate what property wrapper just gave us… It doesn’t matter what dependency injection implementation or library we use, it hides behind @Inject and we can swap out the dependency injection implementation beneath without ever changing the rest of the app. That in itself is dependency injection! 🤯

Building the Dependency Container

In our example above, the consumers are not responsible for creating concrete instances. They come from a central location of dependencies known as our dependency container or composition root. Just like stars are born from a nebula, our dependencies are born from this composition root – not from anywhere else. This way, we can change concrete types within the composition root to update dependencies globally, without having to touch the calling code.

In the most simplest form, our dependency container holds a dictionary of closures to create instances later. A stripped-down implementation would look something like this:

class Dependencies { private var factories = [String: () -> Any]() func add (_ factory: @escaping () -> T) { let key = String(describing: T.self) factories[key] = factory } func resolve () -> T { let key = String(describing: T.self) guard let component: T = factories[key]?() as? T else { fatalError("Dependency '\(T.self)' not resolved!") } return component } }

The add function accepts a closure and stores it in a dictionary using the name of the returning type as the key. The resolve function allows the dependency to be retrieved from the dictionary using the name of the inferred type. That’s it.

Before using resolve , the closure for the dependencies needs to be added to the root container:

Dependencies.root.add({ WidgetService() as WidgetServiceType }) Dependencies.root.add({ SampleService() as SampleServiceType })

Now anywhere WidgetServiceType or SampleServiceType is requested from the dependency container, it will come through the resolve function and execute the closure (which is what the @Inject property wrapper is using).

Adding dependencies should happen early on in the app lifecycle, before any of the dependencies are used. This can be done in the application initializer:

@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { override init() { super.init() Dependencies.root.add({ WidgetService() as WidgetServiceType }) Dependencies.root.add({ SampleService() as SampleServiceType }) // *Forgot to add AnotherServiceType } } // Some time later... struct SomeType { @Inject var widgetService: WidgetServiceType @Inject var sampleService: SampleServiceType @Inject var anotherService: AnotherServiceType func test() { print(widgetService.test()) print(sampleService.test()) print(anotherService.test()) // Crash 💥 } }

Notice I did not add AnotherServiceType to the dependency container and therefore it crashed when it tried to use the property. The @Inject property wrapper didn’t complain about it because its generics allows for all types to fulfill the property, but the resolve function didn’t find the type in the container’s dictionary.

The catch-all generic is a blessing and a curse since although it gives us the convenience of using @Inject for any property, we loose the power of type-safety. Is there a better way to leverage the compiler against unregistered dependencies without creating a property wrapper for each type, i.e. @InjectWidgetService , @InjectSampleService , @InjectAnotherService , etc? That would not be practical to do and obviously become unmanageable, so we can introduce a little bit of convention to be safer and cleaner.

Modular DI == Modular Architecture

Instead of allowing any type to be added to the dependency container, let’s reserve property wrapper injection for only “Modules“. For all other components, dependencies are injected through their initializer, which is the best form of dependency injection. See how WidgetStore and WidgetRemote is being injected through the initializer:

struct WidgetService: WidgetServiceType { private let store: WidgetStore private let remote: WidgetRemote init(store: WidgetStore, remote: WidgetRemote) { self.store = store self.remote = remote } func test() -> String { store.test() + remote.test() } } protocol WidgetServiceType { func test() -> String } protocol WidgetStore { func test() -> String } protocol WidgetRemote { func test() -> String }

The purpose of the module from here is to resolve the above components:

struct WidgetModule: WidgetModuleType { func component() -> WidgetWorkerType { WidgetWorker( store: component(), remote: component() ) } func component() -> WidgetRemote { WidgetNetworkRemote(httpService: component()) } func component() -> WidgetStore { WidgetRealmStore() } func component() -> HTTPServiceType { HTTPService() } } protocol WidgetModuleType { func component() -> WidgetWorkerType func component() -> WidgetRemote func component() -> WidgetStore func component() -> HTTPServiceType }

The component() functions return concrete types. Even when some components need other components, it travels through the dependency graph recursively to resolve the dependency it needs. The module does this in a type-safe manner as well.

Instead of adding WidgetWorkerType , WidgetRemote , WidgetStore , HTTPServiceType to the dependency container, we only add the module to the container in app initializer:

@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { override init() { super.init() Dependencies.root.add({ WidgetModule() as WidgetModuleType }) } }

Then the module is injected to the consumers and resolves components for it from there:

struct SomeType { @Inject private var widgetModule: WidgetModuleType private lazy var widgetService: WidgetServiceType = widgetModule.component() func test() { print(widgetService.test()) } }

You may still be wondering why this extra layer for the modules instead of injecting all components from the property wrapper. Remember our app can have 50+ components we use across the code, i.e. DataService , WidgeService , MigrationUtility , etc. It’s easy to loose track of what we added to the dependency container and can result in a runtime exception if one is missed. By using the concept of “modules” though, we’ve effectively divided our architecture into logical parts:

Composition root stores modules; modules resolve components.

Now we only have to worry about adding a half-dozen modules instead of possibly hundreds of components. Each module can be seen as a feature of the application. A feature needs several components to run, but can only ask the modules for the concrete instances. This forces our features to stay within the scope of the module dependencies. And since modules are vital to the application as a whole, runtime exceptions with modules are much more obvious in development and should crash when entering an entire section of the app, instead of a small slice deep inside.

Furthermore, modules can be injected into other modules using property wrappers as well. Nesting modules makes sense since some components are used across several modules:

struct AppModule: AppModuleType { func component() -> HTTPServiceType { HTTPService() } } struct WidgetModule: WidgetModuleType { @Inject private var module: AppModuleType func component() -> WidgetWorkerType { WidgetWorker( store: component(), remote: component() ) } func component() -> WidgetRemote { WidgetNetworkRemote( httpService: module.component() // Parent module ) } func component() -> WidgetStore { WidgetRealmStore() } }

More Sugar, Please!

To get rid of the Dependencies.root singleton from the public API, we can use the new @functionBuilder feature from Swift 5.1 to make the initializer a little more Swifty, and add a build() function to update the composition root instance for a little more clarity with the intent:

class AppDelegate: UIResponder, UIApplicationDelegate { private let dependencies = Dependencies { Module { WidgetModule() as WidgetModuleType } Module { SampleModule() as SampleModuleType } } override init() { super.init() dependencies.build() } }

The Dependencies type now has an initializer with a @functionBuilder to add an array of modules. Then you call .build() to move that container instance to the global space for our property wrapper to pick up.

The public API of the dependency container has been updated to this:

public extension Dependencies { /// Composition root container of dependencies. fileprivate static var root = Dependencies() /// Construct dependency resolutions. convenience init(@ModuleBuilder _ modules: () -> [Module]) { self.init() modules().forEach { add(module: $0) } } /// Assigns the current container to the composition root. func build() { // Used later in property wrapper Self.root = self } /// DSL for declaring modules within the container dependency initializer. @_functionBuilder struct ModuleBuilder { public static func buildBlock(_ modules: Module...) -> [Module] { modules } } }

The μ-Library!

Dependency injection is actually a simple but vital concept. With all the features discussed here, we can create a library in under 100 lines of code to achieve what we need. There’s no file generation step like other DI libraries, but still achieves some level of type-safety by letting the modules create the components.

Below is the full implementation including the dependency container, property wrapper, function builder, and even comments:

/// A dependency collection that provides resolutions for object instances. public class Dependencies { /// Stored object instance factories. private var modules: [String: Module] = [:] private init() {} deinit { modules.removeAll() } } private extension Dependencies { /// Registers a specific type and its instantiating factory. func add(module: Module) { modules[module.name] = module } /// Resolves through inference and returns an instance of the given type from the current default container. /// /// If the dependency is not found, an exception will occur. func resolve (for name: String? = nil) -> T { let name = name ?? String(describing: T.self) guard let component: T = modules[name]?.resolve() as? T else { fatalError("Dependency '\(T.self)' not resolved!") } return component } } // MARK: - Public API public extension Dependencies { /// Composition root container of dependencies. fileprivate static var root = Dependencies() /// Construct dependency resolutions. convenience init(@ModuleBuilder _ modules: () -> [Module]) { self.init() modules().forEach { add(module: $0) } } /// Construct dependency resolution. convenience init(@ModuleBuilder _ module: () -> Module) { self.init() add(module: module()) } /// Assigns the current container to the composition root. func build() { // Used later in property wrapper Self.root = self } /// DSL for declaring modules within the container dependency initializer. @_functionBuilder struct ModuleBuilder { public static func buildBlock(_ modules: Module...) -> [Module] { modules } public static func buildBlock(_ module: Module) -> Module { module } } } /// A type that contributes to the object graph. public struct Module { fileprivate let name: String fileprivate let resolve: () -> Any public init (_ name: String? = nil, _ resolve: @escaping () -> T) { self.name = name ?? String(describing: T.self) self.resolve = resolve } } /// Resolves an instance from the dependency injection container. @propertyWrapper public class Inject { private let name: String? private var storage: Value? public var wrappedValue: Value { storage ?? { let value: Value = Dependencies.root.resolve(for: name) storage = value // Reuse instance for later return value }() } public init() { self.name = nil } public init(_ name: String) { self.name = name } }

To get a real sense of how this works, here is a working unit test that can be examined:

final class DependencyTests: XCTestCase { private static let dependencies = Dependencies { Module { WidgetModule() as WidgetModuleType } Module { SampleModule() as SampleModuleType } Module("abc") { SampleModule(value: "123") as SampleModuleType } Module { SomeClass() as SomeClassType } } @Inject private var widgetModule: WidgetModuleType @Inject private var sampleModule: SampleModuleType @Inject("abc") private var sampleModule2: SampleModuleType @Inject private var someClass: SomeClassType private lazy var widgetWorker: WidgetWorkerType = widgetModule.component() private lazy var someObject: SomeObjectType = sampleModule.component() private lazy var anotherObject: AnotherObjectType = sampleModule.component() private lazy var viewModelObject: ViewModelObjectType = sampleModule.component() private lazy var viewControllerObject: ViewControllerObjectType = sampleModule.component() override class func setUp() { super.setUp() dependencies.build() } } // MARK: - Test Cases extension DependencyTests { func testResolver() { // Given let widgetModuleResult = widgetModule.test() let sampleModuleResult = sampleModule.test() let sampleModule2Result = sampleModule2.test() let widgetResult = widgetWorker.fetch(id: 3) let someResult = someObject.testAbc() let anotherResult = anotherObject.testXyz() let viewModelResult = viewModelObject.testLmn() let viewModelNestedResult = viewModelObject.testLmnNested() let viewControllerResult = viewControllerObject.testRst() let viewControllerNestedResult = viewControllerObject.testRstNested() // Then XCTAssertEqual(widgetModuleResult, "WidgetModule.test()") XCTAssertEqual(sampleModuleResult, "SampleModule.test()") XCTAssertEqual(sampleModule2Result, "SampleModule.test()123") XCTAssertEqual(widgetResult, "|MediaRealmStore.3||MediaNetworkRemote.3|") XCTAssertEqual(someResult, "SomeObject.testAbc") XCTAssertEqual(anotherResult, "AnotherObject.testXyz|SomeObject.testAbc") XCTAssertEqual(viewModelResult, "SomeViewModel.testLmn|SomeObject.testAbc") XCTAssertEqual(viewModelNestedResult, "SomeViewModel.testLmnNested|AnotherObject.testXyz|SomeObject.testAbc") XCTAssertEqual(viewControllerResult, "SomeViewController.testRst|SomeObject.testAbc") XCTAssertEqual(viewControllerNestedResult, "SomeViewController.testRstNested|AnotherObject.testXyz|SomeObject.testAbc") } } extension DependencyTests { func testNumberOfInstances() { let instance1 = someClass let instance2 = someClass XCTAssertEqual(instance1.id, instance2.id) } } // MARK: - Subtypes extension DependencyTests { struct WidgetModule: WidgetModuleType { func component() -> WidgetWorkerType { WidgetWorker( store: component(), remote: component() ) } func component() -> WidgetRemote { WidgetNetworkRemote(httpService: component()) } func component() -> WidgetStore { WidgetRealmStore() } func component() -> HTTPServiceType { HTTPService() } func test() -> String { "WidgetModule.test()" } } struct SampleModule: SampleModuleType { let value: String? init(value: String? = nil) { self.value = value } func component() -> SomeObjectType { SomeObject() } func component() -> AnotherObjectType { AnotherObject(someObject: component()) } func component() -> ViewModelObjectType { SomeViewModel( someObject: component(), anotherObject: component() ) } func component() -> ViewControllerObjectType { SomeViewController() } func test() -> String { "SampleModule.test()\(value ?? "")" } } struct SomeObject: SomeObjectType { func testAbc() -> String { "SomeObject.testAbc" } } class SomeClass: SomeClassType { let id: String init() { self.id = UUID().uuidString } } struct AnotherObject: AnotherObjectType { private let someObject: SomeObjectType init(someObject: SomeObjectType) { self.someObject = someObject } func testXyz() -> String { "AnotherObject.testXyz|" + someObject.testAbc() } } struct SomeViewModel: ViewModelObjectType { private let someObject: SomeObjectType private let anotherObject: AnotherObjectType init(someObject: SomeObjectType, anotherObject: AnotherObjectType) { self.someObject = someObject self.anotherObject = anotherObject } func testLmn() -> String { "SomeViewModel.testLmn|" + someObject.testAbc() } func testLmnNested() -> String { "SomeViewModel.testLmnNested|" + anotherObject.testXyz() } } class SomeViewController: ViewControllerObjectType { @Inject private var module: SampleModuleType private lazy var someObject: SomeObjectType = module.component() private lazy var anotherObject: AnotherObjectType = module.component() func testRst() -> String { "SomeViewController.testRst|" + someObject.testAbc() } func testRstNested() -> String { "SomeViewController.testRstNested|" + anotherObject.testXyz() } } struct WidgetWorker: WidgetWorkerType { private let store: WidgetStore private let remote: WidgetRemote init(store: WidgetStore, remote: WidgetRemote) { self.store = store self.remote = remote } func fetch(id: Int) -> String { store.fetch(id: id) + remote.fetch(id: id) } } struct WidgetNetworkRemote: WidgetRemote { private let httpService: HTTPServiceType init(httpService: HTTPServiceType) { self.httpService = httpService } func fetch(id: Int) -> String { "|MediaNetworkRemote.\(id)|" } } struct WidgetRealmStore: WidgetStore { func fetch(id: Int) -> String { "|MediaRealmStore.\(id)|" } func createOrUpdate(_ request: String) -> String { "MediaRealmStore.createOrUpdate\(request)" } } struct HTTPService: HTTPServiceType { func get(url: String) -> String { "HTTPService.get" } func post(url: String) -> String { "HTTPService.post" } } } // MARK: API protocol WidgetModuleType { func component() -> WidgetWorkerType func component() -> WidgetRemote func component() -> WidgetStore func component() -> HTTPServiceType func test() -> String } protocol SampleModuleType { func component() -> SomeObjectType func component() -> AnotherObjectType func component() -> ViewModelObjectType func component() -> ViewControllerObjectType func test() -> String } protocol SomeObjectType { func testAbc() -> String } protocol SomeClassType { var id: String { get } } protocol AnotherObjectType { func testXyz() -> String } protocol ViewModelObjectType { func testLmn() -> String func testLmnNested() -> String } protocol ViewControllerObjectType { func testRst() -> String func testRstNested() -> String } protocol WidgetStore { func fetch(id: Int) -> String func createOrUpdate(_ request: String) -> String } protocol WidgetRemote { func fetch(id: Int) -> String } protocol WidgetWorkerType { func fetch(id: Int) -> String } protocol HTTPServiceType { func get(url: String) -> String func post(url: String) -> String }

Conclusion

Dependency injection is one of the most important mechanics of an architecture. Get it wrong early then unit tests become difficult to create, 3rd party dependencies become impossible to divorce, and modularity becomes out of reach.

To try out the dependency injection implementation, I created a repo and open sourced the library. I called it Shank, the name inspired by the Dagger library from the Android side. The APIs and module architecture were heavily inspired by the Koin library.

A final disclaimer, in iOS 13+ it is now possible to use constructor injection almost entirely, which is the best form of dependency injection; it’s clean, simple, and timeless.. best of all no magic. For previous versions, we’re stuck having to use these kinds of custom DI techniques.

Happy Coding!!

Further Reading