Modern Dependency Injection in Swift

Tips, techniques, and strategies for getting the most from your dependency-injection system in iOS applications

Michael Long
Dec 4, 2019 · 13 min read

MVVM. MVP. VIPER. Clean. In modern iOS applications, we often use a specific architecture or design pattern to break our software down into individual components that are easy to read and understand and that are easy to test.

But breaking applications down into individual components creates its own problem. Just how do we put our application back together again?

How does ViewController x find out about ViewModel m, and, for that matter, just how does ViewModel m get everything that it needs to do its job?

There are several possible solutions to that problem, but today we’re going to look into some advanced techniques based on dependency injection and using a dedicated dependency-injection system.

You may already be doing this in some way, shape, or form, but in this article, we’re going to dive into some of the techniques I wish I’d known before I started using a dependency-injection system in my first application.

We’ll demonstrate those techniques using Resolver, a modern dependency-injection system. But don’t worry. The tips and strategies we’re going to discuss can be used with other DI systems — from Dip to Swinject.

This article is not meant to be a complete guide to dependency injection or dependency-injection systems. If you’ve never used one before or if you just want to brush up on the basics, you might want to read “A Gentle Introduction to Dependency Injection” before proceeding further.

Additional resources are listed at the end of the article.

Inject Services, Not Data

One of the first things to get straight is that dependency injection’s sole concern lies in constructing the service graph. To put that into English, it means the dependency-injection system creates and connects the services and objects that manage the application’s data.

It doesn’t inject nor does it manage the data itself

The distinction is subtle but important. Take another look at our initial object graph, where the view controller communicates with the view model, the view model communicates with a data repository, and the repository issues requests to the API.

The dependency-injection system’s responsibility is to create the view model, repository instance, and the API instance, and then wire up those components and connect them to the view controller.

Once that process has been accomplished the dependency-injection system’s job is done, and those components are then free to pass messages and data back and forth among themselves.

Let’s look at an example.

User-list example

Say we’re presenting a list of users. The view controller gets its data from the view model and presents that data in a table view. That user list comes from a repository that owns the user data. The repository, in turn, communicates with the API to request and update the data.

Our UserListViewController, however, doesn’t care about what’s going on behind the scenes. It simply asks the injection system for a UserListViewModel, which the injection system creates and delivers ready to use.

class UserListViewController: UIViewController {    @Injected private var viewModel: UserListViewModel    override func viewDidLoad() {
super.viewDidLoad()
setupViewModelSubscriptions()
viewModel.loadData()
}
func setupViewModelSubscriptions() {
...
}
}

User-details example

Now, let’s say a user has been selected from the list, and we want to see some details. In this case the user data is passed to the our new view controller in prepareForSegue.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let vc = segue.destination as? UserDetailsViewController {
vc.user = viewModel.selectedUser()
}
}

But our new view controller also needs its own view model and its own access to the shared repository and API layer. Guess what? Those services are also provided by the dependency-injection system.

That gives us an object graph similar to the following.

Which means the relevant code for our UserDetailsViewController might look something like this:

class UserDetailsViewController: UIViewController {    var user: User?    @Injected private var viewModel: UserDetailsViewModel    override func viewDidLoad() {
super.viewDidLoad()
setupViewModelSubscriptions()
viewModel.loadDetails(for: user)
}
func setupViewModelSubscriptions() {
...
}
}

Note that our user data is passed into our details view controller by the previous view controller, but our UserDetailsViewModel is created and injected by the dependency-injection system.

The UserDetailsViewModel itself only learns about the selected user when asked to load that information in the call to viewModel.loadDetails(for: user).

As a general rule, you wouldn’t register the selected user with the dependency-injection system in the UserListViewController and then inject that user in the UserDetailsViewModel.

Inject services, not data.

Once you understand this distinction, the roles of the various objects and services in your application will be much clearer, and you’ll be able to use your dependency-injection system to its full advantage.

Annotation

There are five primary ways of performing dependency injection:

  1. Interface injection
  2. Property injection
  3. Constructor injection
  4. Method injection
  5. Service locator

The names and numbers come from the inversion-of-control design pattern. For a more thorough discussion, see the classic article by Martin Fowler.

Constructor injection is the most common technique used when implementing dependency-injection containers, but that can lead to a lot of nasty, boilerplate initialization code in complex view models:

class MyViewModel {    var userStateMachine: UserStateMachine
var keyValueStore: KeyValueStore
var bundle: BundleProviding
var touchIdService: TouchIDManaging
var status: SystemStatusProviding?
init(userStateMachine: UserStateMachine,
bundle: BundleProviding,
touchID: TouchIDManaging,
status: SystemStatusProviding?,
keyValueStore: KeyValueStore) {
self.userStateMachine = userStateMachine
self.bundle = bundle
self.touchIdService = touchID
self.status = status
self.keyValueStore = keyValueStore
}
...
}

… as well as in our registration functions:

register { 
MyViewModel(
userStateMachine: resolve(),
bundle: resolve(),
touchID: resolve(),
status: resolve()?,
keyValueStore: resolve()
)
}

To alleviate this, the latest version of Resolver adds a sixth method, made possible by Swift 5.1

Note: This article assumes you understand the object-registration process and that the automatic type-inference behavior shown in the above registration function makes sense. If not, then please read “A Gentle Introduction to Dependency Injection” before proceeding further.

Method 6: Annotation

Annotation uses comments or other metadata to indicate dependency injection is required. Annotation is common in tools like Dagger 2 on Android, and as of Swift 5.1, we can now perform our own version of annotation on iOS using property wrappers.

With annotation property wrappers, the above code for MyViewModel can now be written in a much cleaner fashion:

class MyViewModel {
@Injected var userStateMachine: UserStateMachine
@Injected var keyValueStore: KeyValueStore
@Injected var bundle: BundleProviding
@Injected var touchIdService: TouchIDManaging
@Injected var status: SystemStatusProviding?
...
}

Our initialization function and all of its associated boilerplate code is gone, and our registration function is down to a single line as well.

register { MyViewModel() }

If this looks familiar, it’s probably because we’ve been using the annotation pattern in our examples.

class UserDetailsViewController: UIViewController {
@Injected private var viewModel: UserDetailsViewModel
}

The @Injected property wrapper is specific to Resolver, but one could easily make such a wrapper around any DI system. For a deeper look on how this is accomplished, read my article “Swift 5.1 Takes Dependency Injection to the Next Level.”

Also note that unlike SwiftUI and Combine, property wrappers don’t require iOS 13 and, as such, are safe to use in earlier versions of iOS.

Scopes

Scopes are used to control the life cycle of a given object instance. They’re extremely powerful tools to have in your dependency-injection toolkit, and Resolver has five of them built-in:

  • Application
  • Cached
  • Graph (default)
  • Shared
  • Unique

All scopes, with the exception of unique, are basically caches, and those caches are used to keep track of the objects they create.

How long? Well, that depends on the scope.

Application scope

Creating an object in the application scope, for example, effectively creates a singleton.

The first time the dependency-injection system resolves an object in the application scope, it’s returned but a reference to that object is also saved in the cache. The next time we try to resolve that same object signature, a reference to the original object is returned instead.

During object registration in Resolver, you indicate that created objects should be placed into a specific scope using the scope builder property.

register { MyWebService() }.scope(application)

You could accomplish this using a static shared variable on a class, but that opens up an entire can of worms including violating the single responsibility principle, making your code dependent on global state, tightly coupling your code to the static shared variable, and so on.

For reasons on why creating and using singletons yourself is a bad idea, read “Singleton Pattern Pitfalls” by Vojtech Ruzicka.

With annotated dependency injection, however, you simply mark the objects you want injected, and the system handles the rest.

class UserDetailsViewModel {
@Injected private var service: MyWebService
}

In this case, the view model using MyWebService doesn’t know it’s a singleton, nor should it even care. From its perspective, MyWebService isn’t different from any other service it requires, and it obtains it in just the same way.

Enforcing singletons in dependency injection

If for some reason you need more than singleton behavior and want to absolutely ensure no one else can create an instance of MyWebService, then just add the following to your MyWebService.swift file:

public struct MyWebService {
fileprivate init() { }
...
}
extension Resolver {
static func registerMyWebService() {
register { MyWebService() }.scope(application)
}
}

Call registerMyWebService from within your application’s registerAllServices function that you created for Resolver. Now the only way for your application to obtain access to your singleton is to request it from the injection system, and the injection system will ensure one and only one copy is created and returned.

Cached scope

Like the application scope, this scope stores a strong reference to the resolved instance. Once created, every subsequent call to resolve that object type will return the same instance.

register { MyViewModel() }.scope(cached)

Unlike the application scope, cached scopes can be reset, releasing their cached objects. Once reset, we’re basically starting over again from zero, and any subsequent resolution requests will create a new instance that’ll again be cached.

Resolver.cached.reset()

You can also add your own custom caches to Resolver and use them just as if they were built into the system.

Resolver.session = ResolverScopeCache()
register { MySessionManager() }.scope(session)

This is extremely useful if you need, say, a session-level scope that caches specific information up until a user logs out.

Unique scope

I mentioned above that the unique scope is, well, unique — and it is. Resolve an unique dependency, and Resolver will create a new instance of the requested object each and every time.

register { MyViewModel() }.scope(unique)

Oddly enough, simply making a new object each and every time isn’t the default scope.

Graph scope

graph is the default scope, and it’s what occurs when no additional scope is specified during object registration. In DI speak, graph will reuse any object instances resolved during a given resolution cycle.

Translating again, let’s say that a depends on b and c and that b and c both depend on d. We then do the following:

@Injected private var a: A

Resolver will attempt to resolve a, and in the process, it finds out that a needs a b and that b needs a d. It makes one of each and wires them together.

Resolver then finds a also needs a c and that c also needs a d. Well, Resolver knows it’s already made a d, so it gives a copy of the existing reference to c. Now a has its b and c, and both of those refer to the same instance of d. Finally, we return our a, ready to use.

In this case, graph depends on the idea that you probably want b and c to share d since they were all made during the same resolution cycle and, as such, would seem to be associated with one another.

If you don’t want this behavior, you can just mark d’s dependency as unique. And if you never want this behavior, you can simply change Resolver’s default scope to unique.

Resolver.defaultScope = Resolver.unique

Now on to another one of our power tools.

Shared scope

This scope stores a weak reference to the resolved instance.

register { MyViewModel() }.scope(shared)

While a strong reference to the resolved instance exists, any subsequent calls to resolve it will return the same instance.

However, once all of the strong references are released, the cached instance is also released, and the next call to resolve a service will produce a new instance.

This is useful in cases like master/detail view controllers, where it’s possible that both the MasterViewController and the DetailViewController want to share the same instance of a specific view model or, perhaps, their view models need to share some common piece of state.

In fact, we saw this in our user-list/user-details example above, where both view models needed access to the information in the current user repository.

… and where the repository and associated API are registered like so:

register { UserRepository() as UserRepositoryType }.scope(shared)
register { UserService() as UserServiceType }

So when our details view controller is pushed and when its view model requests an UserRepositiory, it gets a reference to the repository that was already created and supplied to the list view controller’s view model.

This allows both view models to share the same repository and, as such, share the same synchronized state and data.

Now take another look at the above registration code. The .scope(shared) method should make sense by now, but as type additions may look a little strange. Why register an object as a different type?

It’s a good question, and the answer to it brings us to our next topic.

Protocol-Oriented Programming

Protocols define the methods and properties that suit a particular task or piece of functionality. With it, we define our interface to those features.

Implement an object that implements the interface, and you can pass that object to any other object that wants to use that interface. And passing objects around is the heart and soul of dependency injection.

Mocking UserRepository

One of the big things we can do with protocols is use them to mock data for test purposes. In our first example, we showed a UserListViewController that depends upon a UserListViewModel. That view model, in turn, relies on UserRepository to actually manage its data.

class UserListViewModel {
@Injected var repository: UserRepositoryType
...
}

Note, however, that our variable type is actually UserRepositoryType, a protocol that looks like this.

protocol UserRepositoryType {
var users: CurrentValueSubject<[User], Never> { get }
var error: CurrentValueSubject<String?, Never> { get }
func load()
func save(user: User)
}

Our standard UserRepository uses the API layer to handle data requests, but with our protocol we could also create a mock version to return test data:

class MockUserRepository: UserRepositoryType {
let users = CurrentValueSubject<[User], Never>([])
let error = CurrentValueSubject<String?, Never>(nil)
func load() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
self.users.send([User(name: "Michael Long")])
}
}
func save(user: User) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
var list = self.users.value
if let index = list.firstIndex(where: {$0.id == user.id}) {
list[index] = user
} else {
list.append(user)
}
self.users.send(list)
}
}
}

Note that we’re using a Combine publisher to notify when we have new data or errors and how we even use a delayed dispatch queue to simulate a delay in our data requests.

So we have our protocol, our mock data, and we also have an object that wants to use it. How do we use dependency injection to wire things together?

Injecting mock data

There are, in fact, many ways to swap mock objects for real objects using Resolver when doing data mocking for running your application and for doing unit testing on view models and other objects. Here’s one of the simplest.

extension Resolver: ResolverRegistering {
static public func registerAllServices() {
#if MOCK
register { MockUserRepository() as UserRepositoryType }
.scope(shared)
#else
register { UserRepository() as UserRepositoryType }
.scope(shared)
#endif
}
}

In our registration file, we’ll check a compiler variable that’s controlled by the current Xcode scheme. If MOCK is true, we register our MockUserRepository as UserRepositoryType. If false, we register UserRepository as UserRepositoryType.

When we build the app, the correct registration code is compiled, and when the app is run, our UserViewModel will get the corresponding repository.

Mocking with named services

Let’s try another approach. Consider the following set of service registrations.

register { 
let mode = Bundle.main.infoDictionary!["mode"] as! String
return resolve(name: mode) as UserRepositoryType
}
register(name: "data") {
UserRepository() as UserRepositoryType
}
register(name: "mock") {
MockUserRepository() as UserRepositoryType
}

We’ve registered XYZServiceType three times: once without a name, once with the name data, and then once again with the name mock.

Let’s see it in use by a client.

@Injected var service: XYZServiceType

The client just asks Resolver for a service of XYZServiceType. Since no name was specified, Resolver will attempt to resolve the service using the unnamed service registration.

That registration function gets a string from the app’s info.plist and then recursively asks Resolver to resolve another instance with the same type but also with the retrieved name.

So,depending on how the app is compiled and how the mode value is set in the app’s plist, one build will get actual data while the other build will get mock data.

As long as both services comply with the XYZServiceType protocol, the client doesn’t care.

Nor should it.

Completion Block

Dependency injection is a powerful tool to have in your software-development toolbox, and I hope this article has illustrated just a few of the ways you can begin using it successfully in your own applications.

There’s much more to it, of course, and one of the big ones is how dependency injection can help you simplify not just your application code but your unit- and integration-testing code as well.

But that’s another article.

Additional Resources

Better Programming

Advice for programmers.

Michael Long

Written by

Michael Long is a Senior Lead iOS engineer at CRi Solutions, a leader in cutting edge iOS, Android, and mobile corporate and financial applications.

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade