Tue Nov 19, 2019
At this stage the entire community seems to be using some shiny architecture for their Swift apps. Most of the time it appears to be MVVM, but I still stumble across some MVP and even some brave enough to run with VIPER. When I started learning about properly implementing some of these architectures (which was a shorter time ago than I’d like to admit) they seemed great, but I kept getting stuck at one point.
The problem
When I wanted to create a new view controller to push or present, I’d need to instantiate a whole bunch of different items. I’d need the view model for the view controller, and then I’d need whatever the view model relied on as well, which could be a network interactor, or a model, or some enum type to switch on. The point of these architecture types was to facilitate testing, which meant that dependency injection was a pretty large component. This way we could swap out implementations of concrete types and inject in mock types that made things very easy to test. But storyboards don’t allow dependency injection, and I like storyboards (pls still like me for liking storyboards 🙃).
The setup
The way all of my projects are setup usually results in each view component having three files: one view file; one view model protocol; and one view model implementation file. It looks something like this:
The view model protocol
protocol HomeViewModel {
func viewDidLoad()
}
The view model implementation file
class HomeViewModelBase: HomeViewModel {
func viewDidLoad() {
// do some setup here…
}
}
And lastly the view file, which in this case is a UIViewController
class HomeViewController: UIViewController {
var viewModel: HomeViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel.viewDidLoad()
}
}
We can see that in our view controller we have an implicitly unwrapped optional for the view model. This signifies to us that the view controller really can’t operate without the view model. It’s also public so that we can set it from outside the class. So when we go to push or present this HomeViewController
we need to also instantiate a class or struct conforming to the HomeViewModel
protocol and assign it to the viewModel
property within the HomeViewController
before we show the view controller. This can make it complicated to set things up right as different items need to be instantiated before others. This is the builder problem, and it was the hidden gotcha that I ran into when learning these architecture styles.
The old solution
We’ve identified everything that we need to create a new view:
- An instantiated view controller.
- An instantiated view model.
- Anything the view model may need injected into it, which is nothing in this case, but important to remember.
To put all of this together we could do the following:
func presentHomeView() {
let storyboard = UIStoryboard(name: "Home", bundle: nil)
let vc = storyboard.instantiateViewController(identifier: "Home") as! HomeViewController
let viewModel = HomeViewModelBase()
vc.viewModel = viewModel
present(vc, animated: true, completion: nil)
}
This code can be made a bit more generic and then abstracted out and reused, and this is pretty common, I’ve done it myself numerous times. However, there are parts to it that really aren’t great. The force casting to a HomeViewController
isn’t a good idea. We could use guard let
, but what if the cast fails? Do we throw an error and show the user? Do we just return from this function? What’s the best practice? The other argument is to keep the force casting and if it crashes, just let it crash, it shouldn’t be able to get into that state anyway… That’s also not good. As well as this, in our HomeViewController
the viewModel
property is still force unwrapped and public, wouldn’t it be nicer if it wasn’t either of those things?
The new solution
Swift 5.1 introduces a new instance method that allows us to solve all of these problems: instantiateViewController(identifier:creator:)
. Apple defines the creator
parameter as:
A block containing your custom creation code for the view controller. Use this block to create the view controller, initialize it with the provided coder object and any custom information you require, and return the result.
So now we can pass in the custom objects we need for the view controller to function, such as our view model. To do this we need to make some changes to our existing HomeViewController
as follows:
class HomeViewController: UIViewController {
// 1
private let viewModel: HomeViewModel
// 2
init?(coder: NSCoder, viewModel: HomeViewModel) {
self.viewModel = viewModel
super.init(coder: coder)
}
// 3
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
viewModel.viewDidLoad()
}
}
The three main changes we’ve made here are:
- We’ve changed our
viewModel
from avar
to alet
and removed the bang operator. We’ve also made it private. - We’ve created our own custom initialiser which takes in an
NSCoder
and aHomeViewModel
. - We’ve added in the required initialiser as it is needed when we create our own initialiser, but we’ve left out the implementation as we don’t intend to call this.
With these changes, our presentHomeView()
function can change to this:
func presentHomeView() {
let storyboard = UIStoryboard(name: "Home", bundle: nil)
let vc = storyboard.instantiateViewController(identifier: "Home") { coder -> UIViewController? In
let viewModel = HomeViewModelBase()
return HomeViewController(coder: coder, viewModel: viewModel)
}
present(vc, animated: true, completion: nil)
}
This is pretty great, not we have no forced unwrapping anywhere! Not in the instantiation code or in the properties of the HomeViewController
.
Conclusion
I think this new way makes it a lot clearer on how to setup views and their dependencies. It eliminates some dangerous unwrapping and typecasting and I think simplifies the whole process. I also like the use of initialisers in view controllers as we can now move a lot of setup that may have previously happened in the viewDidLoad
out into the init
.
Spot something wrong? Let me know on Twitter