iOS Dependency Injection Using Swinject
This blog will cover the following things:
- What is Inversion Of Control
- What is Dependency Injection
- What is DI Container
- Basic Usage of Swinject
- Why We Need Swinject
- Object Scopes
- Modularize Dependency Registration Using Assembler
- SwinjectStoryboard
Inversion Of Control (IOC)
In object-oriented programming it’s all about to remove the dependencies from your code.
Dependency Injection
In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. Dependency injection is one way to achieve the Inversion Of Control.
The idea behind this is to have a separate object create the required dependency, and pass it to the client.
Benefits
- Testability
- To achieve Dependency Inversion Principle
DI Container
Dependency Injection Container is an object that knows how to instantiate and configure objects. DI Container is a design pattern to implement Dependency injection. One benefit of using it to resolve Complex dependency
Swinject
Getting Started
We need to test loadData
method of NetworkManager
class. This method uses UrlSession
to fetch data from the server and decode the response in the model. We can’t test this method until we have an internet connection which is not a very good practice. What if we somehow mock the server by injecting mock Urlsession
which return mock data.
As shown in Figure 2 we created MockUrlSession
and MockUrlSessionDataTask
classes that act as a mock server which will return what we told. If you want to deep dive into the network testing strategies you can see this blog.
Come to the main point. We created urlSession
property and injected it through its initializer. Now NetworkManager
is not responsible for creating UrlSession
since we inject it through its initializer what we achieved.
- We removed one unnecessary responsibility of the class to create it’s dependent/external objects. (Goal is to achieve the Single Responsibility Principle).
- We implemented a Dependency Injection by using its Initializer Injection pattern. As said above “dependency injection is a technique whereby one object supplies the dependencies of another object”. Now if any object wants to use this class it needs to provide its dependency.
As shown in Figure 3 we achieved testability using dependency injection
but where is Swinject ?? 😖
Why We Need Swinject
DI Container is a design pattern to implement Dependency injection. Most of the time, you don’t need a Dependency Injection Container to benefit from Dependency Injection. But when you need to manage a lot of different objects with a lot of dependencies, a Dependency Injection Container can be really helpful. Swinject is the implementation of DI Container in Swift.
As shown in Figure 4 we have a client A and B both need DataFetcher
object. We are applying Dependency Injection by passing DataFetcher
dependencies through its initializer. What can we observe by looking into this complex dependencies:
- Duplicate code in Client A and Client B.
- Can we have a central location, whose responsibility is to provide object by creating all its dependencies?
This is where DI container will be helpful. DI container manages the type dependencies of your system. First, you register the types that should be resolved, with their dependencies. Then you use the DI container to get instances of those types whose dependencies are then automatically resolved by the DI container. In Swinject, the Container class represents the DI container.
Before we implement the above code using Swinject
we first need to have a good understanding of how Swinject works, it’s syntax and basic usage. You can also see this link.
Terms
- Service → A protocol defining an interface for a dependent type.
- Component → An actual type of implementing a service.
- Factory → A function or closure instantiating a component/dependent object (In Our case logic of How to create DataFetcher with its dependencies).
- Container → A collection of component instances. (DI Container in which our dependent object register and resolve).
As shown in Figure 5 we performed a number of tasks to resolve our dependency in a central location:
- First, we get the container instance using its constructor, Remember Container class is not a singleton class. So every time you call its constructor you will get the new instance.
- We register
DataFetcher
in a container and how to resolve its dependencies/or it’s creation logic we define in its factory which is a closure. Note:DataFetcher.self
acts as aconcrete Service
calledSelf-registration (Self-binding)
. - Now if a client wants
DataFetcher
instance it calls the resolve method of a container and DI container first check if this type of Service already registered if yes it execute factory closure and after resolving dependencies it returns the freshDataFetcher
object with all the dependencies populated as shown in the console in Figure 5. Note: container instance should be the same when registering and when resolving.
As shown in Figure 6 we register using Fetcher.self
as a service Protocol (abstract type as compared to the previous example) and resolve it using the container. This is a simple example:
As shown in Figure 7 we have two classes that implement the same Fetcher
protocol and the question is how to register the same Service Type Protocol and this is where Named Registration in a DI Container comes to play. As you can see we register two Fetcher with name parameter in which one return FetchFromDatabase
instance and other FetchFromServer
instance. When resolving we do the same thing:
As shown in Figure 8 Now Our Object that needs to be resolved by DI container needs to have some dynamic configuration This is where Registration with Arguments in a DI Container comes into play. The factory closure passed to the register method can take arguments that are passed when the service is resolved. When you register the service, the arguments can be specified after the Resolver parameter. Note: you can pass up to 9 arguments in Swinject.
Now we understand the basics and syntax of Swinject. This is how Figure 4 would look like when using Swinject
As shown in Figure 9 we created sharedContainer
a property in Container
class using extension which will return container singleton object until all registration have been done. We told the container how to create an instance of DataFetcher
in its factory closure of register method.
As shown in Figure 10 we call the resolve method on client class and telling it to create an instance of DataFetcher now container will execute its factory method and after it resolved its dependency it will return DataFetcher instance. As said earlier benefits of DI container is to “resolve Complex dependency” and also we achieved a central location whose responsibility is to create an instance of an object after resolving all its dependencies.
Now every time you call resolve on DataFetcher.self
it will always return a new instance of DataFetcher object as shown in Figure 11. What if we want the same instance this is where Object Scopes comes into play:
Object Scopes
Object scope is a configuration option to determine how an instance provided by a DI container is shared in the system. There are four scopes supported by Swinject
Graph (the default scope) → It always creates a new instance when you call resolve but while resolving graph it shared the instances.
As shown in Figure 12 there is a class A depends on B and C
and C class depends on B
=========== Graph A = A depends on --> (B and (C -> B)) ==============
When client call resolve on A
these are the action performed by the resolver:
- Identify
A
Dependency which isB
andC
. - Resolving
B
, first, checkB
dependency and didn’t find any then createdB
instance(0x0000600002aa69d0) and put in shared container since graph is not resolved yet. - Now resolving C, first check it’s dependency which is B then look into the shared container and find B with address(0x0000600002aa69d0) and assign this
B
onC
property and finally createdC
object. - Now
A
resolved all its dependencies it returns A object and deleted shared container since the graph is resolved.
As shown in Figure 12 B address is the same on A and C classes.
Transient → which always returns a new object. (an instance provided by a container is not shared).
As shown in Figure 13 by making B scope transient we are saying that doesn't share it’s instance as shown in Console B address is different on both A and C instances:
Container → This scope is also known as Singleton. which means once the object is resolved it will share that instance for every resolve method until the application terminates as shown in Figure 14:
Custom Scopes → Instances in .custom
the scope will be shared in the same way as in .container
scope but can be discarded as needed.
As shown in Figure 15 we performed a number of tasks to implement Custom Scopes:
- We extend
ObjectScope
and add the custom static property and made a scope of this storage typePermanentStorage
(Means Persists stored instance until it is explicitly discarded ). - We register
UserSession
with custom object scope. - On
loginInUser
the method we get the instance ofUserSession
and as shown in Figure 15 set its token. Now every time we resolveUserSession
we get the same instance as shown in the console. - On
logOut
we discarded this scope means in Future if we resolveUserSession
we get the new instance as shown in the console:
For more information about scopes see this link. We didn’t cover the weak scope type.
Some Points
Registration Keys → A registration of the component for a given service is stored in a container with an internally created key. The container uses the key when trying to resolve a service dependency.
Value Types injection → All previous examples we saw uses classes but can resolve Struct as well Note: weak scope will not be available on Value types.
Modularizing Service Registration
Sometime in future, you can see this in your code if you don’t properly modularize your service registration as shown in Figure 16. This is where Assembler comes into play:
Assembly
The Assembly
is a protocol that is provided a shared Container
where service definitions can be registered. Let’s create Assembly to modularize and make readable service registration process as shown in Figure 17 and 18. Note: Any assembly can resolve any other assemble object if they have the same Assembler:
Assembler
The Assembler
is responsible for managing the Assembly
instances and the Container.
If all the above Assemblies registered with the same assembler then they have the same container which makes it possible to resolve any other container object by using this container shared instance.
How can we register Assemblies to the assembler?
As shown in Figure 19 we assembled all our assemblies into assembler where all assemblies shared the same container. Since they have the same container ManagerAssembly
can resolve UtilityAssembly
objects. Note: You MUST hold a strong reference to the Assembler
otherwise the Container
will be deallocated along with your assembler:
Usage
As shown in Figure 20 we are now able to resolve our dependency using Assembler:
As shown in Figure 21, we are resolving DateUtility in Fetcher assembly since both have the same assembler:
Swinject have many extensions SwinjectStoryboard: is one of them since it’s very useful we will cover this as well.
SwinjectStoryboard
SwinjectStoryboard is an extension of Swinject to automatically inject dependency to view controllers instantiated by a storyboard. Very useful in VIPER architecture
You can install it through pod by adding the dependency in your podfilepod ‘SwinjectStoryboard’
.
As shown in Figure 22 we have a controller having class and storyBoardId
is ThirdViewController
:
Where ThirdViewController has three dependencies as shown in Figure 23:
As shown in Figure 24 When ThirdViewController instantiated from storyboard SwinjectStoryboard
will resolve all the dependencies required by this controller.
Main → StoryBoard Name
Useful Links
http://fabien.potencier.org/do-you-need-a-dependency-injection-container.html
https://pmbanugo.wordpress.com/2014/05/29/attaining-loose-coupling-with-dependency-inversion-inversion-of-control-and-dependency-injection/