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, or DI, is essential for creating maintainable and testable apps. While DI has been around for many years, some find it intimidating. It sounds complicated, but it’s simple to implement, especially with tools like Resolver, a Dependency Injection framework for Swift.
In this tutorial, you’ll refactor a cryptocurrency app named Cryptic. Along the way, you’ll learn about:
- why you should care about DI.
- how to enable DI using Resolver.
- what Inversion of Control and Dependency Inversion Principle means.
- how to unit test with DI.
Getting Started
Click Download Materials at the top or bottom of this tutorial to download the project. Open the starter project in Xcode and run the app.
You’ll see a list of the top ten crypto-assets sorted by the highest market cap. Don’t worry if the numbers or assets you see are different than those you see in the image. After all, it’s the cryptocurrency market: No changes would be surprising. :]
Look at the code, and you’ll see tightly coupled classes. Even though you can run the app, this code is neither maintainable nor testable. Your goal is to refactor the code, so the classes become loosely coupled. If these terms are foreign to you, don’t worry — you’ll be covering them throughout the tutorial.
Before you start, take a moment to explore the classes in the project.
Refactoring Tightly Coupled Classes
In tightly coupled classes, changes in one class result in unanticipated changes in the other. Thus, tightly coupled classes are less flexible and harder to extend. Initially, this might not be a problem, but as your project grows in size and complexity it becomes harder and harder to maintain.
As you see in the diagram below, you can use a few techniques to create loosely coupled classes. Before diving into the code, it’s important to understand these techniques.
Inversion of Control, or IoC, and Dependency Inversion Principle or DIP, are design principles. While design principles recommend certain best practices, they don’t provide any implementation details.
On the other hand, Dependency Injection is one of the many patterns you can use to implement design principles.
Next, you’ll take a closer look at Inversion of Control.
Inversion of Control
IoC recommends moving all of a class’s responsibilities, other than its main one, to another class.
For a better understanding, imagine yourself as a startup founder. In the beginning, you deal with development, taxes, recruitment, salaries and many other things yourself. As your business grows, you need to start delegating tasks and introduce dependencies.
You do that by adding different departments like legal and HR. As a result, you aren’t involved in the complexity of their work. Instead, you can focus on your main job, which is running the startup.
It’s the same in an app life cycle. When the app is small, it’s easy to handle the dependencies. But, as it grows, managing dependencies becomes more and more complicated.
In this tutorial, you’ll use DI to implement IoC.
Next, you’ll explore dependency flow.
Understanding Dependency Flow
DI is a design pattern you can use to implement IoC. It lets you create dependent objects outside of the class that depends on them.
Before you can add DI to any project, you need to understand that project’s dependency flow. Here’s the main dependency flow for this project:
-
AssetListView
is the project’s main view. It’s dependent onAssetViewModel
. -
AssetViewModel
needsAssetService
. -
AssetService
is dependent onURLComponentsService
andNetworkService
to fetch the assets. - And finally,
NetworkService
is dependent onURLSession
.
Your goal with DI is to resolve this chain of dependencies. Now that you know the dependency flow of the app and the task at hand, it’s time to get started.
Dependency Injection Without Resolver
Open Xcode and find NetworkService.swift. As you can see by its only property, NetworkService
is dependent on URLSession
. You need to hide the details of creating URLSession
from NetworkService
.
This is important because any changes in URLSession
will result in changes to NetworkService
and all classes dependent on NetworkService
. For example, using a custom configuration instead of the default one in URLSession
would change NetworkService
.
You can achieve a looser coupling by adding an initializer to NetworkService
that receives the session
. Replace:
private let session = URLSession(configuration: .default)
With:
// 1
private var session: URLSession
// 2
init(session: URLSession) {
// 3
self.session = session
}
Here you:
- Remove the
URLSession
constructor and make the session mutable. - Create an initializer for
NetworkService
and receive the session as a dependency. - Update the local session variable with the newly received value from the initializer.
Now, NetworkService
isn’t responsible for creating the session. Instead, it receives it through the initializer. Now, build the app.
Yes, it fails. You defined an initializer for NetworkService
as a way to inject the session, but you haven’t injected that dependency yet.
You need to provide the session for NetworkService
. Open AssetService.swift and replace:
private let networkService = NetworkService()
With:
private let networkService = NetworkService(
session: URLSession(configuration: .default))
Here, AssetService
resolves the session by creating a URLSession
instance and passing it to NetworkService
.
As a result, NetworkService
is loosely coupled to URLSession
. Now, AssetService
injects the session to NetworkService
.
Build and run. Everything is back to normal. :]
Take a closer look at AssetService
. Before it was only dependent on NetworkService
and URLComponentsService
. Now, it’s also dependent on URLSession
.
Yes, you’re about to open a rabbit hole. But don’t worry: Resolver is here to save the day. :]
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. :]
Unit Testing and the Dependency Inversion Principle
Unit testing is an important step in building a clean and maintainable codebase. To ease the process of unit testing, you’ll follow the Dependency Inversion Principle, or DIP.
DIP is one of the SOLID principles popularized by Robert Martin, also known as Uncle Bob. It declares that high-level modules shouldn’t depend on low-level modules. Instead, both should depend on abstractions.
For example, AssetService
, a high-level module, should be dependent on an abstraction of NetworkService
, a low-level module. You implement abstractions in Swift with Protocols.
The following steps show how to implement DIP:
- First, create a protocol for the low-level module.
- Second, update the low-level module to conform to the protocol.
- Third, update the high-level module to be dependent on the low-level module protocol.
To apply DIP to your project, you need to go through the steps one by one.
First, create a protocol for the low-level module. In this case, NetworkService
is the low-level module. So, open NetworkService.swift and add NetworkServiceProtocol
right below // MARK: - NetworkServiceProtocol
:
protocol NetworkServiceProtocol {
func fetch(
with urlRequest: URLRequest,
completion: @escaping (Result<Data, AppError>) -> Void
)
}
Second, update the low-level module to conform to the protocol. Since NetworkService
is the low-level module, it has to conform to the protocol. Find the line of code that reads:
extension NetworkService {
And replace it with:
extension NetworkService: NetworkServiceProtocol {
Third, update the high-level module dependency to the protocol. AssetService
is the high-level module. So, in AssetService.swift, change networkService
to be of type NetworkServiceProtocol
. Replace:
@Injected private var networkService: NetworkService
with:
@Injected private var networkService: NetworkServiceProtocol
Good job! Soon, you’ll find out in practice why this is important.
You can’t build the app now because your NetworkService
registration doesn’t reflect the protocol. You’ll fix that next.
Registering Protocols
When resolving instances, Resolver infers the registration type based on the type of object the factory returns. Thus, if your instances are of type protocol like this:
@Injected private var networkService: NetworkServiceProtocol
You need to make sure your registration in the factory returns the protocol type as well.
Open App+Injection.swift. Then, update register { NetworkService() }
to address the protocol:
register { NetworkService() }.implements(NetworkServiceProtocol.self)
Here you create an object of type NetworkService
. But, it’s returned as a type of NetworkServiceProtocol
, thus confirming to the requirement of NetworkService
.
Now build and run. Enjoy the beauty you created even though you can’t see it. :]
With the Dependency Inversion Principle in place, you’ve now made it easier to test your code. You’ll do this next.
Generating Mock Data
Mocking is an essential technique when writing unit tests. By mocking dependencies, you create fake versions of them, so your tests can focus solely on the class at hand rather than its collaborators.
For a better understanding, open AssetServiceTests.swift. Here, you test if AssetService
handles the success and failure responses of NetworkService
as expected.
In cases like this, mocking is an excellent approach because you’re in control of how you challenge your Subject Under Test, or SUT, based on different response types.
Now go to MockNetworkService.swift and add:
// 1
class MockNetworkService: NetworkServiceProtocol {
// 2
var result: Result<Data, AppError>?
// 3
func fetch(
with urlRequest: URLRequest,
completion: @escaping (Result<Data, AppError>) -> Void
) {
guard let result = result else {
fatalError("Result is nil")
}
return completion(result)
}
}
Here you:
- Create a mock class that conforms to
NetworkServiceProtocol
. You’ll use this class in your Test target instead ofNetworkService
. That’s the beauty of abstractions. - Then create the result property. The default value is
nil
. You’ll assignsuccess
andfailure
to it based on your test case. - As a result of conforming to
NetworkServiceProtocol
, you need to implementfetch(with:completion:)
. You can modify the result as you want because it’s a mock class.
Next, you’ll register these mock classes in Resolver, so you can resolve them when testing.
Using Resolver’s Containers
In a DI system, a container contains all the service registrations. By using Resolver, you can create different containers based on what your project needs. In this tutorial, you’ll create a Mock container for your Test target.
By default, Resolver creates a main container for all static registrations. It also defines a root container. If you inspected Resolver’s code you’d see:
public final class Resolver {
public static let main: Resolver = Resolver()
public static var root: Resolver = main
}
You can create different containers and decide when to use them to resolve instances by pointing at them using the root container.
In this project, you use the default main container in the App target and a mock container in the Test target, as shown in the diagram below.
App Target
Test Target
Registering Services in a Mock Container
Now, you’ll register your MockNetworkService
in a mock container. Go to Resolver+XCTest.swift. Right below // MARK: - Mock Container
create a mock container by adding:
static var mock = Resolver(parent: .main)
Now, still in Resolver+XCTest.swift, change the default root value in registerMockServices()
. It must point to your new mock container:
root = Resolver.mock
You’ll use the Application scope instead of Graph in your test target. In registerMockServices()
, add:
defaultScope = .application
Then, register your mock service in the mock container. In registerMockServices()
, right after defaultscope
, add:
Resolver.mock.register { MockNetworkService() }
.implements(NetworkServiceProtocol.self)
Now that your registration is complete, it’s time to use it.
Dependency Injection in Unit Tests
Open AssetServiceTests.swift. Then, call registerMockServices
at the bottom of setUp()
:
Resolver.registerMockServices()
This call ensures all dependencies are registered before use.
Finally, add NetworkService
to AssetServiceTests
. Right below // MARK: - Properties
, add:
@LazyInjected var networkService: MockNetworkService
You might be thinking, “What the heck is @LazyInjected
?” Calm down! It’s not a big deal.
Well, actually, it is. :]
By using Lazy Injection you ask Resolver to lazy load dependencies. Thus, Resolver won’t resolve this dependency before the first time it’s used.
You need to use @LazyInjected
here because the dependencies aren’t available when the class is initiated as you registered them in setup()
.
Now everything is ready for you to write your first unit test.
Unit Testing in Action
Open AssetServiceTests.swift. Then, in testFetchAssetsSuccessfully()
add:
// 1
let asset = mockAsset()
// 2
networkService.result = .success(assetList())
// 3
sut?.fetchAssets { assetList, error in
XCTAssertEqual(assetList?.data.count, 1)
XCTAssertEqual(assetList?.data.first, asset)
XCTAssertNil(error)
}
Here you:
- Create a mock asset. The helper method
mockAsset()
is already included in the codebase for you. You’re welcome. :] - Assert a success result to
MockNetworkService
as you test a success case. - Fetch the assets from
MockNetworkService
. Test if you receive the same asset back as you provided toMockNetworkService
.
By letting Resolver handle the dependencies, you can focus on testing AssetService
without any problems. As a result, you can add as many tests as needed.
Add the following code in testFetchAssetsFailure()
to test a failure case:
// 1
let networkError = AppError.network(description: "Something went wrong!")
// 2
networkService.result = .failure(networkError)
// 3
sut?.fetchAssets { assetList, error in
XCTAssertEqual(networkError, error)
XCTAssertNil(assetList)
}
Here you:
- Create a mock error of type
AppError
, as it’s the error type in this project. - Then create a failure result with
networkError
and pass it to theMockNetworkService
. That’s the benefit of mocking dependencies: you have complete control over the response. - Fetch the assets from
MockNetworkService
. Test if you receive the same error back as you provided toMockNetworkService
.
Good job! Run your unit test by pressing Command-U. Green is the warmest color. :]
Where to Go From Here?
You can download the completed project by clicking Download Materials at the top or bottom of the tutorial.
In this tutorial, you learned about Dependency Injection using Resolver. You can learn more about Resolver in the official GitHub repository.
You may also want to read more about using Resolver scopes.
To learn more about Dependency Injection in Swift, check out our awesome Dependency Injection Tutorial for iOS: Getting Started.
You can also read more about calling types as functions in our What’s New in Swift 5.2 article.
If you have any questions or comments, join the forum discussion below.