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.
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.