Hi @pragm4tic, I’m really glad you’re happy with the depth of the book! Thanks for reaching out. We wanted to include a bit about Storyboards/IB in the Dependency Injection chapter, but we just didn’t get the chance. This is a topic I’d really like to add to the book in future editions. At the moment, we are focusing on adding more architecture chapters. We’ve gotten a lot of requests for including more architecture patterns. In the meantime, here are my thoughts and some suggestions.
The book’s hand-written approach to Dependency Injection can be used with Storyboard backed view controllers. The trick is to replace the view controller initializers with implicitly unwrapped optional properties for the view controller dependencies. For example, instead of this:
public class MainViewController: NiblessViewController {
// MARK: - Properties
// View Model
let viewModel: MainViewModel
// Child View Controllers
let launchViewController: LaunchViewController
var signedInViewController: SignedInViewController?
var onboardingViewController: OnboardingViewController?
// State
let disposeBag = DisposeBag()
// Factories
let makeOnboardingViewController: () -> OnboardingViewController
let makeSignedInViewController: (UserSession) -> SignedInViewController
// MARK: - Methods
public init(viewModel: MainViewModel,
launchViewController: LaunchViewController,
onboardingViewControllerFactory: @escaping () -> OnboardingViewController,
signedInViewControllerFactory: @escaping (UserSession) -> SignedInViewController) {
self.viewModel = viewModel
self.launchViewController = launchViewController
self.makeOnboardingViewController = onboardingViewControllerFactory
self.makeSignedInViewController = signedInViewControllerFactory
super.init()
}
...
You could implement the view controller like this:
public class MainViewController: UIViewController {
// MARK: - Properties
// View Model
var viewModel: MainViewModel!
// Child View Controllers
var launchViewController: LaunchViewController!
var signedInViewController: SignedInViewController?
var onboardingViewController: OnboardingViewController?
// State
let disposeBag = DisposeBag()
// Factories
var makeOnboardingViewController: (() -> OnboardingViewController)!
var makeSignedInViewController: ((UserSession) -> SignedInViewController)!
// MARK: - Methods
...
The reason I prefer to build UI in code is because of the tradeoffs we have to make above. In the second example above, we had to convert all the constant properties into variable properties. We also had to use implicitly unwrapped optionals which means we lose some compile time safety / the code could crash at runtime if a dependency is not set. I like being able to implement view controller initializers and guarantee that a view controller has all the objects it needs to function. You might get enough value out of building UIs in Storyboards that you’re OK with this tradeoff and that’s completely valid. I recommend trying both approaches to get a feel for the difference. I’ve been going from build UI in code to build UI in Storyboard and back several times in the last several years, and for me I’ve just landed on recommending building UI in code. I realize this can be a polarizing topic. The reason I recommend writing UI in code is to eliminate as many possible reasons for an app to crash.
The second trick is in implementing the view controller factory methods found in the dependency containers. You can find one of these factory methods by opening any of the -DependencyContainer classes in the example app for either Chapter 5 or 6. For example, here’s the book’s implementation of makeMainViewController
in KooberAppDependencyContainer
:
public func makeMainViewController() -> MainViewController {
let launchViewController = makeLaunchViewController()
let onboardingViewControllerFactory = {
return self.makeOnboardingViewController()
}
let signedInViewControllerFactory = { (userSession: UserSession) in
return self.makeSignedInViewController(session: userSession)
}
return MainViewController(viewModel: sharedMainViewModel,
launchViewController: launchViewController,
onboardingViewControllerFactory: onboardingViewControllerFactory,
signedInViewControllerFactory: signedInViewControllerFactory)
}
To use Storyboards you could implement the same method like this:
public func makeMainViewController() -> MainViewController {
let launchViewController = makeLaunchViewController()
let onboardingViewControllerFactory = {
return self.makeOnboardingViewController()
}
let signedInViewControllerFactory = { (userSession: UserSession) in
return self.makeSignedInViewController(session: userSession)
}
let storyboard = UIStoryboard(name: "MyStoryboard", bundle: nil)
guard let mainViewController = storyboard.instantiateViewController(withIdentifier: "mainViewController") as? MainViewController else {
fatalError("Error casting storyboard view controller to MainViewController.")
}
mainViewController.viewModel = sharedMainViewModel
mainViewController.launchViewController = launchViewController
mainViewController.makeOnboardingViewController = onboardingViewControllerFactory
mainViewController.makeSignedInViewController = signedInViewControllerFactory
return mainViewController
}
If using Storyboards, you’d probably end up placing the Storyboard instance as a property of the dependency container class. The rest of the material in the book, such as scoping, should continue to apply. I hope this helps. Feel free to reach out with any questions or if there’s any other examples I could provide. Cheers.