In Apple’s documentation, they suggest you use a pattern called MVC to structure your apps. However, the pattern they describe isn’t true MVC in the original sense of the term. I’ve touched on this point here before, but MVC was a design pattern created for Smalltalk. In Smalltalk’s formulation, each of the 3 components, model, view, and controller, each talked directly to each other. This means that either the view knows how to apply a model to itself, or the model knows how to apply itself to a view.
When we write iOS apps, we consider models and views that talk directly to each other as an anti-pattern. What we call MVC is more accurately described as model-view-adapter. Our “view controllers” (the “adapters”) sit in between the model and view and mediate their interactions. In general, I think this is a good modification to MVC — the model and view not being directly coupled together and instead connected via an intermediary seems like a positive step. However, I will caveat this by saying that I haven’t worked with many systems that don’t maintain this separation.
So, that’s why we have view controllers in iOS. They serve to glue the model and view together. Now, there are downstream problems with this style of coding: code that doesn’t obviously belong in models or views ends up in the view controller, and you end up with gigantic view controllers. I’ve discussed that particular problem on this blog many times, but it’s not exactly what I want to talk about today.
I’ve heard whispers through the grapevine of what’s going on under the hood with UIViewController
. I think the longer you’ve been working with UIKit, the more obvious this is, but the UIViewController
base class is not pretty. I’ve heard that, in terms of lines of code, it’s on the higher end of 10,000 to 20,000 lines (and this was a few years ago, so they’ve maybe broken past the 20 kloc mark at this point).
When you want the benefits of an object to glue a UIView
and a model object (or collection thereof) together, typically, we use view controller containment to break the view controller up into smaller pieces, and compose them back together.
However, containment can be finicky. It subtly breaks things if you don’t do it right, with no indication of how to fix any issues. Then, when you finally do see your bug, which was probably a misordering of calls to didMove
or willMove
or whatever, everything magically starts working. In fact, the very presence of willMove
and didMove
suggest that containment has some invisible internal state that needs to be cleaned up.
I’ve seen this firsthand in two particular situations. First, I’ve seen this issue pop up with putting view controllers in cells. When I first did this, I had a bug in the app where some content in the table view would randomly disappear. This bug went on for months, until I realized I misunderstood the lifecycle of table view cells, and I wasn’t correctly respecting containment. Once I added the correct -addChildViewController
calls, everything started working great.
To me, this showed a big thing: a view controller’s view isn’t just a dumb view. It knows that it’s not just a regular view, but that it’s a view controller’s view, and its qualities change in order to accommodate that. In retrospect, it should have been obvious. How does UIViewController
know when to call -viewDidLayoutSubviews
? The view must be telling it, which means the view has some knowledge of the view controller.
The second case where I’ve more recently run into this is trying to use a view controller’s view as a text field’s inputAccessoryView
. Getting this behavior to play nicely with the behavior from messaging apps (like iMessage) of having the textField
stick to the bottom was very frustrating. I spent over a day trying to get this to work, with minimal success to show for it. I ended up reverting to a plain view.
I think at a point like that, when you’ve spent over a day wrestling with UIKit, it’s time to ask: is it really worth it to subclass from UIViewController here? Do you really need 20,000 lines of dead weight to to make an object that binds a view and a model? Do you really need viewWillAppear
and rotation callbacks that badly?
So, what does UIViewController
do that we always want?
- Hold a view.
- Bind a model to a view.
What does it do that we usually don’t care about?
- Provide storage for child view controllers.
- Forward appearance (
-viewWillAppear:
, etc) and transition coordination to children. - Can be presented in container view controllers like
UINavigationController
. - Notify on low memory.
- Handle status bars.
- Preserve and restore state.
So, with this knowledge, we now know what to build in order to replace view controllers for the strange edge cases where we don’t necessarily want all their baggage. I like this pattern because it tickles my “just build it yourself” bone and solves real problems quickly, at the exact same time.
There is one open question, which is what to name it. I don’t think it should be named a view controller, because it might be easy to confuse for a UIViewController
subclass. We could just call it a regular Controller
? I don’t hate this solution (despite any writings in the past) because it serves this same purpose a controller in iOS’s MVC (bind a view and model together), but there are other options as well: Binder
, Binding
, Pair
, Mediator
, Concierge
.
The other nice thing about this pattern is how easy it is to build.
class DestinationTextFieldController {
var destination: Destination?
weak var delegate: DestinationTextFieldControllerDelegate?
let textField = UITextField().configure({
$0.autocorrectionType = .no
$0.clearButtonMode = .always
})
}
It almost seems like heresy to create an object like this and not subclass UIViewController
, but when UIViewController
isn’t pulling its weight, it’s gotta go.
You already know how to add functionality to your new object. In this case, the controller ends up being the delegate of the textField
, emitting events (and domain metadata) when the text changes, and providing hooks into updating its view (the textField
in this case).
extension DestinationTextFieldController {
var isActive: Bool {
return self.textField.isFirstResponder
}
func update(with destination: Destination) {
self.destination = destination
configureView()
}
private func configureView() {
textField.text = destination.descriptionForDisplay
}
}
There are a few new things you’re responsible with this new type of controller:
- you have to make an instance variable to store it
- you’re responsible for triggering events on it — because it’s not a real view controller, there’s no more
-viewDidAppear:
- you’re not in UIKit anymore, so you can’t directly rely on things like trait collections or safe area insets or the responder chain — you have to pass those things to your controller explicitly
Using this new object isn’t too hard, even thought you do have to explicitly store it so it doesn’t deallocate:
class MyViewController: UIViewController, DestinationTextFieldControllerDelegate {
let destinationViewController = DestinationTextFieldController()
override func viewDidLoad() {
super.viewDidLoad()
destinationViewController.delegate = self
view.addSubview(destinationViewController.view)
}
//handle any delegate methods
}
Even if you use this pattern, most of your view controllers will still be view controllers and subclass from UIViewController
. However, in those special cases where integrating a view controller causes you hours and hours of pain, this can be a perfect way to simply opt out of the torment that UIKit brings to your life daily.