Pattern matching in Swift iOS 01.02.2019

Pattern matching saves you from having to type much longer and less readable statements to evaluate conditions. Pattern matching can help you write more readable code than the alternative logical conditions.

Suppose you have a coordinate with x, y, and z axis values:

let coordinate = (x: 1, y: 0, z: 0)

Both of these code snippets will achieve the same result:

// 1
if (coordinate.y == 0) && (coordinate.z == 0) {
  print("along the x-axis")
}

// 2
if case (_, 0, 0) = coordinate {
  print("along the x-axis")
}

The second option, using pattern matching, is concise and readable.

Patterns provide rules to match values. You can use patterns in switch cases, as well as in if, while, guard, and for statements. You can also use patterns in variable and constant declarations.

If and guard

You can transform if and guard statements into pattern matching statements by using a case condition. The example below shows how you use an ifstatement with a case condition:

func process(point: (x: Int, y: Int, z: Int)) -> String {
    if case (0, 0, 0) = point {
        return "At origin"
    }
    return "Not at origin"
}
let point = (x: 0, y: 0, z: 0)
let response = process(point: point) // At origin

A case contition in a guard statement achieves the same effect:

func process(point: (x: Int, y: Int, z: Int)) -> String {
    guard case (0, 0, 0) = point else {
        return "Not at origin"
    }
    return "At origin"
}

Switch

If you care to match multiple patterns, the switch statement is your best friend. You can rewrite processPoint() like this:

func process(point: (x: Int, y: Int, z: Int)) -> String {
    let closeRange = -2...2
    let midRange = -5...5

    switch point {
        case (0, 0, 0):
            return "At origin"
        case (closeRange, closeRange, closeRange):
            return "Very close to origin"
        case (midRange, midRange, midRange):
            return "Nearby origin"
        default:
            return "Not near origin"
    }
}
let point = (x: 15, y: 5, z: 3)
let response = process(point: point)

The switch statement also provides an advantage over the if statement because of its exhaustiveness checking. The compiler guarantees that you have checked for all possible values by the end of a switch statement.

Here is code that shows how easy and smart

let point = (1, 1)
switch point {
    case let (x, y) where x == y:
        print("X is \(x). Y is \(y). They have the same value.")
    case (1, let y):
        print("X is 1. Y is \(y)")
    case (let x, 1):
        print("X is \(x). Y is 1")        
    case (x, y) where x > y:
        print("X is \(x). Y is \(y)")  
    default:
        print("Are you sure?")        
}

In Swift, we can also combine the underscore (wildcard) with the where statement. This is illustrated in the following example:

let myNumber = 10 
switch myNumber { 
    case _ where myNumber.isMultiple(of: 2): 
        print("Multiple of 2") 
    case _ where myNumber.isMultiple(of: 3): 
        print("Multiple of 3") 
    default: 
       print("No Match") 
}   

In this example, we create an integer variable named myNumber and use the switch statement to determine whether the value of the variable is a multiple of 2 or 3. Notice the case statement starts off with an underscore followed by the where statement. The underscore will match all the values of the variable, and then the where statement is called to see if it matches the rule defined within it.

for

A for loop churns through a collection of elements. Pattern matching can act as a filter:

let groupSizes = [1, 5, 4, 6, 2, 1, 3]
for case 1 in groupSizes {
    print("Found an individual") // 2 times
}

In this example, the array provides a list of workgroup sizes for a school classroom. The implementation of the loop only runs for elements in the array that match the value 1. Since students in the class are encouraged to work in teams instead of by themselves, you can isolate the people who have not found a partner.

We can use the for-case statement to filter through an array of tuples and print out only the results that match our criteria. The for-case example is very similar to using the where statement where it is designed to eliminate the need for an if statement within a loop to filter the results. In this example, we will use the for-case statement to filter through a list of World Series winners and print out the year(s) that a particular team won the World Series:

var worldSeriesWinners = [  
  ("Red Sox", 2004), 
  ("White Sox", 2005), 
  ("Cardinals", 2006), 
  ("Red Sox", 2007)]

for case let ("Red Sox", year) in worldSeriesWinners {  
    print(year) 
}

The for-case-in statement also makes it very easy to filter out the nil values in an array of optionals; let's look at an example of this:

let myNumbers: [Int?] = [1, 2, nil, 4, 5, nil, 6]

for case let .some(num) in myNumbers {  
    print(num) 
} 

Following example also combines the for-case-in statement with a where statement to perform additional filtering:

let myNumbers: [Int?] = [1, 2, nil, 4, 5, nil, 6] 
for case let num? in myNumbers where num < 3 {  
    print(num) 
}

Wildcard pattern

Revisit the example you saw at the beginning, where you wanted to check if a value was on the x-axis, for the (x, y, z) tuple coordinate:

if case (_, 0, 0) = coordinate {
  // x can be any value. y and z must be exactly 0
  print("On the x-axis") // Printed!
}

Value-binding pattern

You simply use var or let to declare a variable or a constant while matching a pattern. You can then use the value of the variable or constant inside the execution block:

if case (let x, 0, 0) = coordinate {
    print("On the x-axis at \(x)") // Printed: 1
}

The pattern in this case condition matches any value on the x-axis, and then binds its x component to the constant named x for use in the execution block.

If you wanted to bind multiple values, you could write let multiple times or, even better, move the let outside the tuple:

if case let (x, y, 0) = coordinate {
    print("On the x-y plane at (\(x), \(y))") // Printed: 1, 0
}

Optional pattern

Speaking of optionals, there is also an optional pattern. The optional pattern consists of an identifier pattern followed immediately by a question mark. You can use this pattern in the same places you would use enumeration case patterns.

let names: [String?] =
  ["Body", nil, "Klyd", "Chris", nil, "John"]

for case let name? in names {
    print(name) // 4 times
}

"Is" type-casting pattern

By using the is operator in a case condition, you check if an instance is of a particular type.

let array: [Any] = [15, "George", 2.0]
for element in array {
    switch element {
        case is String:
            print("Found a string") // 1 time
        default:
            print("Found something else") // 2 times
    }
}

With this code, you find out that one of the elements is of type String. But you don’t have access to the value of that String in the implementation.

"As" type-casting pattern

The as operator combines the is type casting pattern with the value-binding pattern. Extending the example above, you could write a case like this:

for element in array {
    switch element {
        case let text as String:
            print("Found a string: \(text)") // 1 time
        default:
            print("Found something else") // 2 times
    }
}

Qualifying with where

You can specify a where condition to further filter a match by checking a unary condition in line.

for number in 1...9 {
    switch number {
        case let x where x % 2 == 0:
        print("even") // 4 times
    default:
        print("odd") // 5 times
    }
}

If the number in the code above is divisible evenly by two, the first case is matched.

Chaining with commas

Here’s an example how to match multiple patterns in a single-case condition.

func timeOfDayDescription(hour: Int) -> String {
    switch hour {
        case 0, 1, 2, 3, 4, 5:
            return "Early morning"
        case 6, 7, 8, 9, 10, 11:
            return "Morning"
        case 12, 13, 14, 15, 16:
            return "Afternoon"
        case 17, 18, 19:
            return "Evening"
        case 20, 21, 22, 23:
            return "Late evening"
        default:
            return "INVALID HOUR!"
    }
}
let timeOfDay = timeOfDayDescription(hour: 12) // Afternoon

Here you see several identifier patterns matched in each case condition. You can use the constants and variables you bind in preceding patterns in the patterns that follow after each comma. Here’s a refinement to the cuddly animal test:

if case .animal(let legs) = pet, case 2...4 = legs {
    print("potentially cuddly") // Printed!
} else {
    print("no chance for cuddles")
}

The first pattern, before the comma, binds the associated value of the enumeration to the constant legs. In the second pattern, after the comma, the value of the legs constant is matched against a range.

Custom tuple

You can create a just-in-time tuple expression at the moment you’re ready to match it. Here’s a tuple that does just that:

let name = "Bob"
let age = 23
if case ("Bob", 23) = (name, age) {
    print("Found the right Bob!") // Printed!
}

Another such example involves a login form with a username and password field. Users are notorious for leaving fields incomplete then clicking Submit. In these cases, you want to show a specific error message to the user that indicates the missing field, like so:

var username: String?
var password: String?
switch (username, password) {
    case let (username?, password?):
        print("Success! User: \(username) Pass: \(password)")
    case let (username?, nil):
        print("Password is missing. User: \(username)")
    case let (nil, password?):
        print("Username is missing. Pass: \(password)")
    case (nil, nil):
        print("Both username and password are missing")  // Printed!
}

Each case checks one of the possible submissions. You write the success case first because if it is true, there is no need to check the rest of the cases. In Swift, switch statements don’t fall through, so if the first case condition is true, the remaining conditions are not evaluated.