on
Swinject in practice
I guess you already have heard of Dependency Injection. Dependency injection (DI) is a software design pattern that implements Inversion of Control for resolving dependencies.
On iOS, one of the popular frameworks you can use for Dependency injection is Swinject.
Today we will quickly cover the basics of using Swinject in your app in order to focus on two edge cases you may have been confronted to: custom object scopes and domain specific assemblies.
Let’s dive in.
The basics
Note: I invite you to read the detailed documentation if you want to fully understand what follows.
Swinject let you split your dependencies into logic related objects, called assemblies.
For instance, let’s create an HelperAssembly
that registers few helpers dependencies, that we will need in our project.
Swinject, like any DI framework, works like a key value store (named container here): the keys are types (abstract with protocols or concrete with classes or structs) and the values instances of those types.
Once low level dependencies are registered, we can start using them into higher level dependencies.
Let’s say we need to fetch a list of the user last viewed products in our app. We create an interface (a protocol in Swift) and a concrete implementation of this interface.
With Swinject, we would register our repository like this:
The LastViewedProductsRepository
uses an instance of UserDefaults
to save the last viewed products in the application. This dependency will be resolved at runtime using the HelperAssembly
.
Note that we use the object scope container
here to create a single instance of LastViewedProductsRepositoryImplementation
in the whole application. There are several scopes available explained in more details here.
At the end, we gather all the assemblies into an assembler and use it to create our dependencies.
Custom object scopes
We have seen earlier that we have a LastViewedProductsRepository
object, responsible for keeping a cache of the last viewed products in the app.
For now, because we used the container
scope in the assembly, there is a single instance of this class in the whole application. And sadly enough, this instance is the same even if we log out the user, then log in with another id.
That’s a bug…
We want to erase all the last viewed products when the current user logs out.
To overcome this we have a few options:
- emit a notification when the user logs out and listen to this notification in the repository
- provide a public method to clean the products (for instance
cleanCache
) in theLastViewedProductsRepository
and call it from the outside when the user logs out.
Theses two options are fine, but not very scalable. We want to deport this logic into the DI, meaning creating a scope for the object that:
- keep the instance alive while the user is logged in
- trash the instance when the user logs out
- create a new instance when a new user logs in.
That’s where custom object scopes shine.
Let’s create a new object scope named discardedWhenLogout
:
We can use it in our assembly instead of the container
scope:
Now we can use the Swinject method resetObjectScope(_ objectScope: ObjectScope)
on a container
. This method discards all the instances registered in the given object scope. That means all the instances will be re-created once they are needed again.
Now the next time a user logs in, all the last viewed products will be cleared.
Domain specific assemblies
In some cases, there are parts of your application that you can access only if some values are downloaded, or set somewhere else in the app. In our case, let’s imagine there is a configuration file downloaded when the user logs in and that this configuration is used intensively in the rest of the app.
This configuration file is mapped into a LoginConfiguration
object, which is just a struct with a bunch of properties.
In the last viewed products page of our application, the number of products displayed is constrained with the value maxProducts
of the configuration. We show a class of LastViewedProductsPresenter
here, keep in mind that a presenter is a just like a viewController but not tied to UIKit.
The problem here is that we guard against the loginConfiguration
even though the configuration must be available in this case, because the user must be logged in to access this page.
But the loginConfigurationRepository
returns an optional (indeed, the configuration does not exist before the user is logged in). But if we think about it, it’s not the right solution. Instead of initializing the presenter object with a repository, we should initialize it with the loginConfiguration
directly, because we are sure at this point that such a value exists.
The LastViewedProductsPresenterImplementation
has two dependencies, so our PresenterAssembly
looks like this:
What we want is to pass the loginConfiguration
to the presenter at init time, instead of the loginConfigurationRepository
. That means we need to store the loginConfiguration
in the assembly itself to resolve the dependency.
Let’s define a new assembly called LoggedInPresenterAssembly
. This assembly will register types that will be used only once the user is logged in.
As the user will be logged in, we can assume the login configuration is fetched and available in the assembly:
The only thing to do now is to create the LoggedInPresenterAssembly
when the user logs in and that the configuration is fetched, and to give it to the assembler.
That way the presenter now becomes:
Wrap up
Dependency injection is a major concept in programming in order to make your code reusable and testable. The concepts explained in this blog post can apply independently of the framework and the platform.
In particular, we have seen two advanced use cases:
- custom object scopes to customize the lifetime of your dependencies
- domain specific assemblies to create dependencies that depend on some external requirements