| Comments

The Swift Enumeration Case Pattern

Posted by Jonathan Lehr

When I first started working with enumerations and switch statements in Swift, I found them immediately easy to use. I also appreciated the usefulness of being able to add properties and methods to an enumeration. Very nice!

However there were a number of subtleties that eluded me initially, and took some time to fully appreciate. So after laying out some basics, I’d like to share a number of powerful capabilities I discovered along the way that might not seem obvious in the early going, or that might at first seem confusing.

The Basics

Defining a Trivial Enum

Initially, Swift enumerations seem similar to what most of us have experienced in other languages.

enum Size {
    case small
    case medium
    case large
    case extraLarge
}

The previous declaration can also be written more compactly:

enum Size {
    case small, medium, large, extraLarge
}

You can also define the type of a Swift enumeration, allowing each case to be mapped to a raw value that can be a string, character, or numeric type:

enum Size: String {
    case small = "S"
    case medium = "M"
    case large = "L"
    case extraLarge = "XL"

To create an instance of a enum case, you simply refer to it. In the following example, medium is an instance of Size.

let shirtSize = Size.extraLarge

Note that for enumerations of type String, Swift will automatically use case labels as raw values unless you provide explicit raw values yourself, as, for example, in the preceding declaration of Size. The code below first prints a case’s string value, followed by its raw value.

print("Size \(mySize)")
// prints "Size extraLarge"

print("Size \(mySize.rawValue)")
// prints "Size XL"

Switch Statements

An easy and obvious way to use an enum instance is in a switch statement:

switch (shirtSize) {
        case .medium: print("Shirt is size M")
        default: print("Shirt is not size M")
    }

The Pattern

Since you’re probably already familiar with switch statements from other languages, their ability to match enumeration cases may seem unsurprising. However, one subtlety is that enumeration case is generalized as a pattern in Swift.

According to The Swift Programming Language (Swift 4): Patterns , “An enumeration case pattern matches a case of an existing enumeration type. Enumeration case patterns appear in switch statement case labels and in the case conditions of if, while, guard, and for-in statements.”

In other words, you can combine the keyword case with various logic constructs, such as if statements. Precisely how to do that, though, may not be immediately obvious. We’ll come back to this point shortly in the upcoming section on Case Conditions.

The Swift language guide goes on to say: “If the enumeration case you’re trying to match has any associated values, the corresponding enumeration case pattern must specify a tuple pattern that contains one element for each associated value.” [emphasis added]

Associated Values

So what does it mean for an enumeration case to have an associated value? Consider the following enum declaration:

enum Garment {
case tie
case shirt
}

You could then use the Garment enumeration, to distinguish between shirts and ties. But, suppose you also wanted to distinguish between small shirts and large shirts? To do so, you could append a tuple declaration to the declaration of the shirt case:

enum Garment {
case tie
case shirt(size: String)
//        ^^^^^^^^^^^^^^
}

Now, initializing an instance of Garment.shirt would look almost the same as initializing a struct:

let myShirt = Garment.shirt(size: "M")

Additional cases could define tuples for their own associated values, if needed:

enum Garment {
case tie
case shirt(size: String)
case pants(waist: Int, inseam: Int)
}

let myPants = Garment.pants(waist: 32, inseam: 34)

You could then write a switch statement to perform pattern matching on myPants:

switch someGarment {
case .tie: print("tie")
case .shirt: print("shirt")
case .pants: print("pants")
}
// prints "pants"

You could also specify associated values to further refine the patterns you wish to match:

let items = [
    Garment.tie,
    Garment.shirt(size: "S"),
    Garment.shirt(size: "M"),
    Garment.shirt(size: "L"),
    Garment.pants(waist: 29, inseam: 32),
    Garment.pants(waist: 35, inseam: 34)
]

for item in items {
    switch item {
    case .tie: print("tie")
    case .shirt("M"): print("\(item) may fit")
    case .shirt("L"): print("\(item) may fit")
    case .shirt: print("\(item) won't fit")
    case .pants(34, 34): print("\(item) may fit")
    case .pants(35, 34): print("\(item) may fit")
    case .pants: print("\(item) won't fit")
    }
}
// tie
// shirt("S") won't fit
// shirt("M") may fit
// shirt("L") may fit
// pants(waist: 29, inseam: 32) won't fit
// pants(waist: 35, inseam: 34) may fit

Perhaps more importantly, you could then use the value-binding pattern to unwrap associated values with let :

let myPants = Garment.pants(waist: 32, inseam: 34)

switch someGarment {
case .tie: print("tie")
case .shirt(let s): print("shirt, size: \(s)")
case .pants(let w, let i): print("pants, size \(w) X \(i)")
}
// pants, size 32 X 34

(For more on value-binding and the value-binding pattern, see my earlier blog post on The Tuple Pattern.)

Note that Swift allows you streamline case statements by ‘factoring out’ any let keywords used in value binding, allowing you to rewrite the above switch statement more concisely:

switch someGarment {
case .tie: print("tie")
case let .shirt(s): print("shirt, size: \(s)")
case let .pants(w, i): print ("pants, size \(w) X \(i)")
}
// pants, size 32 X 34

You could also add where clauses to further refine pattern matches:

switch someGarment {
case let .pants(w, i) where w == 32: print("inseam: \(i)")
default: print("No match")
}
// inseam: 34

Other Pattern-Matching Capabilities

To be clear, the enumeration case pattern can match a number of other kinds of patterns. While not an exhaustive list, here are a few examples:

Strings

Although from Swift’s perspective, pattern matching based on values of strings, characters, and numbers is just base behavior, the fact that it works with strings feels like a special case — and an incredibly useful one at that.

struct Song {
    var title: String
    var artist: String
}

let aria = Song(title: "Donna non vidi mai", artist: "Luciano Pavarotti")

switch(aria.artist) {
case "Luciano Pavarotti": print(aria)
default: print("No match")
}
// Song(title: "Donna non vidi mai", artist: "Luciano Pavarotti")

Intervals

In addition to matching numeric types such as Int or Double value, enumeration cases can match numeric intervals:

let numbers = [-1, 3, 9, 42]
for number in numbers {
    switch(number) {
    case ..<3: print("less than 3")
    case 3: print("3")
    case 4...9: print("4 through 9")
    default: print("greater than 10")
    }
}
// less than 3
// 3
// 4 through 9
// greater than 10

Tuples

Enumeration cases can perform pattern matching on tuples:

let dogs = [(name: "Rover", breed: "Lab", age: 2),
            (name: "Spot", breed: "Beagle", age: 2),
            (name: "Pugsly", breed: "Pug", age: 9),
            (name: "Biff", breed: "Pug", age: 5)]

for dog in dogs {
    switch (dog) {
    case (_, "Lab", ...3): print("matched a young Lab named \(dog.name)")
    case (_, "Pug", 8...): print("matched an older Pug named \(dog.name)")
    default: print("no match for \(dog.age) year old \(dog.breed)")
    }
}
// matched a young Lab named Rover
// no match for 2 year old Beagle
// matched an older Pug named Pugsly
// no match for 5 year old Pug

For more details on tuples, including pattern matching, see my blog post on The Tuple Pattern.

Type Casting

Enumeration also work with the type casting pattern, using either is , which simply checks a value’s type, or as, which attempts to downcast a value to the provided type:

struct Dog {
    var name: String
}

let items: [Any] = [Dog(name: "Rover"), 42, 99, "Hello", (0, 0)]

for item in items {
    switch(item) {
    case is Dog: print("Nice doggie")
    case 42 as Int: print("integer 42")
    case let i as Int: print("integer \(i)")
    case let s as String: print("string with value: \(s)")
    default: print("something else")
    }
}
// Nice doggie
// integer 42
// integer 99
// string with value: Hello
// something else

Case Conditions

In addition to pattern matching in switch statements, Swift allows you to use the case keyword to specify pattern matches in conditionals such as if and guard statements, as well as for-in and while loop logic. While Swift makes that easy do, the syntax sometimes confuses people.

Case Conditions in Branches

In a switch, the keyword case is followed by the pattern you’re interested in matching, and then a colon. For example:

let someColor = "Red"

switch someColor {
    case "Red": // do something here
    // ...

However, when you do pattern matching in a conditional, the case keyword is followed by an initializer — in other words, it looks like an assignment (though it’s not).

if case "Red" = someColor {
    // do something here
}

Now the example above may seem silly, because clearly you could simply have compared the strings directly, which would feel more natural syntactically:

if someColor == "Red" {
    // do something here
}

But suppose you’re interested in comparing enumeration values rather than strings:

let Garment.shirt(size: "XL")

The enum type doesn’t implement Equatable so you can’t directly compare values with the == operator. You could of course overload == for your enum type, but then you’d need to do that every time you declare a new enumeration if you wanted to use that approach consistently. What a hassle!

Instead, you can pattern match with an enumeration case:

let items = [
    Garment.tie,
    Garment.shirt(size: "M"),
    Garment.shirt(size: "L"),
    Garment.shirt(size: "XL"),
    Garment.pants(waist: 32, inseam: 34)
]

for item in items {
    if case .shirt = item { print(item) }
}
// shirt: M
// shirt: L
// shirt: XL

Here we’re printing only those items that match the .shirt enumeration case. We could, of course, be more specific:

for item in items {
    if case .shirt("XL") = item { print(item) }
}
// shirt: XL

As with switch statements, you can use let to bind associated values:

for item in items {
    if case let .shirt(size) = item, size.contains("L") {
        print(item)
    }
}
// shirt: L
// shirt: XL

Case Conditions in Loops

Note that you can use pattern matching with case directly in loop logic. For example, you could roughly approximate the two previous examples with the following, more streamlined code:

for case .shirt("XL") in items {
    print("shirt, size XL")
}
// shirt, size XL

for case let .shirt(size) in items where size.contains("L") {
    print("shirt, size \(size)")
}
// shirt, size L
// shirt, size XL

The great thing is that the latter examples are not only shorter, but arguably more expressive than their former counterparts. Here’s another example:

let items:[Garment] = [
    .tie,
    .shirt(size: "L"),
    .pants(waist: 35, inseam: 31),
    .pants(waist: 35, inseam: 34),
    .pants(waist: 35, inseam: 35),
]

for case let .pants(w, i) in items where w == 35 && 34...36 ~= i {
    print("pants, \(w) X \(i)")
}
// pants, 35 X 34
// pants, 35 X 35

Here the enumeration case matches .pants instances, filtering out shirts and ties. The where clause matches only pants with waist size 35 and inseam between 34 and 36, using the pattern matching operator, ~= to compare the inseam value to an integer range.

Conclusion

Swift’s enumeration case pattern is surprisingly flexible, allowing you to use it creatively in combination with various other patterns, such as the tuple pattern and the value-binding pattern. Many of us haven’t experienced that kind of flexibility in the languages we regularly use. It can take some time to become aware of all the capabilities, and to remember to use them in the heat of battle.

But once you start getting in the habit, enumeration case and other Swift patterns will allow you to write more concise and expressive control logic. And that in turn should lead to code that is both easier to write, and — more importantly — easier to read.