iOS App Development
Easy Dependency Injection With Property Wrappers in Swift
Another approach to DI in Swift 5.1+ with Property Wrappers.
--
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:
Your custom classes, such as
networkManager
,FileManager
andAnalyticsManager
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:
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:
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:
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:
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 typeT
; init
gets one parameter:keyPath
to the variable inInjectionResolver
;wrappedValue
gets a value from theInjectionResolver
's variable.
ImageDownloader
Now, we’ll add a method downloadImage
to our ImageDownloader
:
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
If you want to learn more about SwiftUI, check my series of tutorials: