Using child view controllers as plugins in Swift
Basics article available: Child View ControllersA 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:
// Move the child view controller's view to the parent's view
parent.view.addSubview(child.view)
// Add the view controller as a child
parent.addChild(child)
// Notify the child that it was moved to a parent
child.didMove(toParent: parent)
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(toParent: nil)
// Remove the child
child.removeFromParent()
// 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) {
addChild(child)
view.addSubview(child.view)
child.didMove(toParent: self)
}
func remove() {
// Just to be safe, we check that this view controller
// is actually added to a parent before removing it.
guard parent != nil else {
return
}
willMove(toParent: nil)
view.removeFromSuperview()
removeFromParent()
}
}
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 - on Twitter @johnsundell.
Thanks for reading! 🚀