Building a Flexible Dependency Injection Library for Swift

TribalScale Inc.
TribalScale
Published in
10 min readSep 14, 2022

Written by: Daniel Carmo, Agile Software Engineer, TribalScale

📫 Subscribe to receive our content here.

💬 Have any questions about our development capabilities? Click here to chat with one of our experts!

Photo by Luca Bravo on Unsplash

Dependency Injection is a programming concept that improves testability and allows for easy decoupling of classes. It achieves this by requiring the dependencies of a given class to be passed into it before they are used. Along with the DRY rule, Dependency Injection is something that I recommend to programmers to always follow due to these added benefits to our code:

  1. Improves testability by allowing injection of Mock classes
  2. Implicit with the dependencies required by a class giving information about what it may do
  3. Better modularization of the code, creating classes to be injected into others for use
  4. Combining with protocols, helps with decoupling of classes by using protocols in front of the dependencies

If we look toward our Design Principles, we can see that this is a type of Inversion of Control (IoC) where we are offloading control to another flow in order to achieve our result. The additional flow could be a library, framework, or common code. Following the IoC principle allows us to make our program more modular and improves the reusability of it.

Dependency Injection can be done in a number of ways. You can inject the dependencies during initialization, in the properties, or within the parameters of a method. Initializer injection places dependencies into the initializer of the class. This helps to display what dependencies the class has at the time that we use it, which gives insight into what the class may be doing. Property Injection has the dependencies injected after class creation by setting the properties directly. This could be a bit more risky if a dependency is forgotten. Method Injection has the dependencies passed at the time of calling a method. The dependency can then be used in the method directly and gives more flexibility at the time of calling a method.

The Basic Container

Let’s now take a look at how Dependency Injection is used and create a library together that takes minimal code but is flexible.

We’ll start by defining our networking layer.

This networking layer will be used in our example for injection from the library. We have a protocol in front of our networking layer to ensure that it can easily be swapped out in the future if anything to the underlying networking needs to change or using a mock for unit testing.

For our library, we’ll need a container to hold all of our dependencies. We’re going to start with a basic container and build up additional flexibility to it as we go.

  1. First we create a protocol to define our dependency container.
  2. Create a concrete container class
  3. Implement the protocol definition for our DIContainer

This basic container has a method for resolving our dependencies and for retrieving them. First we need to register the dependency to the container for the type. Once that is done then we can use the container to resolve the dependency in the initializer. This could be done during the ApplicationDelegate to ensure that the dependencies are available throughout the entire application lifecycle.

  1. Register our dependency
  2. Inject the dependency into the class that is using it

This is a very basic container and usage and you could use this small amount of code to improve your Dependency Injection in your application. This is an example of Initializer Injection, where we are using the initializer to inject the dependency into the class.

Flexibility Add #1 — Inject propertyWrapper

Swift 5.1 allowed us to create PropertyWrappers. This can give us a more compact way to perform property injection. Let’s expand our solution with this style of implementation.

  1. Define the struct as a propertyWrapper
  2. Create the struct definition that takes in a generic Value type
  3. Retrieve the value from the DependencyContainer
  1. Annotate the property with the propertyWrapper

Implementing this property wrapper allows us to inject the dependency by simply annotating our property with the @Inject propertyWrapper. This changes the type of injection that we are performing to property injection. It also condensed our solution from needing to type out the singleton usage always. This also helps the readability of our code to show that that property is injected to be used by the class.

Flexibility Add #2 — String Key Registration

What happens if we wanted to register multiple classes of the same type. With the current implementation based on the type we wouldn’t be able to do that. We can expand our implementation by allowing registering and resolving using a key for the property.

  1. Add a method to register a dependency for a key
  2. Added a method to resolve the dependency for a key
  3. Move the code for registering a dependency by type to the by key method to make registration run through a single method
  4. Move the code for resolving a dependency by type to the by key method to make resolving run through a single method
  5. Pass in the key by the initializer. Default it to nil to make it not required and cleaner if you are just using the type and no keys
  6. Check if the key exists and resolve the dependency based on the key or type
  1. Register the dependency using the key
  2. The networkLayer is being injected based on the type and doesn’t require the key to inject
  3. Retrieve the dependency using the propertyWrapper and key

We’ve added additional methods to the container and @Injected propertyWrapper to allow for the use of a String key to register and resolve the dependencies. This simple addition has allowed us to register multiple types of the same class (or protocol) by using a string key to represent the dependency in the container.

Flexibility Add #3 — Dependency from Closure

We can cover more use cases by adding in the ability to inject dependencies using closures. These closures allow you to lay some groundwork for resolving a dependency. Maybe you’d like to resolve a dependency from a UserCache, this would allow us to do it. Let’s explore what that looks like.

  1. The definition for the DIContainer to take in closures for the register methods
  2. DependencyContainer now stores the dependencies as closures
  3. Register methods are now taking in the closures and storing them as they did before
  4. Resolving the dependency now requires us to check for the existence of the key, run the closure, and ensure the type resolves properly.

Note: There has been no changes to how the @Inject propertyWrapper is implemented.

  1. Registering the dependencies now takes in a closure
  2. An example could be retrieving the user from a cache when resolving a user dependency

Note: There has been no changes to how the @Inject propertyWrapper is used.

The change is subtle here, but we’ve just updated the register and resolve methods to take in a closure and updated the storage in the DependencyContainer to be a closure type. We’ve added an additional register for a User type in the usage file that shows a user being retrieved from the cache whenever this dependency is resolved.

Flexibility Add #4 — New or Shared Dependency

There is a slight side effect from adding the closure resolution to our dependencies is that every time we ask the container to resolve a dependency, it will call the closure and return a new object every time. This could work for some dependencies but maybe not for others, so let’s add in the ability to request a shared version or new version of the dependency at resolution time.

  1. Add in a new enum type for how to resolve the dependency
  2. Update the resolve methods to take this mode in to decide how to resolve the dependency
  3. We need to keep the closures in memory to resolve new dependencies
  4. We also need to store the shared dependencies in the case of resolving a shared mode
  5. Switch on the mode to determine how to resolve the dependency
  6. When resolving a new dependency look to the closure and run it to get the dependency to return
  7. When resolving a shared dependency, check if the dependency exists in the shared dictionary, and only generate it at the time it’s needed
  8. Resolve the dependency from the shared dictionary and return it.
  9. Ask for the ResolveMode when initializing the @Inject propertyWrapper, but default to shared so it’s not always needed
  10. Use the mode to resolve the dependency
  1. We now pass in the mode as new when resolving the user since it’s coming from the cache it would be helpful to always get it from there.

This enhancement has now allowed us to ask the library for either a new instance of a dependency or a shared one between other classes. Certain dependencies may benefit from one or the other mode and with this change we now have that flexibility.

Flexibility Add #5 — Multiple Containers

Right now the library is currently locked down to a shared container. Being able to create multiple containers can add additional flexibility to our library. This would allow you to create different containers for managing different dependencies through your application.

  1. Add a container to the @Inject propertyWrapper for customization. Default this to the shared container.
  1. Pass the container to the @Inject propertyWrapper to be able to retrieve from a different container.

With this in place, we can now have multiple containers to use for resolving dependencies.

Flexibility Add #6 — LazyInject

Let’s say that you don’t want a dependency to resolve right away. You’d rather it wait until it is used (if it is ever used at all). That’s where this additional propertyWrapper comes into play. An instance where this might happen is if 2 dependencies rely on each other. If you use the standard @Inject, you’ll run into a race condition where one dependency requires the other but it hasn’t been registered yet. This LazyInject can resolve that for us by deferring the initialization until the value is used.

  1. We need to store these properties in order to perform the creation of the wrappedProperty when it is requested by the user
  2. Resolve the dependency at the time that the property is accessed
  3. Initiate the @LazyInject with the properties needed to resolve the dependency
  1. There are no changes to how the registration is done
  2. We register the AppNetworkingLayer in the UserCache using @LazyInject
  3. We register the UserCache in the AppNetworkingLayer using @LazyInject
  4. We use the standard @Inject on the ViewControllerInjected since resolving this dependency is now protected from circular referencing with the @LazyInject within the AppNetworkingLayer

This initializer allows us to lazy initialize a property of a dependency. Since the dependencies are declared as @LazyInject the injection will be deferred until the object is used. This allows time for the dependency to be registered before it’s attempted to be injected and it prevents recursion of the injecting of properties.

Flexibility Add #7 — WeakInject

The final improvement to our dependency injection library for this blog is to add the ability to create weakly referenced dependencies. These will be helpful to ensure that we aren’t causing any retain cycles with our dependencies.

  1. Notice that we haven’t constrained the Value generic parameter to AnyObject. Doing this causes us not to be able to declare a variable as a protocol even with an AnyObject constraint on the protocol.
  2. We save the value from the DIContainer into an underlyingValue variable that is our weak reference
  3. Compute the wrappedValue to be the underlyingValue casted to the Value return type
  4. When retrieving the value from the DIContainer, we need to cast it to an AnyObject.
  1. We use this property the same as the others, except that we now have the value as an optional.

This weak reference will allow us to remove/clear out the container and cause the references within other classes to fall out of memory as well. This covers the final addition to our dependency library. A Note with the WeakInject propertyWrapper is that you either need to always use the shared resolve mode, or if you are using the new resolve mode then you need to keep a strong reference yourself to the object (e.g. using the UserCache as we did before).

Unit Testing

Now that our library is complete, we will explore how we can use this solution to be able to test our application by using mock objects. It’s quite simple for us to be able to use our DependencyContainer and propertyWrappers to be able to test existing classes. All that is required is to inject your mocks into the container instead of the live object.

  1. Register the dependency for an existing type to resolve to your mock
  2. Remove the dependency from the container

Now any of our propertyWrappers will resolve the mock dependencies that we have added. That’s all that’s required for our tests to use our mocks.

Wrap up

In this blog, we talk about dependency injection and its benefits to use in our day to day coding. We then explored creating a flexible DependencyContainer in Swift using a relatively small amount of code. We also took a look at using propertyWrappers to enhance our implementation and add additional propertyWrappers for injecting the properties using lazy and weak implementations. These will help us to align with the different ways that we will use variables in our day to day. The final implementation can be found at the end of the article.

Have any other flexibility suggestions, comments, or improvements then let me know and we can explore those as well! Next up we’ll convert our library into a Swift Package to be used with Swift Package Manager in our projects. Can’t wait to use this as a Swift Package, then check it out here as FlexInject.

Daniel is an Agile Software Engineer with experience on a variety of platforms in both development and design. He also enjoys collaborative projects with clients, leading developer projects, and mentoring junior and intermediate developers on everything from code quality to programming knowledge. When he’s not coding, his hobbies include baking, running agility with his dog, raising chickens, and gaming.

TribalScale is a global innovation firm that helps enterprises adapt and thrive in the digital era. We transform teams and processes, build best-in-class digital products, and create disruptive startups. Learn more about us on our website. Connect with us on Twitter, LinkedIn & Facebook!

--

--

TribalScale Inc.
TribalScale

A digital innovation firm with a mission to right the future.