Dependency Injection in View Controller
The fact that the init?(coder:)
initialiser is being used suggests that your view controller is initialised from storyboard. If that is the case, storyboard does not contain ModelManager
, thus it cannot decode it.
You can work around this by wrapping storyboard initialisation into your own method, e.g.:
class MyViewController: UIViewController {
private var modelManager: ModelManager
static func create(modelManager: ModelManager) -> MyViewController {
let vc = /* instantiate vc from storyboard */
vc.modelManager = modelManager
return vc
}
}
If the above method does not suit your needs, I would suggest you take a look at the SwinjectStoryboard framework. It provides - besides basic DI functionality - ability to inject dependencies to the view controllers initialised from storyboard.
How to Implement ViewController custom init using dependency injection and factory patterns?
There are different types of dependency injection. You're currently trying to use constructor-based dependency injection, which unfortunately doesn't really work with storyboards, since they need to initialise with a decoder. iOS 13 does introduce some additional functionality which will make this approach possible, but for the moment, you could use setter-based dependency injection instead.
Something like the following:
class ViewController: UIViewController {
var factory: ViewControllerFactory!
}
protocol ViewControllerFactory {
func makeViewController() -> ViewController
}
class DependencyContainer {
let storyboard: UIStoryboard = UIStoryboard(name: "Storyboard", bundle: Bundle.main)
}
extension DependencyContainer: ViewControllerFactory {
func makeViewController() -> ViewController {
guard let vc = storyboard.instantiateViewController(withIdentifier: "ViewController") as? ViewController else {
fatalError("Unrecognised viewController")
}
vc.factory = self
return vc
}
}
How to dependency inject in storyboard managed UIViewControllers?
View controllers in storyboard are always initialised using init?(coder aDecoder: NSCoder)
, so there's no way to set any properties at initialisation.
I've found the following to be a good workaround…
Rather than using
let loginSync: LoginSync
Declare as
private (set) var loginSync: LoginSync!
Declare
func configure(loginSync: LoginSync) {
self.loginSync = loginSync
}
Then
let vc = storyboard.instantiateViewController(withIdentifier: "YourFlow")
vc.configure(loginSync: MockLoginSync())
You can also use this in segues…
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.destination) {
case let vc as MyViewController:
vc.configure(loginSync: MockLoginSync())
default:
break
}
}
It's not perfect, but making the property private (set)
ensures it can't be modified from another class, and the implicit unwrapping (!
) means you'll get a crash if it's not set.
Use configure()
methods in every UIView
/UIViewController
- once you get used to this pattern it becomes second nature.
Related Topics
Why Would One Use Nested Classes
Implement Protocol with Different Associated Type
Produce Sounds of Different Frequencies in Swift
Table View Cells Changing Colors When Scrolling Swift
Load Viewcontroller Swift - Black Screen
How to Mirror The Design of The Codable/Codablekeys Protocols
Swift: Urlsession.Shared.Datatask Says Status Code 304 = 200
How to Access The Model Component of Reality Composer in Realitykit
Detect What Is The Location of The Tap/Press Inside UIbutton Inside Sender Action Method
Swift Can Change Struct Declared with Let If Using an Index But Not If Using a Loop
How to Test If an Instance Is a Specific Class or Type in Swift
Enum Initialized with a Non-Existent Rawvalue Does Not Fail and Return Nil
How to Assign a Generic Function to a Variable
How to Hide The Status Bar Programmatically in iOS 8
Using Dateformatter with Timezone to Format Dates in Swift