Protocol-Oriented Segue Identifiers in Swift
Back in August, I watched the phenomenal Swift in Practice WWDC15 Session. I wrote about the first part of the video that impressed me – A Beautiful Solution to Non-Optional UIImage Named in Swift – and I’m finally getting around to writing about the second and even more exciting part of that video (after all, holidays are best for catching up on blogging!).
This time, I’ll be writing about an elegant solution to handling multiple segue identifiers. You guessed it! There will be protocols.
So today, we’re going on a journey of your choice. You choose: the red pill or the blue pill…
The Problem
Unfortunately, Segue Identifiers are hard-coded String based. When you add them inΒ your Storyboard, you have to copy the string everywhere in your code – accidental misspellings are just ready to happen.
1 2 3 4 5 6 7 8 9 10 11 |
// ViewController.swift @IBAction func onRedPillButtonTap(sender: AnyObject) { // I'm hard-coding my Red Pill segue identifier here π¬ performSegueWithIdentifier("TheRedPillExperience", sender: self) } @IBAction func onBluePillButtonTap(sender: AnyObject) { // I'm hard-coding my Blue Pill segue identifier again here π¬ performSegueWithIdentifier("TheBluePillExperience", sender: self) } |
And of course, if you decide to change the name of the segue identifier in the future, you have to change it in every place it was hard-coded… more potential for accidental copy / paste accidents and misspellings.
To mitigate this, I use enums to deal with my segue identifiers whenever there is more than one segue identifier in my ViewController.
1 2 3 4 5 6 |
// ViewController.swift enum SegueIdentifier: String { case TheRedPillExperience case TheBluePillExperience } |
But that presents another set of problems… Mainly, it’s bloated and ugly:
1 2 3 4 5 6 7 8 9 10 11 |
// ViewController.swift @IBAction func onRedPillButtonTap(sender: AnyObject) { // so this is pretty long... performSegueWithIdentifier(SegueIdentifier.TheRedPillExperience.rawValue, sender: self) } @IBAction func onBluePillButtonTap(sender: AnyObject) { // and so is this... performSegueWithIdentifier(SegueIdentifier.TheBluePillExperience.rawValue, sender: self) } |
The problem becomes a lot worse when dealing with this in prepareForSegue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// ViewController.swift override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { // unwrap all the things!!! if let identifier = segue.identifier { if let segueIdentifier = SegueIdentifier(rawValue: identifier) { switch segueIdentifier { case .TheRedPillExperience: print("π") case .TheBluePillExperience: print("πΌ") } } } } |
This was my actual code before Swift 2.0. Now you can at least use guard to deal with the Pyramid of Doom, but it’s still not great:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// ViewController.swift override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { guard let identifier = segue.identifier, segueIdentifier = SegueIdentifier(rawValue: identifier) else { fatalError("Invalid segue identifier \(segue.identifier)." } switch segueIdentifier { case .TheRedPillExperience: print("π") case .TheBluePillExperience: print("πΌ") } } |
Anyway, this is the problems you’ll have to deal with in every single one of your view controllers throughout your app. So how can you clean it up? Again, you guess it! Protocols to the rescue!
The Solution
The solution is very elegant, and not something I would have come up with myself. Thanks Apple for the great architecture talks at WWDC this year. Seriously, these are awesome!
First, you create a SegueHandlerType that identifies the SegueIdentifier enum as a type:
1 2 3 4 5 6 7 8 |
// SegueHandlerType.swift import UIKit import Foundation protocol SegueHandlerType { typealias SegueIdentifier: RawRepresentable } |
Now you can use the power of protocol extensions to create methods for UIViewControllers with String-based SegueIdentifier enums:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// SegueHandlerType.swift // notice the cool use of where here to narrow down // what could use these methods. π extension SegueHandlerType where Self: UIViewController, SegueIdentifier.RawValue == String { func performSegueWithIdentifier(segueIdentifier: SegueIdentifier, sender: AnyObject?) { performSegueWithIdentifier(segueIdentifier.rawValue, sender: sender) } func segueIdentifierForSegue(segue: UIStoryboardSegue) -> SegueIdentifier { // still have to use guard stuff here, but at least you're // extracting it this time guard let identifier = segue.identifier, segueIdentifier = SegueIdentifier(rawValue: identifier) else { fatalError("Invalid segue identifier \(segue.identifier).") } return segueIdentifier } } |
Notice that the methods in the protocol extensions are not declared in the protocol – they’re not meant to be overwritten. The best use-case for this I’ve seen.
So now, using these is simple and beautiful:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// ViewController.swift import UIKit // just have your UIViewController conform to SegueHandlerType, // easy as π class ViewController: UIViewController, SegueHandlerType { // the compiler will now complain if you don't have this implemented // you need this to conform to SegueHandlerType enum SegueIdentifier: String { case TheRedPillExperience case TheBluePillExperience } override func viewDidLoad() { super.viewDidLoad() } override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { // π goodbye pyramid of doom! switch segueIdentifierForSegue(segue) { case .TheRedPillExperience: print("π") case .TheBluePillExperience: print("πΌ") } } @IBAction func onRedPillButtonTap(sender: AnyObject) { // β
this is how I want to write my code! Beautiful! performSegueWithIdentifier(.TheRedPillExperience, sender: self) } @IBAction func onBluePillButtonTap(sender: AnyObject) { performSegueWithIdentifier(.TheBluePillExperience, sender: self) } } |
Conclusion
From the video, the benefits of using the SegueHandlerType are:
- Compiler errors when adding new segues if the new case isnβt handled
- Reusable
- Convenient syntax
We also get to see the power of Protocols:
- Tighten app constraints using protocols with associated types
- Share implementation through a constrained protocol extension
The most powerful lesson here is to take advantage of the compiler. If you structure your code in this way, the compiler will work with you, warning you when you’re missing cases!
You can view the full sample code on Github here.