Dependency Injection is a really simple concept allowing among other things to decouple the different components of your application’s code (and it’s not just about getting rid of these nasty shared instances!).
There is already a ton of resources online that talks about it. There are also different implementations for Swift projects, but not of them were quite fitting to my needs as they are often:
– done at compile time, meaning that your app can crush at runtime if you mis-configured something and you could notice it too late when your app is already released in production
– not type safe, passing optionals all around
– asking for ton of boilerplate
– not that easy to use when it comes to testing
– not very opinionated on what belong to a centralized dependency injection container and what should just be passed around
In a decently sized project I’m working on, transitioning from all the Singleton design pattern used all over the project with shared instances representing most of my controllers, managers and services to something where dependencies are properly injected asks for a big deal of time and re-architecting.
The following is an experimental solution, even though it works in a small project, it’s really easy to generate cyclic dependencies and have your project crash at run-time — do not use in production.
Among all possible dependency injections (initializers, properties, methods, ambient, etc.), I believe passing down dependencies during components initialization is the cleanest way as it allows compile-time enforcement and remove the need for optionals and/or force unwrapping. Sadly, passing down dependencies from where there are likely being initialized (often the root of the application, like the AppDelegate
) throughout the app isn’t really something you would consider clean. For example, if a component six levels down the navigation needs a particular dependency to function, you would have to pass it through the five first view controllers for it to be available during the sixth component initialization, this isn’t really elegant as most of these view controllers probably won’t need it and it’s simply tightening up the whole dependency graph for nothing.
With the rise of the Coordinator
design pattern, it become easier to centrally manage dependencies. Coordinators is the one place responsible for creating view models and view controllers and can therefore pass on the dependencies in initializers. This is convenient and already cleaner, but let’s imagine this view that requires a lot of dependencies:
class SomeViewModel {
// MARK: - Initializers
init(dependencyA: DependencyA, dependencyB: DependencyB, dependencyC: dependencyC, ..., dependencyX: DependencyX) {
self.dependencyA = dependencyA
// ...
}
// MARK: - Dependencies
let dependencyA: DependencyA
let dependencyB: DependencyB
let dependencyC: DependencyC
...
let dependencyX: DependencyX
}
Now let’s look at the component initializing this view model:
class SomeCoordinator {
let dependencyContainer: DependencyContainer // For our own sake, let's at least imagine dependencies are centralized somewhere
func showSomeViewController() {
let viewModel = SomeViewModel(dependencyA: dependencyContainer.dependencyA,
dependencyB: dependencyContainer.dependencyB,
dependencyD: dependencyContainer.dependencyD,
...
dependencyX: dependencyContainer.dependencyX)
let viewController = SomeViewController(viewModel: viewModel)
navigationController.push(viewController: viewController, animated: true)
}
}
By now, you must certainly see where I’m going: there is a ton of boilerplate to write just to pass down these dependencies. It’s super annoying to write, and it’s taking a huge amount of space in your code making it not as clear and readable as it should be.
But what solutions do we have? Let’s take it bit by bit.
Surely we could fix the initializer signature from getting too many parameters by grouping the dependencies into some kind of data structure:
class SomeViewModel {
// MARK: - Initializers
init(dependencies: SomeViewModel.Dependencies) {
self.dependencies = dependencies
}
// MARK: - Dependencies
let dependencies: SomeViewModelDependencies
struct Dependencies {
let dependencyA: DependencyA
let dependencyB: DependencyB
let dependencyC: dependencyC
...
let dependencyX: DependencyX
}
}
We would still need to initialize the dependencies struct before passing it to the view model, and it introduces one more annoyance in not having the dependencies accessible directly. Instead, the methods insides of view model would have to refer to dependencies.dependencyA
to use it.
So, we are left with boilerplate issue. It’s now time for some code generation and Sourcery seemed to be a good tool to use.
I like to keep things isolated, so I will create a new folder at the root of my project named AutoGenerated
. This folder will contain three sub-folders:
– Generated/
where we will add files generated by Sourcery to our project
– Templates/
where we will group our stencil templates
– Protocols/
where we will define some types to be detected by Sourcery
Let’s then add the AutoDependencies.swift
file in the Protocols/
folder with its content being as simple as:
import Foundation
protocol AutoDependencies {}
In the Templates/
folder, we will then add the AutoDependencies.stencil
template, it’s content relatively easy to understand:
{% for type in types.implementing.AutoDependencies %}
// MARK: {{type.name}}
extension {{type.name}} {
{% for dependencies in type.containedTypes.Dependencies %}
{% for variable in dependencies.variables %}
var {{variable.name}}: {{variable.typeName}} {
return dependencies.{{variable.name}}
}
{% endfor %}
{% endfor %}
}
extension DependencyContainer {
func generate{{type.name}}Dependencies() -> {{type.name}}.Dependencies {
return {{type.name}}.Dependencies({% for dependencies in type.containedTypes.Dependencies %}{% for variable in dependencies.variables %}{{variable.name}}: {{variable.name}}{% if not forloop.last %}, {% endif %}{% endfor %}{% endfor %})
}
}
{% endfor %}
For each type implementing the empty AutoDependencies
protocol, two things will be done:
– an extension on the type will be created to make and a simple get variable will be made so that you can use dependencyA
directly
– an extension on the DependencyContainer will be created to easily create the dependencies struct
Now, let’s configure our project for it generate the code we need at build time. To do that, we simply need to add new Run Script
in our target Build Phases
(replace XXX with the proper path in your project):
sourcery --sources XXX/\
--templates XXX/AutoGenerated/Templates/\
--output XXX/AutoGenerated/Generated/
(note that at this stage, I’m using Sourcery installed on my machine with brew install sourcery
)
We now need our class to implement the AutoDependencies
protocol, it stays exactly the same as before otherwise:
class SomeViewModel: AutoDependencies {
// MARK: - Initializers
init(dependencies: SomeViewModel.Dependencies) {
self.dependencies = dependencies
}
// MARK: - Dependencies
let dependencies: SomeViewModelDependencies
struct Dependencies {
let dependencyA: DependencyA
let dependencyB: DependencyB
let dependencyC: dependencyC
...
let dependencyX: DependencyX
}
}
On save, a AutoDependencies.generated.swift
is automatically created by Sourcery. This file looks like:
// MARK: SomeViewModel
extension SomeViewModel {
var dependencyA: DependencyA {
return dependencies.dependencyA
}
var dependencyB: DependencyB {
return dependencies.dependencyB
}
var dependencyC: DependencyC {
return dependencies.dependencyC
}
...
var dependencyX: DependencyX {
return dependencies.dependencyX
}
}
extension DependencyContainer {
func generateSomeViewModelDependencies() -> SomeViewModel.Dependencies {
return SomeViewModel.Dependencies(dependencyA: dependencyA, dependencyB: dependencyB, dependencyC: dependencyC, ..., dependencyX: dependencyX)
}
}
We need to add it to the Xcode project by right-clicking on our Generated/
folder and “Add Files to …”.
In our coordinator, we can now simplify:
class SomeCoordinator {
let dependencyContainer: DependencyContainer // For our own sake, let's at least imagine dependencies are centralized somewhere
func showSomeViewController() {
let dependencies = dependencyContainer.generateSomeViewModelDependencies()
let viewModel = SomeViewModel(dependencies: dependencies)
let viewController = SomeViewController(viewModel: viewModel)
navigationController.push(viewController: viewController, animated: true)
}
}
Want to see what’s in the DependencyContainer?
class DependencyContainer {
lazy var dependencyA: DependencyA = DependencyA()
lazy var dependencyB: DependencyB = DependencyB(dependencies: generateDependencyBDependencies()) // Assuming DependencyB is also an AutoDependencies
lazy var dependencyC: DependencyC = DependencyC()
...
lazy var dependencyX: DependencyX = DependencyX()
}
It’s a really dumb file lazy instantiating my dependencies as they need be. (Obviously, this kind of dependency container is too dumb to manage circular dependencies and is not a proper dependency graph, I let that to you to solve this problem!)
What about testing? The dependency container implementation being a bunch of lazy var, you can easily swap one with a mock:
class SomeViewControllerTests: XCTestCase {
func testExample() {
// Given
let container = DependencyContainer()
container.dependencyA = MockDependencyA()
let dependencies = container.generateSomeViewModelDependencies()
let viewModel = SomeViewModel(dependencies: dependencies)
// When
...
// Then
...
}
class MockDependencyA: DependencyA {
override func someMethod() -> Bool {
return true // Let's say I want this method to always return true in my tests
}
}
So in the end, what do we have? I will say:
– a functional type safe dependency injection enforced at build time that will probably work in most project
– some low(er) amount of boilerplate: you simply have to declare you type to comply to the AutoDependencies
protocol, have a let dependencies: Dependencies
and a Dependencies
struct with all the desire dependencies
– Sourcery runs automatically when you build, so Xcode might show errors until you actually press Build
– adding a dependency to a type is done in only one place (the Dependencies struct), you simply Build again and you are good to use it in your code
– testable code
One caveat this system is build on-top is that no all dependencies need to be injected this way. For instance, a view controller is still responsible for passing dependencies to subview that will not own necessarily own it, for instance using initializer, property or method injection.