Reduce boilerplate while working with storyboards and xibs

Photo by Faye Cornish / Unsplash

Instantiating view controllers from storyboards and views from xib files is a pain. If you are not careful enough you will end up with a lot of fragile, string-infested boilerplate code.

If you don't write all of your UI in code you constantly have to deal with code looking like this, when trying to instantiate a view:

let view = Bundle(for: self).loadNibNamed("CustomViewXibFilename",
                                          owner: nil,
                                          options: nil)?.last as? CustomView

You have to provide the file name string, watch for typos, and do the typecasting.

Instantiating view controllers isn't better:

let storyboard = UIStoryboard(name: "Main",
                              bundle: Bundle(for: self))

let viewController = storyboard.instantiateViewController(withIdentifier: "CustomViewControllerId") as? CustomViewController

Not only you have to remember your view controller identifier but also in which storyboard it's located and do the typecasting in the end. Names and identifiers are strings so, again, watch for typos or something bad will happen.

There are a lot of ways you can deal with this but there is a pretty simple approach that worked for me better than the others. Allow me to introduce Makeable protocols which along with the protocol extensions feature have proven to be a simple yet very convenient way of handling issues I mentioned before.

Let's start with StoryboardMakeable protocol which deals with UIViewController instantiations:

public protocol StoryboardMakeable: AnyObject {
    associatedtype StoryboardMakeableType
    static var storyboardName: String { get }
    static func make() -> StoryboardMakeableType
}

public extension StoryboardMakeable where Self: UIViewController {
    static func make() -> StoryboardMakeableType {
        let viewControllerId = NSStringFromClass(self).components(separatedBy: ".").last ?? ""

        let storyboard = UIStoryboard(name: storyboardName,
                                      bundle: Bundle(for: self))

        guard let viewController = storyboard.instantiateViewController(withIdentifier: viewControllerId) as? StoryboardMakeableType else {
            fatalError("Did not find \(viewControllerId) in \(storyboardName)!")
        }

        return viewController
    }
}

First, you have to implement the protocol for a view controller and make sure the view controller identifier used in the storyboard is identical to the view controller name:

extension CustomViewController: StoryboardMakeable {
    typealias StoryboardMakeableType = CustomViewController
    static var storyboardName = "Main"
}

When this is done instantiating the view controller is as simple as:

Let customViewControler = CustomViewController.make()

Simple .make() is what it takes now. No typecasting, no dealing with strings, you only focus on what you need to instantiate and nothing more.

For the UIViews there is XibMakeable protocol:

public protocol XibMakeable: AnyObject {
    associatedtype XibMakeableType
    static func make() -> XibMakeableType
}

public extension XibMakeable where Self: UIView {
    static func make() -> XibMakeableType {
        let viewId = NSStringFromClass(self).components(separatedBy: ".").last ?? ""
        guard let view = Bundle(for: self).loadNibNamed(viewId,
                                                        owner: nil,
                                                        options: nil)?.last as? XibMakeableType
        else { fatalError("Couldn't make view from \(viewId).xib!") }

        return view
    }
}

Instantiating views is even simpler. You need to implement the protocol, make sure the xib file is named exactly the same as the view, and set Custom View properly in xib:

extension CustomView: XibMakeable {
    typealias XibMakeableType = CustomView
}

Now when you need this view you just type:

let customView = CustomView.make()

Again, super simple with a strong focus on your needs and not the details or filenames.

Extending both protocols with concrete filenames or identifiers is totally doable. If you have an existing project where you use identifiers or filenames that are different from your class names you can just add additional vars to the protocols and provide this information in extensions. Although I would encourage you to go the other way around and unify the names. I learned to value simplicity. And I had a bunch of issues before I unified filenames and identifiers.

Thank you for reading!

Kamil Tustanowski

Kamil Tustanowski

I'm an iOS developer dinosaur who remembers times when Objective-C was "the only way", we did memory management by hand and whole iPhones were smaller than screens in current models.
Poland