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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

Constructor, method and property injection -- oh my!

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.

Yoda

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:

  1. Extend Resolver and conform to ResolverRegistering, which forces you to implement registerAllServices().
  2. Register URLSession with a default configuration.
  3. Register NetworkService, URLComponentsService and AssetService.

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

Reward for successfully running the app

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.

Cryptic app main dependency flow

  • The resolution cycle starts when AssetListView asks Resolver to resolve assetListViewModel.
  • To resolve assetListViewModel, Resolver has to resolve AssetService, URLComponentsService and NetworkService.
  • 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. :]

Hero enjoying your work