Upcoming and OnDemand Webinars View full list

Using Swift Enumerations Makes Segues Safer

Matt Mathias

Swift style encourages developers to use the compiler to their advantage, and one of the ways to accomplish this is to leverage the type system. In many cases, doing so can feel fairly obvious, but working with UIKit can be challenging since it often hands you String instances to identify view controllers, storyboards and so on. We have received some guidance on this issue in the form of a WWDC session in 2015, but it’s a good idea to revisit the problem to continue our practice of thinking Swiftly.

Let’s take a look at storyboard segues as our example, which can be especially tricky. One of the difficulties arises from the fact that UIKit requires that we use a String to identify the segue that we want to use. That means, in a sense, that UIStoryboardSegues are “stringly” typed. This makes it difficult to work with segues in a type-safe manner, and can lead to a lot of repetitive code. How can we address this problem?

A Naive Approach to Storyboard Segues

You may have seen something like this out in the wild. You may have even written this code before (gasp!).

// Sitting somewhere in the source for a UIViewController

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    guard let identifier = segue.identifier else {
        assertionFailure("Segue had no identifier")
        return
    }

    if identifier == "showPerson" {
        let person = Person(name: "Matt")
        let personVC = segue.destination as! PersonViewController
        personVC.person = person
    } else if identifier == "showProduct" {
        let product = Product(title: "Book")
        let productVC = segue.destination as! ProductViewController
        productVC.product = product
    } else {
        assertionFailure("Did not recognize storyboard identifier")
    }

}

Here, we have a prepare(for:sender:) method overridden in a UIViewController.
It grabs the identifier from the inbound segue, and then matches against a couple of hardcoded strings.
These strings, like "showPerson" and "showProduct", will match a segue seeking to show a PersonViewController or a ProductViewController.

This approach is overly mechanical, which can lead to buggy code.
It is easy to mistype the segue identifiers.
It is also easy to forget to add a new segue identifier to this list.
Forgetting to capture an identifier in one of the else clauses above would make for a likely crash in the next view controller.

Notice that we use assertionFailure() above and not preconditionFailure().
assertionFailure() will be caught in debug mode, whereas preconditionFailure() will crash in both debug and release build configurations.
This is perfect for testing your application while you are developing it.
Ideally, we aim to capture all of these sorts of bugs during our development cycle.
In the unfortunate circumstance that we do not, it is worth it to let our segue proceed as usual.
It’s possible that things will go okay and that the subsequent view controller will have the data it needs—of course, this depends upon the data and circumstances at hand, but it is good to let the app proceed if it makes sense.
Preemptively crashing with preconditionFailure() is not always the best option.

A Type-safe Approach

There has to be a better way.
What we are looking for is something that will help us to catch these errors before we ever get to the runtime.
How can we catch this sort of error before we get there?

Enumerations Make Segues Safer

Let’s take a moment to examine the if/else statement we have above.
Notice that we have several clauses; this is no simple if/else.
A general rule of thumb to recall is that if an if/else statement has multiple clauses, then it may be suitable to replace it with a switch statement.
Let’s see what that looks like.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    guard let identifier = segue.identifier! else {
        assertionFailure("Segue had no identifier")
        return
    }

    switch identifier {
    case "showPerson":
        let person = Person(name: "Matt")
        let personVC = segue.destination as! PersonViewController
        personVC.person = person

    case "showProduct":
        let product = Product(title: "Book")
        let productVC = segue.destination as! ProductViewController
        productVC.product = product

    default:
        assertionFailure("Did not recognize storyboard identifier")
    }

}

Refactoring to a switch statement helps us to see the path forward.
Switching over a String like identifier feels a little buggy.
Instead of switching over a String, we’d rather switch over something whose set of possible values is more determined.
Enumerations are perfect in this scenario.
Let’s think about what is needed.

Obviously, we need to replace to replace the identifier String with an enumeration case.
We can do this with an enumeration whose raw values are Strings.
This will allow us to define a type whose cases comprise the segues we anticipate to be passed to prepare(for:sender:).
It will also yield an enumeration whose cases are backed by String instances.
String raw values will be nice when we need to match against the incoming segue’s identifier.

Here’s an enumeration that defines cases for the segues we have seen so far.

extension ViewController {

    enum ViewControllerSegue: String {
        case showPerson
        case showProduct
    }

}

I like to define this enumeration as a nested type within its associated UIViewController.
It helps to make the relationship clear: The view controller is what helps to prepare for a segue before it is performed.
In the example at hand, ViewControllerSegue is simply there to help out by providing comprehensive information to the compiler.

How does this work?
Well, we need to modify our override of prepare(for:sender:) above.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    guard let identifier = segue.identifier,
            let identifierCase = ViewController.ViewControllerSegue(rawValue: identifier) else {
        assertionFailure("Could not map segue identifier -- (segue.identifier) -- to segue case")
        return
    }

    switch identifierCase {
    case .showPerson:
        let person = Person(name: "Matt")
        let personVC = segue.destination as! PersonViewController
        personVC.person = person

    case .showProduct:
        let product = Product(title: "Book")
        let productVC = segue.destination as! ProductViewController
        productVC.product = product
    }

}

This implementation of prepare(for:sender:) leverages our new enumeration.
Its first task is to get the identifier associated with the segue and transform it into an instance of ViewControllerSegue.
If this fails, then you assertionFailure() with the relevant debug information.
Otherwise, you have the information you need to safely switch over the segue.identifier.

Finally, the switch we use exhaustively checks all of the possible cases.
This helps in a couple of ways.
First, the switch allows us to avoid repeating the calls to assertionFailure().
We are able to avoid this redundancy because the switch can determine whether or not we are covering all of the enumeration’s cases.

Second, this switch can also warn us if we forget to cover a new segue.
The best path here is that we remember to add a new case to our ViewControllerSegue enumeration.
Doing so will trigger the compiler to issue an error in the above switch if it does not cover the new case.
If we forget to add the new segue identifier case to our enumeration, then we will hit the assertionFailure() within our guard statement during our testing.
Either way, the above code is less repetitive and gives more information to the compiler to help us avoid bugs at runtime.

Eliminating Redundancy Through Protocol Extensions

While we are on the topic of making our code less repetitive, let’s rethink our approach.
Currently, every view controller that wants to take advantage of the segue enumeration will need to remember to do two things.
First, it will need to provide an enumeration with cases for all of the segues that it needs to handle.
Second, it will need to write the guard statement above in prepare(for:sender) to map the segue.identifier to a segue case in the enumeration.
That will get a little tedious to remember to type every time we make a new UIViewController subclass.

Protocol extensions can help to alleviate this issue.

protocol SegueHandler {

    associatedtype ViewControllerSegue: RawRepresentable
    func segueIdentifierCase(for segue: UIStoryboardSegue) -> ViewControllerSegue?

}

The protocol above uses an associatedtype named ViewControllerSegue. Conforming types will have to provide a nested type of the same name enumerating all of the segues the view controller expects to handle. These nested types will have to be RawRepresentable, which will work well with our String backed segue cases. Last, the protocol requires a method that will take a UIStoryboardSegue and map to an of the nested enumeration. This method returns an optional to handle the scenario of not being able to map the segue.identifier to a specifc case on the nested enumeration.

We can use a protocol extension to provide a default implementation of the required method above.

extension SegueHandler where Self: UIViewController, ViewControllerSegue.RawValue == String {

    func segueIdentifierCase(for segue: UIStoryboardSegue) -> ViewControllerSegue {
        guard let identifier = segue.identifier,
            let identifierCase = ViewControllerSegue(rawValue: identifier) else {
            return nil
        }
        return identifierCase
    }

}

Now all you have to do on your UIViewController subclasses is to declare conformance to the SegueHandler protocol.
Doing so will nudge the compiler to issue you an error if your view controller does not provide the nested type ViewControllerSegue.
(Remember that you listed this requirement in the protocol via an associatedtype.

Now, heading back to ViewController, our prepare(for:segue) method looks like so:

// extension ViewController: SegueHandler {} somewhere in the file...

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    guard let identifierCase = segueIdentifierCase(for: segue) else {
        assertionFailure("Could not map segue identifier -- (segue.identifier) -- to segue case")
        return
    }

    switch identifierCase {
    case .showPerson:
        let person = Person(name: "Matt")
        let personVC = segue.destination as! PersonViewController
        personVC.person = person

    case .showProduct:
        let product = Product(title: "Book")
        let productVC = segue.destination as! ProductViewController
        productVC.product = product
    }

}

This is a bit nicer, but it can get better.
We’ve written a protocol and protocol extension to handle the mapping from segue.identifier to segue enumeration case.
This is great because conforming UIViewController subclasses will get some help from the compiler to ensure that we provide the correct enumeration for our view controller’s segues.
But we still have a guard statement above, and that’s because we currently have segueIdentifierCase(for:) returning an optional.
It would be nicer to not have to worry about optionals.

What about Segues with No Identifiers?

Before we update segueCaseIdentifier(for:) to not return an optional, let’s talk about segues without identifiers.
Our current approach means that we will trap in the guard statement above if our segue doesn’t have an identifier.
This really isn’t a problem because all of our segues should have an identifier.
But, you say, I dont need an identifier because I’m not passing any data to the next view controller.
I just need to show it.

Okay, okay.
Unnamed segues have an empty String identifier: "".
Let’s add a new unnamed case to ViewControllerSegue.

extension ViewController {

    enum ViewControllerSegue: String {
        case showPerson
        case showProduct
        case unnamed = ""
    }

}

The compiler is now bugging us that we need to make our switch statement exhaustive in prepare(for:sender:) above, so let’s add the new case.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    guard let identifierCase = segueIdentifierCase(for: segue) else {
        assertionFailure("Could not map segue identifier -- (segue.identifier) -- to segue case")
        return
    }

    switch identifierCase {
    case .showPerson:
        let person = Person(name: "Matt")
        let personVC = segue.destination as! PersonViewController
        personVC.person = person

    case .showProduct:
        let product = Product(title: "Book")
        let productVC = segue.destination as! ProductViewController
        productVC.product = product

    case .unnamed:
        assertionFailure("Segue identifier empty; all segues should have an identifier.")
    }

}

Notice that we use assertionFailure() again to accomodate for the possibility that our app won’t crash at runtime, but also to give us the nudge during our development that we really ought to provide an identifier to the segue.

Now that we have a new case, we are in a good position to revisit our default implementation of segueCaseIdentifier(for:).

extension SegueHandler where Self: UIViewController, ViewControllerSegue.RawValue == String {

    func segueIdentifierCase(for segue: UIStoryboardSegue) -> ViewControllerSegue {
        guard let identifier = segue.identifier,
            let identifierCase = ViewControllerSegue(rawValue: identifier) else {
            fatalError("Could not map segue identifier -- (segue.identifier) -- to segue case")
        }
        return identifierCase
    }

}

This new default implementation uses fatalError().
Why did we make this change?

The answer has to do with the new ViewControllerSegue.unnamed case.
This case acts as a kind of friendly default case for our switch statement.
All unnamed segues will match against this case.
I call this a “friendly” default case because it won’t ruin our switch’s attempts at being exhaustive.
If we add a new case, then the compiler will see that our switch doesn’t match against it and issue an error.

Therefore, segueIdentifierCase(for:) should never fail to generate a ViewControllerSegue.
If it receives an empty string (in the case of a segue without an identifier), then it will create an instance of ViewControllerSegue set to .unnamed.
It may receive a segue with an identifier that doesn’t match a case in our enumeration, which would be bad.
After all, we have an .unnamed case, which means that we purposefully chose to give the segue an identifier.
That suggests we need to pass that view controller some data.
This sounds like an unrecoverable error, and so crashing is the right way to go.

We can finally head back to prepare(for:sender:) to remove the optional unwrapping and streamline our code.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    switch segueIdentifierCase(for: segue) {
    case .showPerson:
        let person = Person(name: "Matt")
        let personVC = segue.destination as! PersonViewController
        personVC.person = person

    case .showProduct:
        let product = Product(title: "Book")
        let productVC = segue.destination as! ProductViewController
        productVC.product = product

    case .unnamed:
        assertionFailure("Segue identifier empty; all segues should have an identifier.")
    }

}

Since segueIdentifierCase(for:) doesn’t return an optional, we can simply switch over its result.

Wrapping Up

UIKit’s API often expects to receive and hands back String instances to interface with storyboards, view controllers and segues.
This can lead to buggy code. For example, it’s easy to mistype the String used to identify the item you need.

One useful way we can improve our interaction with UIKit is to leverage Swift’s type system to limit our options.
Enumerations are especially suited for this work as they define a precise listing of options for a type.
Swift’s enumerations work perfectly here because we can back each case’s raw value with a String that corresponds to a specifc resource.

We can also use protocols and protocol extensions to help. Using these help to remove redundancy in our code. They also leverage the compiler’s knowledge of the protocol’s requirements to remind us to write safer code.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project