The power of switch statements in Swift

While switch statements are hardly something that was invented as part of Swift (in fact, according to Wikipedia, the concept dates back as far as to 1952), they are made a lot more powerful when combined with Swift's type system.

The thing I like the most about switch statements is that they enable you to easily act on different outcomes of a given expression using a single statement. Not only does this usually lead to code that is easier to read & debug, but can also enable us to make our control flows more declarative and bound to a single source of truth.

As an example, let's take a look at how we may handle a user's login state in an app. Using chained if and else statements, we can construct a procedural control flow like this:

if user.isLoggedIn {
    showMainUI()
} else if let credentials = user.savedCredentials {
    performLogin(with: credentials)
} else {
    showLoginUI()
}

However, if we instead apply one of the techniques from "Modelling state in Swift", and model our user login state using an enum, we can simply bind various actions to various states using a switch statement, like this:

switch user.loginState {
case .loggedIn:
    showMainUI()
case .loggedOutWithSavedCredentials(let credentials):
    performLogin(with: credentials)
case .loggedOut:
    showLoginUI()
}

The main advantage of such an approach, is that we get a compile-time guarantee that all states and outcomes of a given expression are handled. When a new state is introduced, a new action matching it needs to be defined as well.

While something like the above - switching on single enum values - is by far the most common use of switch statements, this week - let's go further beyond that and take a look at more of the powerful capabilities that switch statements offer in Swift.

Switching on tuples

A technique that has become quite popular in Swift is to use a Result type to express various outcomes of an operation. For example, we might define a generic Result enum in our app that can hold either a value or an error that occurred while performing an operation:

enum Result {
    case success(Value)
    case error(Error)
}

Now let's say we want to make our Result type conform to Equatable. There are a number of ways this can be implemented, including nested switch statements, or creating some form of hash value or identifier for an instance and comparing those. However, there's a very simple way this can be done using a single switch statement, by combining both sides of the equality operator into a tuple, like this:

extension Result: Equatable {
    static func ==(lhs: Result, rhs: Result) -> Bool {
        switch (lhs, rhs) {
        case (.success(let valueA), .success(let valueB)):
            return valueA == valueB
        case (.error(let errorA), .error(let errorB)):
            return errorA == errorB
        case (.success, .error):
            return false
        case (.error, .success):
            return false
        }
    }
}

Just like the initial example with the login state handling code, the above also has the advantage of being very future proof - if a new result case is added, the compiler forces us to update our Equatable implementation.

Using pattern matching

One of the lesser known aspects of Swift is just how powerful its pattern matching capabilities are. Let's say that we are using the Result type from the previous section in a network request that can either produce Data or an error.

We have setup our backend to return a 401: Unauthorized error whenever the current user has been logged out or deactivated, and we want to handle that explicitly in our code, to be able to also log the user out client side if such a response is received.

Rather than using multiple if statements and if let conditional casting, we can use pattern matching on any encountered error and handle all cases using a single switch statement, like this:

switch result {
case .success(let data):
    handle(data)
case .error(let error as HTTPError) where error == .unauthorized:
    logout()
case .error(let error):
    handle(error)
}

As you can see above, we pattern match error against a HTTPError type, which lets us compare directly against its cases in a very type-safe way, without having to use any form of casting. When using pattern matching like this, cases are evaluated in a cascading top-to-bottom way, which is why we put the "catch all" error handling case at the bottom.

Switching on a set

A while ago I discovered a new interesting use case for switch statements, and when sharing it on Twitter, it seems like this was new for a lot of other people as well. It turns out that you can switch on more kinds of values than just enum cases, or primitives such as String and Int.

For example, let's say that we're building a game or a map view where roads can be connected using tiles in a grid. We might model such a tile using a RoadTile class, and by maintaining a Set with directions in which the tile is connected to other road tiles, we can write very declarative rendering code by actually switching directly on that set, like this:

class RoadTile: Tile {
    var connectedDirections = Set()

    func render() {
        switch connectedDirections {
        case [.up, .down]:
            image = UIImage(named: "road-vertical")
        case [.left, .right]:
            image = UIImage(named: "road-horizontal")
        default:
            image = UIImage(named: "road")
        }
    }
}

Compare the above to the procedural way of writing the same control flow using chained if statements:

func render() {
    if connectedDirections.contains(.up) && connectedDirections.contains(.down) {
        image = UIImage(named: "road-vertical")
    } else if connectedDirections.contains(.left) && connectedDirections.contains(.right) {
        image = UIImage(named: "road-horizontal")
    } else {
        image = UIImage(named: "road")
    }
}

I personally really prefer the switch version πŸ‘

Switching on a comparison

For the final example in this post, let's take a look at how we can use switch statements to make dealing with operator outcomes cleaner as well. Let's say that we're building a 2-player game in which players battle to get the highest score. To indicate whether the local player is in the lead or not, we want to display a text based on comparing the score of both players.

Let's again first take a look at how that can be done with chained if and else statements:

if player.score < opponent.score {
    infoLabel.text = "You're losing 😒"
} else if player.score > opponent.score {
    infoLabel.text = "You're winning πŸŽ‰"
} else {
    infoLabel.text = "You're tied 😬"
}

Just like the other examples, the above totally works, but it would be super nice to be able to just react to the outcome of a single expression, like this:

switch player.score.compare(to: opponent.score) {
case .equal:
    infoLabel.text = "You're tied 😬"
case .greater:
    infoLabel.text = "You're winning πŸŽ‰"
case .less:
    infoLabel.text = "You're losing 😒"
}

To achieve the above, we can move our chained comparisons into an extension on Int - which enables us to define a reusable way of easily handling various comparison outcomes. Here's what such an extension could look like:

extension Int {
    enum ComparisonOutcome {
        case equal
        case greater
        case less
    }

    func compare(to otherInt: Int) -> ComparisonOutcome {
        if self < otherInt {
            return .less
        }

        if self > otherInt {
            return .greater
        }

        return .equal
    }
}

As podcast guest Louis D'hauwe pointed out on Twitter, you could also define the above extension on the Comparable protocol, to make it available on more types than just Int.

Conclusion

Switch statements can be really powerful in many different situations, especially when combined with types defined using enums, sets and tuples. While I'm not saying that all if and else statements should be replaced with switch statements, there are many situations in which using the latter can make your code easier to read & reason about, as well as becoming more future proof.

What do you think? Did I miss any way to use switch statements that you find particularly useful, or do you have any questions, comments or feedback? Let me know - either here in the comments section below or on Twitter @johnsundell.

Thanks for reading! πŸš€

Using generic type constraints in Swift 4

Creating custom collections in Swift