iOS Dependency Injection by Needle
Introduction
In software engineering. dependency injection is a technique whereby one object supplies the dependencies of another object. A dependency is an object that can be used.
What is dependency?
For example, when class A uses some functionality of class B, then its said that class A has a dependency of class B, referred by freeCodeCamp. This is simple explanation, but I think this is the essence.
See some codes.
In the above code, ViewController
class has a dependency of User
class. In this case, User
instance is initialized in the ViewController
. What if you want to change the text of titleLabel? It is impossible. That is not good, less flexible.
Change this code to the following.
Then, can change the text to what you want because the User
class dependency is injected in initializer.
let viewControllerAlpha = ViewController(user: User(name: "Alpha"))
let viewControllerBeta = ViewController(user: User(name: "Beta"))
let viewControllerGamma = ViewController(user: User(name: "Gamma"))
This is basic.
By the way, Singleton?
If you want to make ViewController
only flexible, Singleton
pattern can be used.
User.shared.name = "Alpha"
let viewController = ViewController()
// titleLabel.text will be "Alpha"
But, the User.shared
can be accessible and changed from anywhere. Also, if you perform unit test, previous test will affect remaining tests. So, this is not good and safe. (I’m sorry this singleton example is too biased.)
Dependency Injection by Needle
Needle is a DI system for Swift. This encourages hierarchical DI structure and utilizes code generation to ensure compile-time safety.
Compile-time safety is a big feature of this library.
Try it. Based on GitHub README
1. Installation
Use carthage here. The latest version is 0.10.0 (12/Jul/2019).
github "https://github.com/uber/needle.git" == 0.10.0
And setup carthage configuration.
2. Xcode Integration
In Xcode build phase, add Run Script Phase
and the following script.
export SOURCEKIT_LOGGING=0 && $SRCROOT/Carthage/Checkouts/needle/Generator/bin/needle generate $SRCROOT/$TARGETNAME/NeedleGenerated.swift $SRCROOT/$TARGETNAME
This script exposes NeedleGenerated.swift
.
3. Execute Generated code
In NeedleGenerated.swift
, the function should be invoked as the first step upon launching appears, registerProviderFactories
.
Call this function in AppDelegate
.
main.swift
will be used depending on application structures.
Is Ready for use
Assuming we implement the following case.
- Root: Root page whose viewController is a rootViewController of application key window. This manages
User
object. - Tutorial: Tutorial page which will be appeared in first application launching (ex: determined by a certain key in UserDefaults). After tutorial, move to Login page.
- Login: Login page which will be appeared when the user’s login information is not stored. ex: not stored in Keychain. After login, move to Home page.
- Home: Home page which will be appeared in others cases. It has
User
type property and show the user’s name totitleLabel
.
In this case, if every flow succeed, will reach to Home page in all three patterns.
Thinking of the central pattern, Root -> Login -> Home
. Root has User
object, Login does not have and Home has dependency of User
object. To pass Root’s User
object to Home, do we have to pass it to Login which doesn’t need it? The following code show this situation.
LoginViewController
class has initializer which has User
type argument just only for passing it to HomeViewController
.
Component
Using Needle, the User
object can be injected directly from Root to Home using Component in NeedleFoundation.framework.
In Component, each dependency scope will be defined. Only you have to do is defining each dependency. After that, needle resolves the dependency.
This is RootDiComponent class. It inherits BootstrapComponent that represents the root of a dependency graph.
Root can display Tutorial, Login or Home, so it has each DI component, having RootDiComponent as parent component. Dependency of a component can be injected from its parent component.
And has user object. The user object is computed property, so it normally returns new instance every time. But shared
function is used here. This will make the property return same instance every time.
TutorialDiComponent and LoginDiComponent are defined in this code.
Both of them inherit Component<EmptyDependency>. These components don’t have dependency.
See LoginViewController’s initializer argument, it doesn’t require user object anymore.
HomeDiComponent inherits Component<HomeDependency>, and HomeDependency requires user object. The dependency will be injected from parent component.
- LoginDiComponent has
HomeDiComponent(parent: self)
. - RootDiComponent has
LoginDiComponent(parent: self)
.
Now, the dependency injection branch will be like Root -> Login -> Home
. So, user object in Root will be injected to Home. (LoginDiComponent doesn’t have to have user object because its parent RootDiComponent has it.)
What if RootDiComponent doesn’t have user
Comment out user property and try building.
Then, building will failed.
💩 Could not find a provider for (user: User) which was required by HomeDependency, along the DI branch of ^->RootDiComponent->LoginDiComponent->HomeDiComponent.
The error message is the above.
Unresolved dependency cause compile error.
Summarize
There are some great Swift DI libraries, Swinject, DIKit, Cleanse. Here, I tried using Needle. DI will make your codes flexible, independent and testable. Such techniques will improve our application quality.
I will upload my NeedleScratch repository soon. 🙏