Storyboard Initializable

Episode #273 | 12 minutes | published on May 25, 2017 | Uses iOS-10.3, Xcode-8.3
Subscribers Only
In the refactoring series, Soroush mentioned a protocol he uses to make initializing view controllers from a storyboard as easy as adopting a protocol (and completely type-safe). In this episode we will build this using Swift protocol extensions. The end result is something you can easily carry with you from project to project.

Episode Links

Creating the Protocol

The protocol will define a few methods that can be used to fetch the appropriate storyboard and construct the view controller instance:


protocol StoryboardInitializable {
    static var storyboardName: String { get }
    static var storyboardBundle: Bundle? { get }

    static func makeFromStoryboard() -> Self
}

Note that this protocol has what Swift calls "Self Requirements". This means that the class that implements this protocol must be known at compile time. In practice, this means our view controller classes must be final.

Adding a default implementation

Most of the implementations of the above methods are obvious and every view controller would share the same behavior (with a few exceptions). We can provide this default behavior with Swift protocol extensions:

extension StoryboardInitializable where Self : UIViewController {
    static var storyboardName: String {
        return "Main"
    }

    static var storyboardBundle: Bundle? {
        return nil
    }

    static var storyboardIdentifier: String {
        return String(describing: self)
    }

    static func makeFromStoryboard() -> Self {
        let storyboard = UIStoryboard(name: storyboardName, bundle: storyboardBundle)
        return storyboard.instantiateViewController(
            withIdentifier: storyboardIdentifier) as! Self
    }
}

Now any implementers of this protocol will inherit this default behavior and things will just work. View controller defined in a different storyboard, or with a different identifier? No problem, just implement that method and override the default implementation.

Adopting the Protocol

We simply have to conform to the protocol and make our class final, to prevent subclassing:

final class NoteListViewController : UITableViewController, StoryboardInitializable {
  // ...
}

That's it!

Adding Convenience methods for UINavigationController

Often our view controllers need to be constructed this way (so we can configure them, set delegates, etc), but then need to be wrapped in a navigation controller before being presented.

We can easily add that to our protocol:


protocol StoryboardInitializable {
    // ...

    func embedInNavigationController() -> UINavigationController
    func embedInNavigationController(navBarClass: AnyClass?) -> UINavigationController
}

And also our protocol extension:


extension StoryboardInitializable where Self : UIViewController {
    // ...

    func embedInNavigationController() -> UINavigationController {
        return embedInNavigationController(navBarClass: nil)
    }

    func embedInNavigationController(navBarClass: AnyClass?) -> UINavigationController {
        let nav = UINavigationController(navigationBarClass: navBarClass, toolbarClass: nil)
        nav.viewControllers = [self]
        return nav
    }
}

Putting it all together

Usage is simple:

let noteListVC = NoteListViewController.makeFromStoryboard().embedInNavigationController()
blog comments powered by Disqus