There are hundreds of complex routing frameworks and libraries in iOS. Usually they’re overly complex to retrofit into an existing app or they completely bypass Storyboards. In this post, I’d like to offer a simple, native-like routing mechanism that leverages Storyboards like a boss to handle navigation.

The “Normal” Way

Let’s examine the “normal” way of handling navigation between view controllers. First, avoid segue’s at all costs since they lock you into a certain navigation flow that’s rigid and inflexible. Instead, we’ll create an instance of the target view controller and then use the show or present API’s of UIViewController against it to handle the navigation.

To do this, let’s stick with a feature-based app structure and create one storyboard-per-view-controller. Here’s what our sample app would look like:

Once we add the view controller onto the storyboard via Interface Builder, assign the class to it, and checkmark “Is Initial View Controller”, we can create an instance of the view controller by first getting a reference to the storyboard and calling the instantiateInitialViewController API from it:

let storyboard = UIStoryboard(name: "Login", bundle: nil) guard let controller = storyboard.instantiateInitialViewController() else { fatalError("Invalid controller for storyboard.") } show(controller, sender: nil)

Since we have to route the user several times within the app life cycle, the above code can get verbose and it isn’t compile-safe either.

The Routable Protocol Way

In the WWDC 2015 talk called “Swift in Practice“, Apple engineers outlined how to make segue identifiers strongly-typed by creating a protocol with an associated RawRepresentable type that others must conform to:

protocol SegueHandlerType { associatedtype SegueIdentifier: RawRepresentable }

We’re throwing segues out the window, but we can still use this clever implementation to handle the storyboard routing:

protocol Routable { associatedtype StoryboardIdentifier: RawRepresentable }

Let’s move our original “normal” routing code above to a protocol extension to abstract it away:

extension Routable where Self: UIViewController, StoryboardIdentifier.RawValue == String { func show(storyboard: StoryboardIdentifier) { let storyboard = UIStoryboard(name: storyboard.rawValue, bundle: nil) guard let controller = storyboard.instantiateInitialViewController()) else { return assertionFailure("Invalid controller for storyboard \(storyboard).") } show(controller, sender: self) } }

Now we can make our view controller conform to the Routable protocol and provide its enum of storyboards, then feed the enum case to the show API:

class LoginViewController: UIViewController { @IBAction func loginTapped() { show(storyboard: .profile) } } extension LoginViewController: Routable { enum StoryboardIdentifier: String { case profile = "Profile" case more = "More" } }

You can use `show(storyboard: .profile)` a dozen of times and is compile-safe plus sleek.

Routable Micro-Library

Let’s add sugar and spice to make this more reusable and flexible:

public protocol Routable { associatedtype StoryboardIdentifier: RawRepresentable func present<T: UIViewController>(storyboard: StoryboardIdentifier, identifier: String?, animated: Bool, modalPresentationStyle: UIModalPresentationStyle?, configure: ((T) -> Void)?, completion: ((T) -> Void)?) func show<T: UIViewController>(storyboard: StoryboardIdentifier, identifier: String?, configure: ((T) -> Void)?) func showDetailViewController<T: UIViewController>(storyboard: StoryboardIdentifier, identifier: String?, configure: ((T) -> Void)?) } public extension Routable where Self: UIViewController, StoryboardIdentifier.RawValue == String { /** Presents the intial view controller of the specified storyboard modally. - parameter storyboard: Storyboard name. - parameter identifier: View controller name. - parameter configure: Configure the view controller before it is loaded. - parameter completion: Completion the view controller after it is loaded. */ func present<T: UIViewController>(storyboard: StoryboardIdentifier, identifier: String? = nil, animated: Bool = true, modalPresentationStyle: UIModalPresentationStyle? = nil, configure: ((T) -> Void)? = nil, completion: ((T) -> Void)? = nil) { let storyboard = UIStoryboard(name: storyboard.rawValue) guard let controller = (identifier != nil ? storyboard.instantiateViewController(withIdentifier: identifier!) : storyboard.instantiateInitialViewController()) as? T else { return assertionFailure("Invalid controller for storyboard \(storyboard).") } if let modalPresentationStyle = modalPresentationStyle { controller.modalPresentationStyle = modalPresentationStyle } configure?(controller) present(controller, animated: animated) { completion?(controller) } } /** Present the intial view controller of the specified storyboard in the primary context. Set the initial view controller in the target storyboard or specify the identifier. - parameter storyboard: Storyboard name. - parameter identifier: View controller name. - parameter configure: Configure the view controller before it is loaded. */ func show<T: UIViewController>(storyboard: StoryboardIdentifier, identifier: String? = nil, configure: ((T) -> Void)? = nil) { let storyboard = UIStoryboard(name: storyboard.rawValue) guard let controller = (identifier != nil ? storyboard.instantiateViewController(withIdentifier: identifier!) : storyboard.instantiateInitialViewController()) as? T else { return assertionFailure("Invalid controller for storyboard \(storyboard).") } configure?(controller) show(controller, sender: self) } /** Present the intial view controller of the specified storyboard in the secondary (or detail) context. Set the initial view controller in the target storyboard or specify the identifier. - parameter storyboard: Storyboard name. - parameter identifier: View controller name. - parameter configure: Configure the view controller before it is loaded. */ func showDetailViewController<T: UIViewController>(storyboard: StoryboardIdentifier, identifier: String? = nil, configure: ((T) -> Void)? = nil) { let storyboard = UIStoryboard(name: storyboard.rawValue) guard let controller = (identifier != nil ? storyboard.instantiateViewController(withIdentifier: identifier!) : storyboard.instantiateInitialViewController()) as? T else { return assertionFailure("Invalid controller for storyboard \(storyboard).") } configure?(controller) showDetailViewController(controller, sender: self) } } public extension UIStoryboard { /** Creates and returns a storyboard object for the specified storyboard resource file in the main bundle of the current application. - parameter name: The name of the storyboard resource file without the filename extension. - returns: A storyboard object for the specified file. If no storyboard resource file matching name exists, an exception is thrown. */ convenience init(name: String) { self.init(name: name, bundle: nil) } }

Notice I’ve added show and present API’s and a trailing closure to configure the controller before and after its loaded so I can use it like this:

class ProfileViewController: UIViewController { @IBAction func moreTapped() { show(storyboard: .more) { (controller: MoreViewController) in controller.someProperty = "\(Date())" } } } extension ProfileViewController: Routable { enum StoryboardIdentifier: String { case more = "More" case login = "Login" } }

I pushed this into another library so it will be maintained there going forward. For a complete sample app, you can download a working demo here.

Happy Coding!!