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.

Leave a rating/review
Download materials
Save for later
Share

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.
Note: This tutorial assumes you’re comfortable with using Xcode to develop iOS apps and familiar with SwiftUI basics like those in SwiftUI Tutorial: Getting Started.

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

Cryptic home screen with live cryptocurrency market data

Note: The starter project includes the Resolver package to the project using Swift Package Manager. When you open the project, Xcode updates all the package dependencies. If for some reason it doesn’t, go to File ▸ Swift Packages and select Resolve Package Versions.

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.

Roadmap to move from Tightly Coupled to Loosely Coupled classes.

Roadmap for refactoring tightly coupled classes. Using the principle of of Inversion of Control you use Dependency Injection to achieve loosely coupled classes.

Roadmap to move from Tightly Coupled to Loosely Coupled classes.

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.

Design principles and design patterns

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:

Dependency flow for the Cryptic app

  • AssetListView is the project’s main view. It’s dependent on AssetViewModel.
  • AssetViewModel needs AssetService.
  • AssetService is dependent on URLComponentsService and NetworkService to fetch the assets.
  • And finally, NetworkService is dependent on URLSession.

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:

  1. Remove the URLSession constructor and make the session mutable.
  2. Create an initializer for NetworkService and receive the session as a dependency.
  3. 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. :]

Note: In this tutorial, none of the changes you make to the project will affect the UI of the app, so screenshots won’t be added after every build and run.

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

Resolver to the rescue