Resolver for iOS Dependency Injection: Getting Started
Learn how to use Resolver to implement dependency injection in your SwiftUI iOS apps to achieve easily readable and maintainable codebases. By Mina Ashna.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Resolver for iOS Dependency Injection: Getting Started
25 mins
- Getting Started
- Refactoring Tightly Coupled Classes
- Inversion of Control
- Understanding Dependency Flow
- Dependency Injection Without Resolver
- Dependency Injection Using Resolver
- Injecting Dependencies by Annotation
- Registering Services
- Registering Arguments
- Service Locator
- Using Scopes
- Unit Testing and the Dependency Inversion Principle
- Registering Protocols
- Generating Mock Data
- Using Resolver’s Containers
- Registering Services in a Mock Container
- Dependency Injection in Unit Tests
- Unit Testing in Action
- Where to Go From Here?
Dependency Injection Using Resolver
Resolver is a Dependency Injection framework for Swift that supports IoC. The framework is already in your Xcode project via the Swift Package Manager, or SwiftPM. Along with SwiftPM, Resolver also supports CocoaPods and Carthage.
In general, there are three ways to perform dependency injection: Constructor Injection, Method Injection and Property Injection. Resolver also introduced a new type named Annotation.
- Constructor Injection: Injecting dependencies through constructors or initializers.
- Method Injection: Injecting dependencies through methods.
- Property Injection: Injecting dependencies through properties.
-
Annotation Injection: Resolver uses
@Injected
as a property wrapper to inject dependencies.
In this tutorial, you’ll use Annotation. You’ll also use Service Locator, a design pattern distinct from Dependency Injection.
Injecting Dependencies by Annotation
It’s your time to shine! Refactor the code so it uses the Annotation technique to inject dependencies.
First, find NetworkService.swift and replace:
private var session: URLSession
init(session: URLSession) {
self.session = session
}
With:
@Injected private var session: URLSession
Here, you replace Constructor Injection with Annotation Injection. Using @Injected
, you declare that Resolver resolves and injects this dependency. Now, NetworkService
can focus on its main job, fetching assets.
Next, open AssetService.swift. As you can see, AssetService
is dependent on NetworkService
and URLComponentsService
. To loosen these dependencies, you need to wrap them in @Injected
.
Replace:
private let networkService = NetworkService(
session: URLSession(configuration: .default))
private let urlComponentsService = URLComponentsService()
With:
@Injected private var networkService: NetworkService
@Injected private var urlComponentsService: URLComponentsService
As you see, AssetService
doesn’t have to create the URLSession
and inject it to NetworkService
anymore. From now on, Resolver will take care of all dependencies.
Finally, you need to inject a dependency in AssetListViewModel.swift. AssetListViewModel
is dependent on AssetService
. To make sure Resolver handles that as well, wrap assetService
in @Injected
.
Try modifying the code yourself before seeing the solution.
[spoiler title=”Solution”]
@Injected private var assetService: AssetService
[/spoiler]
Good job! Now all your dependencies use Resolver of type Annotation.
Even though you can build the app, it crashes at runtime. Don’t worry: You’ll soon find out why that’s happening and how to fix it.
Patience, you must have, my young Padawan.
Registering Services
You learned how to inject dependencies using Resolver by annotating them with @Injected
. But, what’s the magic? How does Resolver know to resolve URLSession
like URLSession(configuration: .default)
but NetworkService
like NetworkService()
?
The magic is that there is no magic!
In the Resolver world, you need to provide all dependencies for Resolver. You do that by registering them in a factory called registerAllServices()
. In short, no matter which DI technique you use, Resolver will check the factory to find the registered services and resolve them.
To register dependencies, find App+Injection.swift and add:
// 1
extension Resolver: ResolverRegistering {
public static func registerAllServices() {
// 2
register { URLSession(configuration: .default) }
// 3
register { NetworkService() }
register { URLComponentsService() }
register { AssetService() }
}
}
Here you:
- Extend Resolver and conform to
ResolverRegistering
, which forces you to implementregisterAllServices()
. - Register
URLSession
with a default configuration. - Register
NetworkService
,URLComponentsService
andAssetService
.
As a result, each time you use Resolver to inject a dependency, you need to register it in the factory. Resolver calls registerAllServices()
the first time it needs to resolve an instance.
Build and run. Even though there are no visual changes, now your classes are loosely coupled behind the scenes.
Next, you’ll learn how to register arguments for your dependencies.
Registering Arguments
Not all services are as easy to register as the ones you just did. Some services take arguments you need to provide when registering them.
For these cases, Resolver offers the possibility of registering dependencies with arguments. For example, open AssetListView.swift. Then, find the following line in .loaded
:
AssetView(assetViewModel: AssetViewModel(asset: asset))
As you can see, AssetView
is dependent on AssetViewModel
. Look closer and you’ll see it’s not like other dependencies. It takes asset
as an argument.
Now, you’ll register AssetViewModel
with an argument. Go to App+Injection.swift. In registerAllServices()
, add:
register { _, args in
AssetViewModel(asset: args())
}
Resolver uses the new callAsFunction
feature from Swift 5.2 to immediately get the single passed argument from args
.
To resolve this dependency, go back to AssetListView.swift.
Replace:
AssetView(assetViewModel: AssetViewModel(asset: asset))
With:
AssetView(assetViewModel: Resolver.resolve(args: asset))
Here, you ask Resolver to resolve a dependency of type AssetViewModel
by passing asset
as an argument.
Build and run. See your reward. That app should look and feel exactly the same as before, but you now have more loosely coupled classes :]
Service Locator
Service Locator is another design pattern you can use to implement IoC. Fortunately, Resolver supports Service Locator well. You may ask yourself, why am I using Service Locator when Annotation is so convenient?
Find AssetListView.swift and check assetListViewModel
. As you can see, it already has @ObservedObject
as property wrapper. So, you can’t use Annotation and add @Injected
here.
Instead, you either have to use Service Locator or other types of DI, such as Constructor Injection. In this tutorial, you’ll use Service Locator.
Next, still in AssetListView.swift, resolve AssetListViewModel
using Service Locator. Replace:
@ObservedObject var assetListViewModel: AssetListViewModel
With:
@ObservedObject var assetListViewModel: AssetListViewModel = Resolver.resolve()
Resolver.resolve()
reaches out to registerAllServices()
, where all services are registered. Then, it looks for AssetListViewModel
. As soon as it finds the instance, it resolves it.
Next, you need to register AssetListViewModel
as a service. Open App+Injection.swift. Then, add the following to registerAllServices()
:
register { AssetListViewModel() }
Now go to AppMain.swift.
Resolver will handle the dependency for you, so you can remove AssetListViewModel
. Replace:
AssetListView(assetListViewModel: AssetListViewModel())
With:
AssetListView()
Build and run. Now, you can easily access modules without worrying about dependencies.
Using Scopes
Resolver uses Scopes to control the lifecycles of instances. There are five different scopes in Resolver: Graph, Application, Cached, Shared and Unique.
To better understand the scopes, check the app’s main dependency flow again.
- The resolution cycle starts when
AssetListView
asks Resolver to resolveassetListViewModel
. - To resolve
assetListViewModel
, Resolver has to resolveAssetService
,URLComponentsService
andNetworkService
. - The cycle finishes when it resolves them all.
In this tutorial, you’ll use Graph and Application scopes.
- Graph: Graph is Resolver’s default scope. Once Resolver resolves the requested object, it discards all objects created in that flow. Consequently, the next call to Resolver creates new instances.
-
Application: Application scope is the same as a singleton. The first time Resolver resolves an object, it retains the instance and uses it for all subsequent resolutions as long as the app is alive. You can define that by adding
.scope(.application)
to your registrations.
You can define scopes in two ways. First, you could add scope to each registration separately like this:
register { NetworkService() }.scope(.graph)
Alternatively, could add scope to all registrations by changing the default scope. It’s important to set the default scope before registering services.
You’ll use the Graph scope in your app target. Go to App+Injection.swift. Add the following as the first line in registerAllServices()
:
defaultScope = .graph
Great job! You’re about to become a DI master by adding some unit tests to the project.
But first, build and run. Enjoy your work. :]