Swift: UIStoryboard Protocol

Because String literals are so yucky.

A couple weeks ago I came across a great post on by Guille Gonzalez for which he had devised a way to make UITableViewCell registration and reuse to be much safer with protocols and extensions.

I saw this post and was in absolute awe about how simple and easy it was to implement custom behaviour with protocol extensions and generics without having to rely on inheritance. Ever since WWDC15, we’ve been hearing about how Swift is a protocol orientated language, and part of me got it but didnt quite get it, if you know what I mean. This was the moment in time where I finally understood what they were talking about.

The app I spend most of my time working on had one big storyboard, which was incredibly cumbersome to work with so I finally decided to split it up. After separating the mega UIStoryboard into lots of little UIStoryboard’s, I was then left with relying on several different string literals to instantiate UIStoryboard instances all throughout my code, which is never safe.

String Literals

The silent killer you let into your home

let name = "News"
let storyboard = UIStoryboard(name: name, bundle: nil)
let identifier = "ArticleViewController"
let viewController =
storyboard.instantiateViewController(withIdentifier: identifier)
as! ArticleViewController

In the code above, we create an instance of UIStoryboard with the name “News”, which will look for the “News.storyboard” file in the project’s resources. But what if the storyboard had a more complex name such as “Onomatopoeia”, it’s a word so weird and uncommon I actually had to look up how to spell it just for this post. Imagine if I repeatedly gambled on guessing how to spell it in production code, it’s a silly idea but people really do such crazy things.

There’s always a chance for a typo each time you type out that string literal. To make matters worse, Xcode’s syntax checker won’t pick up on it because it’s a string, and strings don’t get checked. So now you’re left with a possible run time error. Ugh.

So how do we make UIStoryboard safer?

Global constant string literals

No, not ever. At first it sounds like a good idea, because you only have to define the constant once and it’s available everywhere. If you want to change the value, theres only one place in code to change for which the effects will cascade throughout the project.

But then you have one less variable name to work with, you’d be surprised how often you might want to reuse a variable name. Ever tried naming a variable description on a class that inherits from NSObject? Then you know what I mean. If there are multiple constant string literal identifiers for storyboards, uniformity can easily be lost, or they could be defined in separate parts of the project, making them harder to find or merge together.

A few other reasons exist on why defining a global constant string literal is a bad idea, but to get onto the good bits of the post, we’re gonna skip past them.

Relatable storyboard names

As a general rule of thumb, your storyboards should be named after the sections of which they cover. For instance, if you have a storyboard which houses view controllers related to News, name that storyboard’s file to “News.storyboard”.

Uniform storyboard identifiers

When you intend to use UIStoryboard Storyboard Identifiers on your view controllers, a good practice is to use the class name as the identifier. For example, “ArticleViewController” would be the identifier for ArticleViewController. This will reduce the burden for you and your colleagues of having to think of a unique identifier, or naming conventions as well as remembering either or.

Enums

Think of enums as uniform, centralized global string literal identifiers for UIStoryboard. To make instantiation really safe with storyboard, we can create an extension to the UIStoryboard class which defines all the different storyboard files we have in our project.

extension UIStoryboard {
enum Storyboard: String {
case main
case news
case gallery
        var filename: String {
return rawValue.capitalized
}
}
}

As you can see, everything is uniform and centrally located within our project. Instantiation is also a whole lot safer too, and Xcode will also help with auto-complete when you begin typing the identifier.

We’ve created a computed variable filename because our files will be be capitalised. So we simply get the rawValue and capitalise the first letter

let storyboard = UIStoryboard(
name: UIStoryboard.Storyboard.News.filename,
bundle: nil)

This code will compile and run without any hassles, but the syntax is quite ugly. So let’s take this further even further and reduce our syntax by creating our own convenience initializer, and adding it to the UIStoryboard extension:

convenience init(storyboard: Storyboard, bundle: Bundle? = nil) {
self.init(name: storyboard.filename, bundle: bundle)
}
...
let storyboard = UIStoryboard(storyboard: .news)

As you will notice, we’ve made the default value for the bundle: argument to be nil, thus making it optional to omit the bundle: argument completely when the initializer is called.

Reason being because if you supply nil to the bundle argument, UIStoryboard class will look inside the main bundle for the resource, which makes nil the same as supplying Bundle.main to the bundle argument, as stated in the Apple documentation:

The bundle containing the storyboard file and its related resources. If you specify nil, this method looks in the main bundle of the current application.
UIStoryboard Class Reference

An alternative to convenience initializers is to just create class functions for UIStoryboard that return an instance of UIStoryboard.

class func storyboard(storyboard: Storyboard, bundle: Bundle? = nil) -> UIStoryboard {
return UIStoryboard(name: storyboard.filename, bundle: bundle)
}
...
let storyboard = UIStoryboard.storyboard(.news)

Whether you choose convenience initializers or class methods, both produce the same outcome. I guess the only difference is personal taste over syntax style, in my opinion I think the class functions look nicer, so I tend to use them in my own code. Whatever you choice is, just make sure it’s uniform throughout your project.

OK, let’s turn this up to 11 by throwing in the things I originally baited you to this post with.

Protocol Extensions and Generics

Generally projects won’t have that many storyboard files, even if we have 20 storyboard files, it’s still easily maintainable with the solutions provided above. View controllers on the other hand are a whole different story. Doing a quick search of my work’s Xcode project, I find that we are currently using over 100 different subclasses of UIViewController. This is a problem.

let storyboard = UIStoryboard.storyboard(.news)
let identifier = "ArticleViewController"
let viewController = 
storyboard.instantiateViewController(withIdentifier: identifier)
as! ArticleViewController

Now we have to deal with managing not only storyboard identifiers in code and Interface Builder, but now type casting is thrown into the mix, because the function only returns UIViewController:

func instantiateViewController(withIdentifier identifier: String) -> UIViewController

Because we have so many subclasses of UIViewController, the enum solution we used for UIStoryboard will suffice a lot better than string identifiers, but it is still too cumbersome to manage for the amount of view controllers that exist within the project.

StoryboardIdentifiable protocol

protocol StoryboardIdentifiable {
static var storyboardIdentifier: String { get }
}

We’ve made a protocol which gives any class that conforms to it, a static variable, storyboardIdentifier. This will reduce the amount of work we have to do when managing identifiers for view controllers.

StoryboardIdentifiable protocol extension

extension StoryboardIdentifiable where Self: UIViewController {
static var storyboardIdentifier: String {
return String(describing: self)
}
}

In our protocol extension declaration, there is a where clause which makes it only apply to classes that are either UIViewController or it’s subclasses. This will stop other classes such as NSDate from getting a storyboardIdentifier protocol variable.

Inside the protocol extension, we’re providing a method to get the storyboardIdentifier string dynamically from the class at runtime.

StoryboardIdentifiable global conformance

extension UIViewController: StoryboardIdentifiable { }

We’ve now made it so every UIViewController within our project conforms to the StoryboardIdentifiable protocol. This just alleviates us from updating every UIViewController class to conform to the new protocol, as well as having to remember to make new classes conform.

class ArticleViewController: UIViewController { }
...
print(ArticleViewController.storyboardIdentifier)
// prints: ArticleViewController

UIStoryboard extension with generics

func instantiateViewController<T: UIViewController>() -> T  
where T: StoryboardIdentifiable

We’re getting rid of the previous way to instantiate view controllers from a storyboard with string literal storyboard identifiers and replacing it with a new, much safer way. Behold:

We’re using generics here which only allow us to pass in classes that are either UIViewController or subclasses of, and there’s also a where statement included in the generics declaration that limit the compatible arguments to those classes that conform to the StoryboardIdentifiable protocol.

If we tried passing in an NSObject, Xcode wouldn’t compile. Or if we tried passing in a UIViewController that didn’t conform to the StoryboardIdentifiable protocol, that too would stop Xcode from compiling. Already this is much safer. #winning.

<T: UIViewController>() -> T 
where T: StoryboardIdentifiable
Yo! What’s up with all this strange syntax?

By common convention, generics typically have the parameter name of T, however, you can replace this with whatever you want when you first declare it inside the angle braces. If we wanted to, we could rename T to something a bit more readable such as VC or ViewController:

<VC: UIViewController>() -> VC 
where VC: StoryboardIdentifiable

Whatever you do, it just has to be uniform throughout the declaration and inside the body. But for this example, we’re gonna stick with T because it’s the Swift convention you will most likely come across in other code and examples.

Note: To learn a bit more about Swift’s generics, head over to the documentation.

Back to the breakdown:

let optionalViewController =  
instantiateViewController(withIdentifier: T.storyboardIdentifier)

We’re calling the original UIStoryboard instantiateViewController API and passing it the storyboardIdentifier variable, which will return an optional UIViewController

guard let viewController = optionalViewController as? T else {
fatalError(“Couldn’t instantiate view controller with identifier \(T.storyboardIdentifier)“)
}
return viewController

We attempt unwrap the optional UIViewController and cast it as the same class of which we passed in. If for whatever reason the view controller doesnt exist within the storyboard instance that’s calling it, a fatalError will occur and the console will notifiy you during debugging time so these kind of mistakes wont slip into production releases.

Finally, we return the unwrapped viewController of type T to the caller.

In Practice

class ArticleViewController: UIViewController
{
func printHeadline() { }
}
...
let storyboard = UIStoryboard.storyboard(.news)
let viewController: ArticleViewController = storyboard.instantiateViewController()
viewController.printHeadline()
presentViewController(viewController, animated: true, completion: nil)

So there you have it, we’ve managed to get rid of ugly, unsafe string literals as identifiers by replacing them with enums, protocol extensions and generics.

Also, we’re able to instantiate a specific type of view controller through UIStoryboard functions and perform class specific actions on it without typecasting. Isn’t this just the best thing you’ve seen all day?


Updates

Thanks to feedback from Raifura Andrei and Kyle Davis, I’ve updated the article and example codes to reduce syntax and improve readability. Github and Gists have also been updated. Enjoy.


Sample code of this post can be found on GitHub.


If you like what you’ve read today you can check our my other articles or want to get in touch, please send me a tweet or follow me on Twitter, it really makes my day. I also organise Playgrounds Conference in Melbourne, Australia and would to see you at the next event.