Articles, podcasts and news about Swift development, by John Sundell.

Showing view controllers, rather than pushing them

Published on 03 Sep 2020
Basics article available: Child View Controllers

When building iOS apps, it’s incredibly common to use UINavigationController to enable the user to navigate between various screens. Although some apps perform that kind of navigation through storyboard segues, many also programmatically push new view controllers onto the current navigation stack — for example when the user selects a row within a table view, like this:

extension ListViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView,
                   didSelectRowAt indexPath: IndexPath) {
        let item = items[indexPath.row]
        let detailVC = DetailViewController(item: item)
        navigationController?.pushViewController(detailVC, animated: true)
    }
}

The above code works perfectly fine as long as the above view controller will indeed be presented within a navigation controller, but given the way that iOS has evolved over the past few years, that’s not necessarily always going to be the case.

For example, when our app is running on a larger device, such as the iPad Pro (or the Mac if we’re using Catalyst), we might instead dynamically choose to present our content view controllers within a UISplitViewController, or some other form of container view controller that makes better use of a larger display.

That’s why it’s typically a good idea to use the show method to perform our navigation between view controllers, rather than directly calling pushViewController on the current navigationController — as that lets the system decide exactly how the next view controller should be presented, and fully decouples the current view controller from the navigation paradigm that it’s being presented in:

extension ListViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView,
                   didSelectRowAt indexPath: IndexPath) {
        let item = items[indexPath.row]
        let detailVC = DetailViewController(item: item)
        show(detailVC, sender: self)
    }
}

When using a UINavigationController, the above code sample will act exactly the same way as our original implementation — only now our code is much more dynamic and future-proof. Plus, if we ever wanted to customize the way our view controllers get presented, then we can now do that by implementing a custom container view controller that overrides that very same show method to perform its custom presentation logic.

Here we’re doing just that to simply replace a single content view controller every time that we’re asked to show a new one:

class ContainerViewController: UIViewController {
    private var contentViewController: UIViewController

    ...

    override func show(_ vc: UIViewController, sender: Any?) {
        contentViewController.remove()
        contentViewController = vc
        add(vc)
    }
}

Above we’re using the child view controller convenience APIs introduced in the Basics article on that topic.

When we call show within any given view controller, UIKit will (by default) search the view controller hierarchy for one that overrides that same method, and will use that implementation to present the view controller that we’re looking to show.

There’s also a showDetailViewController method for performing the same kind of operation, but for detail view controllers instead, for example when using a split view controller.