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 if
statement 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.