iOS Week: Safely typed segues and storyboards

So you use storyboards and you find yourself wanting to instantiate view controllers from one. The API to do this isn’t the nicest. First you have to create your UIStoryboard object, passing a string to define what storyboard you want. Then you pass another string in, and force unwrap to the UIViewController you want – yuck!

Screen Shot 2018-03-27 at 21.39.42.png

So instead we can create an enum and extension on UIStoryboard like this:

Screen Shot 2018-03-27 at 21.37.48.png

Each enum case is a different storyboard bundled in our app. Now we can do the exact same thing like this:

Screen Shot 2018-03-27 at 21.41.13.png

A much cleaner and safer approach.

We can also extend this to segues. The segue API is also kinda ugly, so we can create another enum and extension to clean this up for us!

Screen Shot 2018-03-27 at 21.42.36.png

Each segue that you make with an identifier, go ahead and put it in this enum as a new case. Now to perform a segue in a view controller, all we need to do is this:

Screen Shot 2018-03-27 at 21.44.09.png

The last piece of the puzzle is that pesky prepare(for segue:) method. In there you check the identifier as a string, check the destination, unwrap as the destination – yuck! And if that view controller is wrapped in a navigation controller you have to check that too and then get it out of the navigation controller. That’s a lot of boiler plate. So we can simplify it using the extension above. Now, we can just do this:

Screen Shot 2018-03-27 at 21.46.18.png

If this destination is a UINavigationController, it will automatically reach in and give you the correct view controller you want.


import UIKit
enum Storyboard: String {
case main
var value: String {
return self.rawValue.capitalized
}
}
extension UIStoryboard {
convenience init(_ name: Storyboard) {
self.init(name: name.value, bundle: nil)
}
func instantiateViewController<T: UIViewController>() -> T {
if let name = NSStringFromClass(T.self).components(separatedBy: ".").last, let vc = instantiateViewController(withIdentifier: name) as? T {
return vc
}
fatalError("Could not find " + String(describing: T.self))
}
}
enum Segue: String {
case showLogin
}
extension UIViewController {
func perform(segue: Segue) {
performSegue(withIdentifier: segue.rawValue, sender: nil)
}
}
extension UIStoryboardSegue {
func destination<T: UIViewController>(matching segue: Segue, as vc: T.Type) -> T? {
if identifier != segue.rawValue { return nil }
if let nav = destination as? UINavigationController {
return nav.viewControllers.first as? T
}
return destination as? T
}
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s