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. :]