First introduction of Dependency Injection.
I haven't updated my blog recently as I got some interviews recently. And I need to prepare each one properly becuase I really wanna got an iOS related job.
Today I'd like talk about Dependency Injection
. In recent, I have refactor one of my app with it and also I was asked if I used it before during one interview.
What is Dependency Injection?
Normally, we use it to manage global state of our app. For instance, we have a music app, and we want to manage those properties like songs, currIndex and playMode in a global poll, so each ViewController will use this unique data source to keep them synced with each other. Another thing is we usually use it to replace singletons in our project. Because the singleton pattern increases coupling whereas dependency injection reduces coupling.
Ok, enough talk for the theory. Let's see how to create a Dependency Injection
.
An Example
In this example, I create a UIViewController
subclass, it has a property stateManager
with type of StateManager
, which is our dependency class.
import UIKit
class ViewController: UIViewController {
var stateManager: StateManager!
}
StateManager
import Foundation
enum PlayMode: String {
case cycle = "repeat"
case cycleOne = "repeat.1"
case shuffle = "shuffle"
}
class StateManager {
var songs: [Song]? = nil
var position = 0
var playingMode: PlayMode? = .cycle
}
inject the dependency
// Initialize View Controller
let vc = ViewController()
// inject the dependency(state manager)
viewController.stateManager = StateManager()
In this way, we create dependency object outside our ViewController
and inject/assign it to stateManager
of ViewController. That means ViewController
class only knows StateManager
class but don't know its initialization since we create that instance outside ViewController
. That is very important!
Great, I think you have got what is Dependency Injection
and how to use it. But it is just 1 form of it, called property injection
, because we are actually passing a StateManager instance to ViewController
's property.
Generally, there are three types of Dependency Injection:
- initializer/constructor injection
- property injection
- method injection
Well, they may be named slightly different from various developers, though it doesn't matter. I have covered property injection above, let's see the other two types.
Initializer Injection
Most of developers will prefer to use this type. I will explain per below example.
struct User {
var id: UUID
var name: String
}
struct UserStatusProvider {
private let storage: UserLocalStorage
init(storage: UserLocalStorage) {
self.storage = storage
}
func isLoggedIn() -> Bool {
// 1. create UserLocalStorage instance inside UserStatusProvider
// return UserLocalStorage().getUser() != nil
// 2. create UserLocalStorage instance outside UserStatusProvider
return storage.getUser()
}
}
struct UserLocalStorage {
func getUser() -> User? {
return UserDefaults.standard.object(forKey: "UserStorageKey") as? User
}
}
// initialize UserLocalStorage
let localStorage = UserLocalStorage()
// initialize UserStatusProvider
let userStatusProvider = UserStatusProvider(storage: localStorage)
This is actually one of my interview question. Here we want to inject a dependency into UserStatusProvider
object, instead of passing the property storage
directly, we pass that dependency during initialization of UserStatusProvider
. The major benifit is that dependency passed in during initialization can be made immutable.
The localStorage
instance is passed as an argument during initialization. The init(storage:
method is the designated initializer. Another benfit is the designated initializer clearly shows what the dependency of the UserStatusProvider
struct are, because we are required to pass storage
as an parameter during initialization. And that passed parameter in init()
is immutable by default.
You may have a question? What about constructor injection
in ViewController, why you change it to struct/class for the 2nd example? Well, that is because I found use constructor injection
in ViewController is a bit tricky, we need to deal with ViewController's designated initializer. Let's see below example.
import UIKit
class ViewController: UIViewController {
var stateManager: StateManager!
// 1. I don't want to use designated initializer
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
}
// 2. or use convenience initializer
convenience init(modelController: ModelController) {
self.init()
self.modelController = modelController
}
}
Use designated initializer
is sort of complicated, we need to call super.init()
to provide the nib and bundle, though I don't use nib and bundle, just put them as nil. Then another function is required to pass in the modelController
(dependency).
I found I could use convenience initializer
instead of designated initializer
as the code shown, but for now I only test it on a small project. That is things about best practice, so I want to check it completely on some complicated project later...
designated initializer vs convenience initializer? Check This Apple doc
I would not cover Method Injection
in this article as I didn't try it yet. I don't want to give the wrong information to others.
Well, here is a Repo to test property injection, feel free to have a look.
It includes:
- UITabBarController, NavigationViewController, those are popular used when creating an app.
- Where to create the entry point for dependency injection in real project? Check the SceneDelegate file.
- How to pass
Dependency
to UITabBar SubViewController? CheckTabBarController
file, you could found it. - How to use
convenience init
on ViewController? CheckNewViewController
file.
Okay, that is everything for this session, just a simple taste of Dependency Injection
. I didn't cover topics like Unit Test
or Singletons vs Dependency Injection
. Maybe add them later~