Sometimes we need to reuse the same UI element on few different screens. Let’s say you want to present your custom navigation bar or tab bar. The usual approach is to use child view controllers: create a separate custom UIViewController for navigation bar, then add it as a child view controller. In this article I’ll explain how to reduce the amount of boilerplate code needed to handle this child view controller and make it highly reusable.

Note: by reusing here I don’t mean reusing an instance of the child view controller, but rather reusing the child view controller class.

TL;DR: Github repo: https://github.com/agordeev/ReusableViewController

How to reuse view controller

Let’s start with designing our reusable view controller. My reusable view controller takes the simplest form: a view controller with a single label.

Reusing a view controller between screens

Don’t forget to set Storyboard ID, as we’re planning to connect those view controllers through the code.

Reusing a view controller between screens

After it’s done, let’s move to our parent view controller. As we’re planning to add a child view controller onto it, I suggest to add a view, which will contain that child view controller. In my case I’ve added a blank UIView with the size of the reusable view controller:

Reusing a view controller between screens

Create an IBOutlet for that view and call it containerView:

Reusing a view controller between screens

It’s time to write some code. Implement the logic of adding the reusable view controller to the parent view controller:

class ParentViewController: UIViewController {
    @IBOutlet weak var containerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        addReusableViewController()
    }

    func addReusableViewController() {
        guard let vc = storyboard?.instantiateViewController(withIdentifier: String(describing: ReusableViewController.self)) as? ReusableViewController else { return }
        vc.willMove(toParentViewController: self)
        addChildViewController(vc)
        containerView.addSubview(vc.view)
        constraintViewEqual(view1: containerView, view2: vc.view)
        vc.didMove(toParentViewController: self)
    }

    /// Sticks child view (view1) to the parent view (view2) using constraints.
    private func constraintViewEqual(view1: UIView, view2: UIView) {
        view2.translatesAutoresizingMaskIntoConstraints = false
        let constraint1 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.top, multiplier: 1.0, constant: 0.0)
        let constraint2 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: 0.0)
        let constraint3 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.bottom, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.bottom, multiplier: 1.0, constant: 0.0)
        let constraint4 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.leading, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.leading, multiplier: 1.0, constant: 0.0)
        view1.addConstraints([constraint1, constraint2, constraint3, constraint4])
    }
}

Build and run the app and you’ll see our reusable view controller sitting in it’s place:

Reusing a view controller between screens

That’s awesome, but not reusable at all: we’ll have to implement addReusableViewController() method for each view controller presenting the reusable view controller. We want to follow DRY principle. There’s a place for further improvements.

Using a protocol and extension for implementing a shared functionality

My suggestion is to create a protocol called ReusableHost and require our method there. Another thing that every host would need to have is containerView outlet, so we need to require it as well. Here’s the structure of our protocol:

protocol ReusableHost {
    var containerView: UIView! { get }
    func addReusableViewController()
}

The protocol says that all types conforming it must implement addReusableViewController() method and have a gettable containerView variable. Okay, but how about reusability? We still have to implement our method for each ReusableHost? No, because Swift has a great feature: protocol extensions with default implementations. All what we need is to extend our protocol and provide our implementation of addReusableViewController() method:

extension ReusableHost where Self: UIViewController {
    func addReusableViewController() {
        guard let vc = storyboard?.instantiateViewController(withIdentifier: String(describing: ReusableViewController.self)) as? ReusableViewController else { return }
        vc.willMove(toParentViewController: self)
        addChildViewController(vc)
        containerView.addSubview(vc.view)
        constraintViewEqual(view1: containerView, view2: vc.view)
        vc.didMove(toParentViewController: self)
    }

    /// Sticks child view (view1) to the parent view (view2) using constraints.
    private func constraintViewEqual(view1: UIView, view2: UIView) {
        view2.translatesAutoresizingMaskIntoConstraints = false
        let constraint1 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.top, multiplier: 1.0, constant: 0.0)
        let constraint2 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.trailing, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.trailing, multiplier: 1.0, constant: 0.0)
        let constraint3 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.bottom, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.bottom, multiplier: 1.0, constant: 0.0)
        let constraint4 = NSLayoutConstraint(item: view1, attribute: NSLayoutAttribute.leading, relatedBy: NSLayoutRelation.equal, toItem: view2, attribute: NSLayoutAttribute.leading, multiplier: 1.0, constant: 0.0)
        view1.addConstraints([constraint1, constraint2, constraint3, constraint4])
    }
}

Note we’re explicitly specifying that this extension applies only to UIViewControllers. This constraint allows us to use self in our methods, and the compiler considers self as an instance of UIViewController.

With having this method and extension defining the parent view controller can be shorten to:

class ParentViewController: UIViewController, ReusableHost {
    @IBOutlet weak var containerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        addReusableViewController()
    }
}

Awesome, eh? We’ve just encapsulated the logic of adding a child view controller into the child view controller itself!

Github repo: https://github.com/agordeev/ReusableViewController