Using child view controllers as plugins in Swift

A very common problem when building apps for Apple's platforms is where to put common functionality that is used by many different view controllers. On one hand we want to avoid code duplication as much as possible, and on the other hand we want to have a nice separation of concerns to avoid the dreaded Massive View Controller.

An example of such common functionality is dealing with loading and error states. Most view controllers in an app will at some point need to load data asynchronously - an operation that could take a bit of time and also has the potential to fail. In order to let our users know what's going on, we usually want to display some form of activity indicator while we're loading and an error view in case the operation failed.

So where to put that kind of functionality? 🤔 A very common solution is to create a BaseViewController that we have all of our other view controllers inherit from:

class BaseViewController: UIViewController {
    func showActivityIndicator() {
        ...
    }

    func hideActivityIndicator() {
        ...
    }

    func handle(_ error: Error) {
        ...
    }
}

While doing something like the above may seem nice - because it's very convenient - it's also usually a slippery slope that leads to some tricky architectural problems. It's very easy for BaseViewController to become a catch-all for all kinds of functionality, which usually makes it really hard to maintain.

Another problem with the BaseViewController approach is that it locks all of our view controllers into inheriting from a single class. This is in general not a good situation to be in, since it gives you less flexibility to pick the best fit for a given class' superclass. For example, if we want to implement a view controller based on a UITableView, inheriting from UITableViewController would probably be a much better choice.

This week, let's take a look at how we can use child view controllers as "plugins", to enable us to easily mix and match common functionality without having to resort to a single base class.

Child view controllers

Child view controllers have been around ever since iOS 5, but is still a feature that is quite often overlooked. It's a simple concept - just like you can build UIView hierarchies with subviews and superviews, you can do the exact same thing with view controllers.

What I love about using child view controllers is the fact that they get access to the exact same events as their parent view controller (things like viewDidLoad, viewWillAppear, etc), without having to be a subclass of it. They can also be responsible for their own internal layout, and perform their own controller logic. This enables us to structure our code very much like a suite of modular plugins, that can be added and removed as needed.

For example, we can implement a child view controller that we can add whenever we want to show an activity indicator while loading data:

class LoadingViewController: UIViewController {
    private lazy var activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)

    override func viewDidLoad() {
        super.viewDidLoad()

        activityIndicator.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(activityIndicator)

        NSLayoutConstraint.activate([
            activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        // We use a 0.5 second delay to not show an activity indicator
        // in case our data loads very quickly.
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            self?.activityIndicator.startAnimating()
        }
    }
}

The major benefit of the above approach instead of using a BaseViewController, is that any view controller in our app that needs to display a loading indicator can simply add LoadingViewController as a child. It also lets us contain all the logic that goes into displaying a loading indicator in a single place, rather than having it live together with completely unrelated functionality 👍.

Adding and removing a child view controller

So how do we actually use our new LoadingViewController? UIViewController has an API that lets us add child view controllers, called addChildViewController, but it turns out that things are not as simple as just calling that method 😅.

In order to add a child view controller, we need to do the following:

// Add the view controller as a child
addChildViewController(child)

// Move the child view controller's view to the parent's view
view.addSubview(child.view)

// Notify the child that it was moved to a parent
child.didMove(toParentViewController: self)

Then, to remove a child view controller, we also need to perform 3 different steps:

// Notify the child that it's about to be moved away from its parent
child.willMove(toParentViewController: nil)

// Remove the child
child.removeFromParentViewController()

// Remove the child view controller's view from its parent
child.view.removeFromSuperview()

If you want to start using child view controllers a lot in your app, doing the above every single time will quickly become tedious. Since it fits my 3 requirements for abstraction - it's repetitive, boring and error prone - let's abstract it! 😀

Let's make an extension on UIViewController that makes handling child view controllers a lot simpler:

extension UIViewController {
    func add(_ child: UIViewController) {
        addChildViewController(child)
        view.addSubview(child.view)
        child.didMove(toParentViewController: self)
    }

    func remove() {
        guard parent != nil else {
            return
        }

        willMove(toParentViewController: nil)
        removeFromParentViewController()
        view.removeFromSuperview()
    }
}

We can now simply call add() and remove() to manage child view controllers in our app 👌.

Using a child view controller

The cool thing is that since UIKit takes care of both the layout, and sending all of the standard UIViewController events to our child view controller, all we have to do is to add and remove it. Here's how we can now super easily add support for showing and hiding a loading indicator in a ListViewController:

class ListViewController: UITableViewController {
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadItems()
    }

    private func loadItems() {
        let loadingViewController = LoadingViewController()
        add(loadingViewController)

        dataLoader.loadItems { [weak self] result in
            loadingViewController.remove()
            self?.handle(result)
        }
    }
}

Pretty nice! And the best part is that all of our view controllers can now take advantage of this functionality, no matter what superclass they inherit from 🎉.

Error handling

Now that we have a view controller that we can plug in for loading states, let's do the same thing for error states. Similar to how we created a LoadingViewController before, we can create an ErrorViewController that displays an error message. Let's say we also include a Reload button in our UI, so we'll include an API to set a reloadHandler closure that gets called whenever the reload button is tapped:

class ErrorViewController: UIViewController {
    var reloadHandler: () -> Void = {}
}

Just like how we could simply add LoadingViewController as a child, we can now do the exact same thing to show an error view:

private extension ListViewController {
    func handle(_ error: Error) {
        let errorViewController = ErrorViewController()

        errorViewController.reloadHandler = { [weak self] in
            self?.loadItems()
        }

        add(errorViewController)
    }
}

Conclusion

Structuring your code as modular plugins, rather than relying too much on subclassing, can make your code a lot easier to extend and maintain. The one thing that is true for almost all code bases is their need to adapt and change for new features or new versions of the SDK, and having common functionality structured as separate child view controllers can really help making that as easy as possible.

While I'm not suggesting that you completely abandon inheritance, designing composable APIs that you can mix and match depending on your needs is usually a much more flexible approach. We'll take a closer look at other examples of using composition and plugins in future blog posts.

What do you think? Have you used child view controllers this way before, or is it something that you'll try out? Let me know, along with any questions, comments or feedback that you have - either in the comments section below or on Twitter @johnsundell. Feel free to also share this post on Twitter if you liked it, I would really appreciate it 😊.

Thanks for reading! 🚀

Using unit tests to identify & avoid memory leaks in Swift

Avoiding force unwrapping in Swift unit tests