Container View Controllers

Massive View Controllers are a common pain with iOS development. A quick search will give you much good advice on how to slim down these monsters. In this post I want to give some attention to the often overlooked use of Container View Controllers.

Too Many Responsibilities

Take a look at this user interface. The top half is a table view listing countries with a latitude and longitude coordinate. The bottom half is a standard iOS map view. When a user chooses a location in the table view we show it in the map.

User Interface

This is a simple application but there is a lot happening with interactions between table view data source, delegate and map view. This is a great time to use container view controllers and split our app into a parent and two child view controllers:

Using a Storyboard

This is our starting Storyboard with a parent master view controller embedded in a navigation controller. The table view controller lists locations for a user to choose and has a delegate protocol to send the location back to the master view controller. The map view controller holds a MKMapView to show the location passed to it by the master view controller.

Initial Storyboard

The aim is to have the root views of our child controllers appear in the view of the master view controller. Using Interface Builder drag two container views from the Xcode Object Library into the empty root view of the master view controller:

Container View

Interface Builder creates two placeholder container views connected to view controllers with an embed segue.

Embedding container views

After deleting these added view controllers control-drag from the top container view to the location table view controller and choose embed segue:

Embed segue

Repeat this process for the lower container view and the map view controller.

Container View Constraints

The next step is to setup some Auto Layout constraints for our container views. I have added both views to a vertical stack view pinned to the margins of the root view:

Stack view

The stack view has a fill equally distribution. The view hierarchy:

View hierarchy

Note that when using Interface Builder to create container views you end up with an extra placeholder view that the root view of the child controller is then added to.

Accessing the Child Controllers

The childViewControllers property of UIViewController is a read-only array of child view controllers. We use this in the viewDidLoad of the master view controller to get references to the children and set the delegate:

fileprivate var locationTableViewController: LocationTableViewController?
fileprivate var mapViewController: MapViewController?

override func viewDidLoad() {
  super.viewDidLoad()

  guard let locationController = childViewControllers.first as? LocationTableViewController else  {
    fatalError("Check storyboard for missing LocationTableViewController")
  }

  guard let mapController = childViewControllers.last as? MapViewController else {
    fatalError("Check storyboard for missing MapViewController")
  }

  locationTableViewController = locationController
  mapViewController = mapController
  locationController.delegate = self
}

Update: An alternate approach suggested by Cameron in the comments is to use prepare(for segue: sender:):

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let destination = segue.destination
    if let locationController = destination as? LocationTableViewController {
      locationTableViewController = locationController
      locationController.delegate = self
    }

    if let mapController = destination as? MapViewController {
      mapViewController = mapController
    }
}

I will skip the details of the table view and map view controllers (see the sample code).

Stack View Fun

A quick side-track to show the table and map side-by-side in landscape. That needs a few lines of code in the master view controller to have viewWillTransition(to size:with coordinator:) switch the axis of the stack view:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  super.viewWillTransition(to: size, with: coordinator)
  topStackView.axis = axisForSize(size)
}

private func axisForSize(_ size: CGSize) -> UILayoutConstraintAxis {
  return size.width > size.height ? .horizontal : .vertical
}

Much better:

landscape layout

Adding and remove child views in code

Storyboards make it easy to setup static container views but what if you want to dynamically add and remove child view controllers? To see an example I will remove the container views and embed segues from the Storyboard and recreate the same setup in code.

My MasterViewController now takes care of the stack view and lazily creates the two child view controllers from the Storyboard when first accessed (details omitted for brevity):

private let topStackView = UIStackView()
fileprivate lazy var locationTableViewController: LocationTableViewController = ...
fileprivate lazy var mapViewController: MapViewController = ...

In the viewDidLoad method of the master view controller we set up the stack view and add the two child view controllers:

override func viewDidLoad() {
    super.viewDidLoad()
    setupStackView()

    addContentController(locationTableViewController, to: topStackView)
    addContentController(mapViewController, to: topStackView)
    locationTableViewController.delegate = self
}

I will skip the setup of the stack view (setting the axis, alignment and pinning to the margins). What is interesting are the steps to add a child view controller which I have collected into a small method:

private func addContentController(_ child: UIViewController, to stackView: UIStackView) {
    addChildViewController(child)
    stackView.addArrangedSubview(child.view)
    child.didMove(toParentViewController: self)
}

There are only three steps:

Notes:

I don’t need it this time but the steps to remove the child from the parent are as follows:

private func removeContentController(_ child: UIViewController, from stackView: UIStackView) {
    child.willMove(toParentViewController: nil)
    stackView.removeArrangedSubview(child.view)
    child.view.removeFromSuperview()
    child.removeFromParentViewController()
}

We reverse the steps compared to the add operation:

Sample Code

You can find both versions of the Container project in my CodeExamples GitHub repository.

Further Reading

The Apple View Controller Programming Guide for iOS goes into more details:

Never miss a post!

iOS Size Classes Cheat Sheet

Subscribe and get my free iOS Size Classes Cheat Sheet

Success! Now check your email to confirm your subscription and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
No time to watch WWDC videos?

Sign up to get my iOS posts direct to your inbox and I will send you a free PDF of my iOS Size Classes Cheat Sheet.

OK! Check your inbox (or spam folder) for an email to confirm your details and download your free guide to iOS Size Classes.

There was an error submitting your subscription. Please try again.

Unsubscribe at any time.
Archives Categories