iOS App Development

Easy Dependency Injection With Property Wrappers in Swift

Another approach to DI in Swift 5.1+ with Property Wrappers.

Alex Zarr
The Startup

--

Photo by Alex Knight on Unsplash

If you’re a developer, you probably know that Dependency Injection is required in any decent project. You can’t live without it, since it helps you make your project testable, helps to loosen tightening between your classes and brings many other advantages.

But also, you might know DI forces you to write an extra code and init methods take a ridiculously huge number of parameters. Imagine you need to create ImageDownloader that has dependencies on NetworkManager (to download an image), FileManager (to cache the image), NotificationCenter (to post a notification when the image is downloaded) and AnalyticsManager (to log if there is any error). In this case, you’re going to have something like this:

Conventional way to DI

Your custom classes, such as networkManager, FileManager and AnalyticsManager are hidden behind protocols, so you could mock them in your unit tests.

Even though we all are probably used to writing that code, it’s a little bit too much.

Long story short, there is an approach that may help you make your life easier! Thanks Property wrappers, a great feature in Swift (5.1+) that comes in handy here.

If you’re interested in that, here is how your code can look like:

DI with a property wrapper

You don’t need to put all these dependencies into the init method of your class. And you don’t even need to specify the type for each variable. And yet, your code is still testable.

So, how do we do that? Let’s write some code to implement that approach.

InjectionResolver

Firstly, we need a resolver which responsibility will be to provide a proper dependency to a property. Here is how it’s going to look like:

InjectionResolver

If you want to check how it works, you need to create and implement these classes and protocols. Or you can simply use my code:

Simple classes for DI

We will keep these classes very simple. Now, let’s implement a property wrapper.

Property Wrapper

Create a new Swift file called Injected.swift. Here is the code:

Injected.swift

If you’re not familiar with Property Wrappers in Swift 5.1, it’s not a big deal:

  • We created a struct;
  • Added @propertyWrapper before its declaration;
  • Every Property Wrapper has to have wrappedValue. In our case, it has a generic type T;
  • init gets one parameter: keyPath to the variable in InjectionResolver;
  • wrappedValue gets a value from the InjectionResolver's variable.

ImageDownloader

Now, we’ll add a method downloadImage to our ImageDownloader:

ImageDownloader with downloadImage()

It’s time to check if it works. Add this code somewhere in your project:

When you run this code, you’ll get the following console output:

Will init ImageDownloader()NetworkManager initFileManager initAnalyticsManager initWill call downloadImage()real fetchwrite filelogged: Image is downloadedDid call downloadImage()

As you can see, the instances of NetworkManager, FileManager and AnalyticsManager are not initialized until you create an instance of ImageDownloader(). This happens because your properties in InjectionResolver are lazily loaded. As a result, they’re initiated only when they are used for the first time.

Once you’ve called downloadImage(), you got three messages from your dependencies: real fetch, write file, logged: Image is downloaded. But what does happen if you want to test this class? You don’t really want to rely, for example, on your network connection. So, instead of NetworkManager, you’d use NetworkManagerMock that mimics the NetworkManager's behavior.

Add InjectionResolver.shared.networkManager = NetworkManagerMock() before the initialization of ImageDownloader:

If you run this code again, you’ll get the following output:

NetworkManagerMock initWill init ImageDownloader()FileManager initAnalyticsManager initWill call downloadImage()mock fetchwrite filelogged: Image is downloadedDid call downloadImage()

As you can see, now ImageDownloader injects NetworkManagerMock. And instead of real fetch, we see mock fetch.

You can replace all the necessary dependencies with mocks before testing a class (or struct, or anything else) in your app.

Now, you can test your app in the same way as you’re used to, but you don’t need huge init methods for your classes.

It might be concerning that one class (InjectionResolver) is linked to all the systems of your app. On the other hand, it initializes only necessary objects, and this behavior is basically the same as with singletons. But in this implementation, your singletons can be easily replaced with mock classes and all your code will remain testable.

There is another good implementation of DI with Property Wrappers written by Ilario Salatino that might be interesting to you.

The idea with keyPaths is taken from the SwiftUI’s Property Wrapper @Environment that allow you to inject Environmental values in SwiftUI views:

@Environment(\.managedObjectContext) private var managedObjectContext

--

--