The 3 Rules to Great Base View Controllers

As developers, we’re in a unique position where we can create our own tools. If nothing fits our needs, we can make our own! Isn’t that an empowering feeling? Today, I want to share with you a simple UIViewController base class that I built as tool for myself. 
I affectionately call it the StackViewController!

It’s a UIStackView, inside a UIScrollView, inside a view controller. As simple as that.

In this article, you’ll learn:

  • When you should create your own UIViewController base class.
  • When to subclass a UIViewController and when to use composition.
  • The three rules I follow to create your own robust base controllers that won’t be a pain to maintain.

Let’s look at how we can make it work.

Why do this?

Building a base view controller is an important philosophical decision. Whenever you add an extra layer to your inheritance chain, there needs to be a darn good reason for it.

Long inheritance chains are maintenance nightmares. Have you ever worked on an app with some sort of class called MyAppBaseViewController? You change something inside and suddenly new bugs are popping up everywhere?

Yeah. It’s an awful feeling.

More often than not, we can use composition instead of subclassing to get the results we want. This should be your first reflex: break things down into manageable parts and then mediate between them. It helps prevent Massive View Controllers and it’ll make your codebase far more enjoyable to work in.

However, sometimes what we really need is a base class. We want to change the very underlying thing we’re working with. I often find myself doing this for controllers when I need generic, custom UI. However, in order to maintain a healthy project, I have a few guidelines that I follow to make sure my foundations don’t give out from under me.

The 3 rules for great UIViewController subclasses

In order to create a great base view controller, these are the three rules that I follow:

  1. No business logic
  2. Support the rest of the public API
  3. No external dependencies

Let’s take a quick look at all of these in turn.

No business logic

The first and most important guideline is simple: no business logic in your view controller. The reasons are numerous: we want this class to be reusable no matter where we are in our app. We want it to be focused and have clear responsibilities.

But don’t get me wrong, I understand the temptation! Adding business logic to a base view controller makes things easy. Everything you need is right there! But seriously, can you imagine if UITableViewController had a loggedInUser property?

Yeah, makes no sense 🤓

Support the rest of the public API

The second guideline is to properly support the rest of the API. This is especially important if you plan on reusing this in the future. Six months from now, you’re not going to remember that addSubview: causes a crash if Jupiter isn’t aligned with Saturn 🌝

This also benefits the rest of your team. They’re going to be using your class as well, and it should behave like a normal view controller as much as possible.

It’s important to point out that some of the legacy UIKit APIs don’t follow this guideline. Take UITableViewController, for example. Have you ever tried using addSubview: to add some floating UI on top of it? And then you headed straight to StackOverflow, right?

Well that’s exactly what I mean. Support the underlying API.

(In contrast, UICollectionViewController’s more modern API properly supports this. Thanks, UIKit team!)

No other dependencies

Finally, it’s important for me that my base view controller be portable from project to project. That means absolutely no outside dependencies.

Yeah, using SnapKit would be easier than creating Autolayout constraints manually, but my next project may not use SnapKit. Maybe it’ll use Anchorage or Cartographer. I wouldn’t want to introduce a new dependency just so I can use my controller.

Actually building the thing

Alright, enough talk, let’s build this thing!

StackViews and ScrollViews

Our class starts off simple enough: a view controller subclass with a scroll view and a stack view. Note that this class is open since we want to be able to subclass it if needed, even outside of its own module.

open class StackedViewController: UIViewController {
	private(set) public var scrollView: UIScrollView = {
		let view = UIScrollView()
		view.translatesAutoresizingMaskIntoConstraints = false
		view.backgroundColor = .green
		view.contentMode = .top
		return view
	}()

	private(set) public var stackView: UIStackView = {
		let view = UIStackView()
		view.translatesAutoresizingMaskIntoConstraints = false
		view.distribution = .fill
		view.alignment = .fill
		view.axis = .vertical
		return view
	}()

	public init() {
		super.init(nibName: nil, bundle: nil)
	}

	required public init?(coder aDecoder: NSCoder) {
		fatalError("init(coder:) has not been implemented")
	}
}

Next, we override the loadView method. This is where we’ll set up our views and constraints. Lucky for us, UIStackView and UIScrollView play well together if their constraints are set up correctly 😇

override func loadView() {
	view.addSubview(scrollView)
	scrollView.addSubview(stackView)

	NSLayoutConstraint.activate([
		scrollView.topAnchor.constraint(equalTo: view.topAnchor),
		scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
		scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
		scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
		])

	let topConstraint = stackView.topAnchor.constraint(equalTo: scrollView.topAnchor)
	let bottomConstraint = stackView.bottomAnchor.constraint(lessThanOrEqualTo: scrollView.bottomAnchor)

	topConstraint.priority = UILayoutPriorityRequired
	bottomConstraint.priority = UILayoutPriorityDefaultLow

	NSLayoutConstraint.activate([
		topConstraint,
		bottomConstraint,
		stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
		stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
		])
}

Finally, we create our public API. I wanted to add convenience methods to add and remove the views that I’m stacking, so that’s below.

This would also be an excellent place to add methods for spacer views, separators, and other convenience methods if you’re into that stuff :)

public func addStackedView(view: UIView) {
	stackView.addArrangedSubview(view)
}

public func addStackedViews(views: [UIView]) {
	for view in views {
		addStackedView(view: view)
	}
}

public func removeStackedView(view: UIView) {
	stackView.removeArrangedSubview(view)
	view.removeFromSuperview()
}

public func insertStackedView(view: UIView, at index: Int) {
	stackView.insertArrangedSubview(view, at: index)
}

And there you go! Your very own StackViewController is ready to go!

I find this controller particularly useful when building forms or any UI that will need to scroll when the keyboard is present.

Bonus Points

Finally, I have one last tip for building this type of “library” code:

Give it its own Xcode project and add it to your main project’s workspace. As such, it’ll be defined in a separate module you’ll able to better assess what should be public and private through actual usage.

It’ll also be easier to version, and keep track of changes.

I hope you enjoyed this short philosophical dive into base view controllers! The key takeaway here is avoid them if you can.

But if you can’t, now you can build base VCs that won’t break every other commit!

Happy coding! ✌️