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:

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!!
Thanks for this article. It is great. Quick question. How can I use @Inject to pass parameters in a ViewController?
Thanks, great material. According to disclaimer, “in iOS 13+ it is now possible to use constructor injection almost entirely”. Could you please tell more about that? What happened with iOS 13+ that were preventing using constructor injection?
Thank you for the thorough write up! I would like to point out one important detail though, the proposed solution is an implementation of a Service Locator, and not of a Dependency Injection solution. You can find a nice comparison here: https://medium.com/analytics-vidhya/dependency-injection-and-service-locator-4dbe4559a3ba
Let’s look at the second code block above
@propertyWrapper
public struct Inject {
public var wrappedValue: Value {
Dependencies.root.resolve()
}
public init() {}
}
The code does not use Dependency Injection, because:
1. Classes that consume the dependencies are actively asking for them. (The property wrapper `Inject` is a method call that request the Dependency Manager class `Dependency` to provide an object)
2. Consuming modules depend on the Dependency Manager. (The class `ViewController` in your example above which consumes the dependency would not compile without the Dependency Manager)
The code further down adds some syntactic sugar, but that doesn’t change the basic mechanics of how dependencies are resolved (not injected).