The power of switch statements in Swift
Basics article available: EnumsWhile 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<Value: Equatable, Error: Swift.Error & Equatable> {
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<Direction>()
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 on Twitter @johnsundell.
Thanks for reading! 🚀