Dependency injection in Swift

Arturs Derkintis
5 min readMar 28, 2022

--

Over the past few months I’ve been trying to find ways to make testable code easier to write. I’ve become a strong believer in clean and tested code and well designed dependency injection is essential part to be able to write tests with ease.

First let’s look at couple of well know design patterns that somehow are still wildly used in way too many projects.

Singletons

Singletons in essence are not a bad thing. You got one instance of dependency that can do something useful for your code. It is so easy for any level of dev to pick it up and use to anywhere in the code like there’s no tomorrow.

But unfortunately the tomorrow comes far too often and the same dev now have 75 different bugs on his hand, because he didn’t know about the unintended uses of that singleton. And most of those bugs are in code he didn’t even know existed as the singleton usually is all knowing, all doing entity and interacts with many parts of the app while keeping state in itself.

This is caused by the inevitable side effect of singletons. Singletons are always bloated with all the code devs didn’t know where else to put, making is being responsible for far too much. Also so far in my career I have never seen a code, using singleton somewhere in the mix, being properly tested. I won’t go into how to make singleton-exploiting code easier to test or give any examples of code as I think it is an illegal waste of everyones time.

Note about my beliefs on unit tests.

Some wise man who may have been me once said: Which part of your code are you okay with being broken?

When doing any kind of changes to poorly tested or untested code you will introduce bug you will be completely unaware of. A lot of devs, including me at times, who are ignoring the importance of well designed test are running a risk of destroying the world by accident. And we’ll be held responsible for it by government of space if we manage to survive. It will be hard to explain to them that we set our world on fire because we couldn’t be bothered to test our work.

Manual dependency injection

The second and noticeably better solution is to pass in your dependencies into every object that will need them. This way by using protocols you can mock the dependencies into which ever state you need and test against it accordingly.

To give an example, let’s imagine a very real use case of ViewModel needs to access TrackingService which is responsible for sending events from your app to data people of your team.

First lets create a protocol proxied tracking service that will be mockable later on:

Then for the view model utilizing tracking service we would need something like this:

In unit test then we could easily pass a mocked tracking service to ensure view model did send event to it:

It works well and is much easier (actually possible) to write tests for small units in isolation if compared plain singleton.

However there’s a few problems with this approach as well.

  • Where should TrackingService()main instance should live?
  • If we ever want to remove from a class or remove it in general it is massive hassle to edit each constructor.
  • Same when needing to add it. You either need to add constructor with new dependency or add to existing params which often is a hassle.

But what I have had trouble with the most is this: If you decide your only instance of dependency lives in, for a wild example, AppDelegate you will need to pass it through all the layers in between AppDelegate and class that needs it. It rather is demotivating to say the least and lot of times devs unfamiliar/ignorant about this architecture pattern will likely ignore it and call AppDelegate.shared.trackingService from wherever they please. And you can’t blame them as I used to fall into that trap as well.

So while it is neat on paper in real application with many complex layers it is more of a nightmare to deal with it.

Dependency injection…but usable

This lead me to search for good code architectures that welcome combination of change, tests and lazy devs. One thing that sticked out the most to me was how dependency injection was implemented. It was usually a neat Singleton, of all things, holding all stateless dependencies into on layer. It then was facilitated with mechanism that registers and resolves dependencies. Topped off with actual injection that you can use in any of the objects.

Recently I discovered a great way of making this possible natively without any 3rd party libs in Swift.

First lets talk about features we would like from dependency injection tool:

  1. Has to be easy to add/remove dependencies
  2. Dependencies have to be lazy
  3. Has to be effortless to add dependency to any object within your app by any dev of any skill/laziness level
  4. In order to not pollute one file/class with all the dependencies under the sun, we need a way to neatly split out injections to multiple files by responsibility and feature stacks
  5. It should be one liner at most to mock for tests any dependencies

To start with we’ll write basics to cover first 3 points. We need to create our last singleton to rule them all to store our lazy dependencies.

To be able to use the trackingService dependency we need a PropertyWrapper based inject struct that will know where to grab the dependency based on KeyPath we provide to it.

This should now allow us to use it in our ViewModel by simply adding one line anywhere you want.

And thats it. There’s no more code involved to add/remove dependencies that are lazy and are dumb easy to add to your objects.

Now for our next feature we’ll look at implementing number 4 point of multiple dependency files. Here I introduce an enum for all the source of injections with could have in the app. Following is a somewhat of a pseudo code to slightly exaggerate the amount of injections we could have:

In Inject file we should now add a couple of custom constructors so that our KeyPaths would be exposed from all Injection sources classes. And add switch statement for wrappedValue computed variable to resolve the correct dependency for each case.

To use these dependencies it is still the same @Inject(\.someService) var service without any additional code needed regardless of which source it is coming from.

Now as for testing we need a way to manually replace a dependency from test setup code. We’ll need a helper function to replace any of the keyPath variables with new value.

In unit code we had earlier for manual dependency injections we would just need to call replace(:_) function to setup our tests with observable trackingService.

And thats about it. This now allows dependencies to be added with ease and greatly reduce the amount of excuses devs have on why they don’t want to test their code.

--

--