Subclass-free view controllers in Swift
Basics article available: Child View ControllersEver since the release of Swift and its protocol-oriented paradigms started to become popular, many iOS developers have started to reduce their use of subclassing in favor of composition, protocol extensions and other more "Swifty" techniques & solutions.
One area in which subclassing is still heavily used, however, is when creating new UI features for an iOS app. Whether we're creating a new view to be pushed onto the navigation stack, some form of modal sheet, or a new member of a tab bar - the go-to starting point is often to create a new subclass of UIViewController
.
While I personally don't think that subclassing is universally bad, avoiding it can in many situations leave us with simpler code that is easier to change and reuse. This week, let's take a look at a few different techniques that can help us write subclass-free view controllers, and how that can help us avoid the Massive View Controller problem.
Custom root views
Sometimes when we create a new UIViewController
subclass, we're not super interested in any view controller-specific functionality - we simply want some form of container for our view code, and need to use a view controller in order to be able to present our new view in some way.
For example, let's say we're building a detail view for some form of Event
. A very common solution would be to create an EventDetailsViewController
, give it an initializer that takes an Event
model and have the view controller construct the UI that we need, like this:
class EventDetailsViewController: UIViewController {
private let event: Event
init(event: Event) {
self.event = event
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
let titleLabel = UILabel()
...
let subtitleLabel = UILabel()
...
let imageView = UIImageView()
...
}
}
While there's nothing wrong with the above approach, if all EventDetailsViewController
is doing is configuring and setting up subviews, it would probably be more appropriate for it to simply be a view. By moving our view code from view controllers to plain views, we can also help ourselves avoid a lot of the traps that lead to Massive View Controllers - for example when view controllers start to fill up with everything from layout code, to subview creation, to data binding and beyond.
What we'll do in this case is to create an EventDetailsView
and move all of our event detail view code into it:
class EventDetailsView: UIView {
let titleLabel = UILabel()
let subtitleLabel = UILabel()
let imageView = UIImageView()
}
However, we still need a view controller in order to be able to display our detail view as a modal or in a container such as a navigation controller or a tab controller. To be able to do that, we can simply create a new instance of UIViewController
and assign our EventDetailsView
as its root view, like this:
let vc = UIViewController()
vc.view = EventDetailsView()
navigationController.pushViewController(vc, animated: true)
Factory methods
Moving our view code from a UIViewController
subclass to a UIView
subclass might not seem like that big of a deal. But this approach starts to become a lot more powerful once we start combining it with other patterns that let us improve the encapsulation of our code.
One such pattern is the Factory pattern, which we've taken a look at before as a way to avoid shared state and as a way to statically create objects. When we're dealing with more simple UIs that are quite static and don't need a ton of interactions, the factory pattern can also be a really nice way to encapsulate the creation of view controllers that simply act as containers for a custom view.
Here we're creating an EventViewControllerFactory
which lets us abstract both the creation of our EventDetailsView
and its data binding into one easy-to-use API:
class EventViewControllerFactory {
func makeDetailViewController(for event: Event) -> UIViewController {
let view = EventDetailsView()
view.titleLabel.text = event.title
view.subtitleLabel.text = event.location
view.imageView.image = event.image
let vc = UIViewController()
vc.view = view
return vc
}
}
Pretty nice and clean - and we've now also introduced a nice layer of abstraction that'll let us iterate on EventDetailsView
without having to update a lot of different call sites, since it now becomes an implementation detail of our factory 👍.
Presenters
Another pattern that can be really useful when wanting to use view controllers in a more simple way is the presenter pattern. While there are many different flavors of this pattern that involve a lot more logic than what we'll use here, it can be a great option when we want to unify the way a certain UI is presented.
Let's say we always want to present our EventDetailsView
modally, and we want to make it easy to do so from anywhere throughout our app. One way to do that is to simply turn our EventViewControllerFactory
into a presenter instead, and instead of returning the view controller it creates it presents it modally - like this:
struct EventDetailsPresenter {
let event: Event
func present(in container: UIViewController) {
let view = EventDetailsView()
view.titleLabel.text = event.title
view.subtitleLabel.text = event.location
view.imageView.image = event.image
let vc = UIViewController()
vc.view = view
container.present(vc, animated: true)
}
}
The beauty of the above approach is that it becomes much less likely that we'll end up using EventDetailsView
"the wrong way". By abstracting not only the creation of it, but also its presentation, we can much more easily make sure that it gets presented consistently throughout our code base.
We'll take a closer look at many different flavors of the presenter pattern (and design patterns that promote the use of them) in future posts.
The declarative nature of Auto Layout
So far our EventDetailsView
has always filled its entire parent view controller, but a lot of times our top-level layout is not as simple as that. Often we want to add multiple subviews to a view controller's view and define layout relationships between them.
This is a situation in which the declarative nature of Auto Layout really shines. Since Auto Layout is based on defining constraints, it doesn't require any form of subclassing in order to work. That means that we can continue to use the same subclass-free view controller approach even when we have more than one view. Here we're extending our event detail UI with a header view, and adding constraints between it and our EventDetailsView
from before:
let vc = UIViewController()
let headerView = EventHeaderView()
headerView.translatesAutoresizingMaskIntoConstraints = false
vc.view.addSubview(headerView)
let detailView = EventDetailsView()
detailView.translatesAutoresizingMaskIntoConstraints = false
vc.view.addSubview(detailView)
NSLayoutConstraint.activate([
headerView.topAnchor.constraint(equalTo: vc.view.topAnchor),
headerView.widthAnchor.constraint(equalTo: vc.view.widthAnchor),
headerView.heightAnchor.constraint(equalTo: vc.view.heightAnchor, multiplier: 0.3),
detailView.widthAnchor.constraint(equalTo: vc.view.widthAnchor),
detailView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
detailView.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor)
])
The above approach can easily be used regardless of whether we choose the factory pattern, the presenter pattern, or some other way of creating our view controller. We could also move the creation of constraints into yet another factory if our layout code keeps growing (for example a EventDetailConstraintsFactory
that returns an array of NSLayoutConstraint
given a header view and a detail view).
Conclusion
Avoiding the Massive View Controller problem is in many ways all about breaking many common assumptions about iOS development. Just like how we've broken the assumption that each screen can only have a single view controller through techniques like using child view controllers and creating custom container view controllers - breaking the common assumption that a new UI always needs to be created in a view controller subclass can sometimes be really beneficial.
Does this mean that subclassing UIViewController
is bad and should be avoided at all costs? Of course not. Sometimes we need the power that a view controller subclass gives us, and sometimes a custom view controller is the appropriate abstraction for a complex UI that has interactions or model updates that we need to observe. However - for more simple, static view controllers, using a plain UIViewController
can be a great option.
If we wanted to, we could also have made our solution completely subclass free by also composing plain UIViews
instead of creating an EventDetailsView
subclass - but let's not take things too far 😉.
What do you think? Do you already use subclass-free view controllers, or is it something you'll try out for your more static UI code? Let me know - along with your questions, comments and feedback - on Twitter @johnsundell.
Thanks for reading! 🚀