In this article we are going to go through a couple of solutions to a common problem when developing mobile applications, and this is Dependency Injection.
We all saw a project that was calling singleton classes from any part of the code without any security when writing data to them, battling with race conditions, and coupling implementations to all these issues. You can also find many third-party libraries that can help to solve or at least manage these problems, but many times we don’t want or we can’t add extra dependencies, in this case, a good approach is to implement the solution that fits your needs in a basic but sufficient way and with native code.
To start with this we will go through the problem first and see what would actually help us. Let’s say we have a class called AViewModel that needs to use ServiceA, the first thing to think of is to instantiate the service in the initializer or directly inside the class like this:
class AViewModel {
let serviceA = ServiceA.shared
}
This does not give us any guarantee that at the moment we call the service, the instance has not been used by some other code that could imply race conditions or unwanted behaviors if not handled. It’s also worth mentioning that this class would be impossible to test decoupled from the ServiceA singleton instance. To improve this a little we could go with something like this:
protocol ServiceAProtocol {
func execute()
}
class AViewModel {
let serviceA = ServiceAProtocol
init(serviceA: ServiceAProtocol) {
self.serviceA = ServiceA
}
}
Now we can create a mocked class that implements ServiceAProtocol and we can test AViewModel without depending on ServiceA.shared instance.
To solve this we can first implement this little system with protocols and typealiases that will let us define which dependencies we need for a certain class and contain that definition in it. If we adapt our AViewModel to handle this we would have something like this:
protocol ServiceAContainer {
var serviceA: ServiceAProtocol { get }
}
class AViewModel {
typealias Dependencies = ServiceAContainer
let dependencies: Dependencies
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
}
Inside AViewModel now we can access serviceA directly from our dependencies property.
Maybe you still don’t see the benefit of this abstraction, but you will in a bit. For now, let’s see how we could use this implementation with the service as it was before, this could be done so:
extension AViewModel {
struct ADependencies: Dependencies {
let serviceA: ServiceAProtocol = ServiceA.shared
}
convenience init() {
self.init(dependencies: ADependencies())
}
}
So at this point, if we handle when AViewModel is instantiated and we are sure it would be ok if it instantiates ServiceA after any other class might have instantiated it, we are good to go.
Imagine that we don’t only have ServiceA but also ServiceB and probably many more that will be used all over our app, and remember that in the last paragraph we made the assumption that we are not running against any race condition because of using shared instances. If we want to have control of all of these classes, we need to centralize them in some container, that we are going to call DependencyContainer.
protocol ServiceBContainer {
var serviceB: ServiceBProtocol { get }
}
typealias DependencyContainers = ServiceAContainer
& ServiceBContainer
struct DependencyContainer: DependencyContainers {
var serviceA: ServiceAProtocol
var serviceB: ServiceBProtocol
}
With this little code we can have a struct that has a reference to specific implementations of the desired services only knowing them by the protocol and the best of this is that DependencyContainer conforms to a typeAlias that guarantees us that the class has those references.
At this point, we have the possibility to create an instance of this container, and access its services like so:
let container = DependencyContainer(serviceA: ServiceA.shared,
serviceB: ServiceB.shared)
And as DependencyContainer conforms to ServiceAContainer because it’s defined in DependencyContainers we can go ahead and try to adapt our AViewModel to make use of this:
extension AViewModel {
convenience init() {
let container = DependencyContainer(serviceA: ServiceA.shared,
serviceB: ServiceB.shared)
self.init(dependencies: container)
}
}
So if we initialized the container at the beginning of the app, we are sure all our instances will be ready when any other class needs them after the app is launched. For example, if we now want to create a new class that uses ServiceB it would be really straightforward:
class BViewModel {
typealias Dependencies = ServiceBContainer
let dependencies: Dependencies
init(dependencies: Dependencies) {
self.dependencies = dependencies
}
}
extension BViewModel {
convenience init() {
let container = DependencyContainer(serviceA: ServiceA.shared,
serviceB: ServiceB.shared)
self.init(dependencies: container)
}
}
And if we want to have more than one dependency it’s as easy as adding them to the specific typealias for each class and as long as the main container implements this container, you are ready to go.
This implementation will serve the purposes of having a solid DI system without the need for external code and it’s simple enough to have control of it, but let’s give it a little twist more to make use of property wrappers to simplify a bit the boilerplate code we need to implement each dependency injection.
First we need to know what a WritableKeyPath is in Swift, in short, Key Paths are expressions to access properties dynamically. In the case of WritableKeyPath those let you read and write from the value you are accessing, so if we want a property wrapper that provides us a dependency that is inside a container, it means we will have a value stored in DependencyContainer that we can dynamically access with a given WritableKeyPath. Once we make it, it will look like this in our AViewModel:
@Dependency(\.aService) var aService
As you can see there is no need to add any other property nor initialize anything at the point of the integration, which is very convenient. But let’s see the code involved to get to this result, and for this we need to implement a generic type to store the dependencies in the container, we also need to handle the nil case so we can set dependencies after the container is created. So let’s go with this:
/// A protocol to define an injected dependency.
public protocol DependencyKey {
/// Representing the type of the injected dependency.
associatedtype Value
/// The current value of the injected dependency.
static var currentValue: Self.Value { get set }
}
/// A protocol to define an injected dependency whose initial value is set lazy.
public protocol LazyDependencyKey {
/// Representing the type of the injected dependency.
associatedtype Value
/// The current value of the injected dependency.
static var currentValue: Self.Value? { get set }
}
extension LazyDependencyKey {
/// The unwrapped value of the injected dependency. Fails if the actual value has not been set before access.
static var value: Self.Value {
get {
guard let currentValue = currentValue else {
preconditionFailure("A value must be set before accessing the property.")
}
return currentValue
}
set {
currentValue = newValue
}
}
}
These protocols have a Value type associated and a currentValue that is an instance of that Value type. With this in place, we can now define our DependencyContainer class as follows:
public class DependencyContainer {
/// Singleton instance used to be accessed by the computed properties.
private static var current = DependencyContainer()
/// Access the dependency with the specified `key`.
/// - Parameter key: Implementation type of `DependencyKey`.
public static subscript<K>(key: K.Type) -> K.Value where K: DependencyKey {
get { key.currentValue }
set { key.currentValue = newValue }
}
/// Accesses the dependency with the specified `keyPath`.
/// - Parameter keyPath: The key path of the computed property.
public static subscript<T>(_ keyPath: WritableKeyPath<DependencyContainer, T>) -> T {
get { current[keyPath: keyPath] }
set { current[keyPath: keyPath] = newValue }
}
/// Set the initial value for the specified `key`. This method has to be called before the property is injected or accessed anywhere.
/// - Parameter initialValue: The initial value that is injected wherever it is used.
/// - Parameter key: The key to set the value for.
public static func set<K>(initialValue: K.Value, key: K.Type) where K: LazyDependencyKey {
key.currentValue = initialValue
}
}
Here we can see that the container is only storing a value or returning it for a specific Key Path and that is more or less it. Now we have to define the property wrapper and include it in our original class.
The property wrapper would look something like this:
/// A property wrapper type that reflects a dependency injected using `DependencyContainer`.
@propertyWrapper
public struct Dependency<T> {
private let keyPath: WritableKeyPath<DependencyContainer, T>
public var wrappedValue: T {
get { DependencyContainer[keyPath] }
set { DependencyContainer[keyPath] = newValue }
}
public init(_ keyPath: WritableKeyPath<DependencyContainer, T>) {
self.keyPath = keyPath
}
}
Now if for example we want to use BService in BViewModel we should first create the key for the service, and set it in the container, we also need to define a property to access it directly in the DependencyContainer.
public struct BServiceDependencyKey: LazyDependencyKey {
public static var currentValue:BService?
}
extension DependencyContainer {
public var bService: BService {
get { Self[BServiceDependencyKey.self] }
set { Self[BServiceDependencyKey.self] = newValue }
}
}
...
// Somewhere on your app launch code
setupDependencies()
}
// MARK: - Dependencies
private extension AppDelegate {
func setupDependencies() {
DependencyContainer.set(initialValue: BService.shared, key: BServiceDependencyKey.self)
DependencyContainer.set(initialValue: AService.shared, key: AServiceDependencyKey.self)
}
}
class BViewModel {
@Dependency(\.bService) var bService
}
class AViewModel {
@Dependency(\.aService) var aService
}
And that is it! We have a one-line dependency injection with total control of the instances we have at the moment of launching the app and instantiating any class that needs them.
Author
-
iOS Developer working in the software development industry with agile methodologies. Skilled in Swift, objective-C, Python, PostgreSQL, SQL, PHP, and C++. More than 8 years of experience working as iOs Developer, following the best practices.
View all posts