Switch Statements in Swift: Selecting Among Multiple Options

Switch statements compare specific values against different cases and execute the block of code associated with the first matching case. You can achieve the same result using an if statement with multiple else clauses, but switch statements are easier to read and understand at a glance.

This article covers the most common use cases of switch statements. There are other, more advanced features of switch statements that I will talk about in future articles.



Architecting SwiftUI apps with MVC and MVVM

GET THE FREE BOOK NOW

Table of contents

Make switch statements exhaustive with default cases

Switch statements cover all of the possible cases out there. You should define your default cases to ensure that your switch statements are always exhaustive.

We can use a number guessing game as an example. It uses switch statements to determine information about a pair of whole numbers based on the exact difference between them.

let first = 12
let second = 10
let difference = first - second
switch difference {
	case 0: print("\(first) is equal to \(second) and they are both either odd or even numbers.")
	case 1: print("\(first) is greater than \(second) and they are both odd and even consecutive numbers.")
	case 2: print("\(first) is greater than \(second) and they are both either odd or even consecutive numbers.")
	default: print("\(first) and \(second) are both random numbers.")
}

The switch statement in this code handles only a few very particular values manually. The code that is executed if a specific case is true is placed right after the colon. All cases are placed between curly brackets since they are all part of the switch statement.

The default case covers all of the other remaining values automatically. This ensures that the switch statement is exhaustive.

The switch statement handles cases manually if the first whole number in the pair is greater than or equal to the second one. You can add another switch statement covering all the other missing cases. 

let first = 10
let second = 12
let difference = first - second
switch difference {
	case -2: print("\(first) is less than \(second) and they are both either odd or even consecutive numbers.")
	case -1: print("\(first) is less than \(second) and they are both odd and even consecutive numbers.")
	case 0: print("\(first) is equal to \(second) and they are both either odd or even numbers.")
	default: print("\(first) and \(second) are both random numbers.")
}

This switch statement handles values manually if the first whole number in the pair is less than or equal to the second one. The default case covers all of the other remaining values automatically as before.

You can simplify this by combining the two simple switch statements into a single complex one.

let first = 10
let second = 10
let difference = first - second
switch difference {
	case -2: print("\(first) is less than \(second) and they are both either odd or even consecutive numbers.")
	case -1: print("\(first) is less than \(second) and they are both odd and even consecutive numbers.")
	case 0: print("\(first) is equal to \(second) and they are both either odd or even numbers.")
	case 1: print("\(first) is greater than \(second) and they are both odd and even consecutive numbers.")
	case 2: print("\(first) is greater than \(second) and they are both either odd or even consecutive numbers.")
	default: print("\(first) and \(second) are both random numbers.")
}

This switch statement covers all of the cases handled by the previous switch statements. The default case works exactly as before too.

There are certain situations where default cases in switch statements don’t need to do anything else because all of the other cases have already been appropriately handled. In the next section of this article, you will learn what to do when this happens. 

Add break statements to default switch cases if default cases don’t need to do anything else

Default cases are always needed to make switch statements exhaustive. They can never be left empty even if they don’t do anything. The break statement is a valuable tool to ensure this.

A multiple whole number generator finds the first multiple whole number of 3 that is greater than or equal to a specific whole number. 

let random = 10
let remainder = random % 3
let multiple: Int
switch remainder {
	case 0: multiple = random
	case 1: multiple = random + 2
	case 2: multiple = random + 1
	default: break
}

This switch statement uses the remainder operator to generate the following multiple whole number. All of the possible remainder values are covered manually by the switch statement. The break statement simply exits the default case since there are no other values to be handled automatically.

This switch statement only works for positive whole numbers. You can add another switch statement which works only for whole negative numbers.

let random = -10
let remainder = random % -3
let multiple: Int
switch remainder {
	case -2: multiple = random + 2
	case -1: multiple = random + 1
	case 0: multiple = random
	default: break
}

This switch statement uses the remainder operator with whole negative numbers to generate the following multiple whole number as before. The break statement also works in the same way in the default case of the switch statement.

The modulo operator always returns the same result for both the a % b and a % -b operations for any given a and b positive and negative whole numbers. This is how the modulo operator works by default in Swift.

10 % 3 == 10 % -3 // true
-10 % 3 == -10 % -3 // true

This means that the previous two switch statements cover all possible cases, and, as a result, you can combine them into a single one. 

let random = 10
let remainder = random % 3
let multiple: Int
switch remainder {
	case -2, 1: multiple = random + 2
	case -1, 2: multiple = random + 1
	case 0: multiple = random
	default: break
}

This switch statement uses compound cases to group related cases into one. Commas separate the values handled by the compound case since they are all part of the compound case. The `break` statement exits the default case of the switch statement as before.

There are scenarios where it is helpful to enable switch statements to go from a particular case to another instead of stopping at the first valid case as they otherwise do by default. We will look at how to do this next. 

Control the natural flow of switch statements with fallthrough statements

Switch statements stop at the first valid case by default. You can change this default behavior to automatically make the switch statement jump from one case to the next.

For example, a random whole number generator uses both dice rolls and compound assignment operators to generate random whole numbers.

var result = 10
let dice = 2
switch dice {
	case 1: result = -result
	case 2: result += 2
	case 3: result *= 3
	case 4: result -= 4
	case 5: result /= 5
	case 6: result %= 6
	default: break
}

This switch statement rolls the die and determines the corresponding random whole number associated with a specific compound assignment operator and the die’s number.  

It would be nice to generate even more significant random whole numbers. You can do that by tweaking the switch statement just a little.

var result = 10
let dice = 2
switch dice {
	case 1: result = -result
	case 2: result += 2
			result *= 3
	case 3: result *= 3
	case 4: result -= 4
	case 5: result /= 5
	case 6: result %= 6
	default: break
}

The above switch statement adds the block of code associated with the 3 case to the 2 case. This generates a much bigger random whole number than before. Unfortunately, this violates the DRY principle because the same code is duplicated in both cases of the switch statement. You can quickly fix this by using functions instead.

var result = 10
let dice = 2
func multiply(_ number: Int, by multiplier: Int) -> Int {
	number * multiplier
}
switch dice {
	case 1: result = -result
	case 2: result += 2
			result = multiply(result, by: 3)
	case 3: result = multiply(result, by: 3)
	case 4: result -= dice
	case 5: result /= dice
	case 6: result %= dice
	default: break
}

The multiply(_:by:) function encapsulates the corresponding logic for multiplying a certain number by a given multiplier. If you take this approach, you should create different functions for all case transitions in switch statements. An easier way to handle things properly is to use fallthrough statements instead. 

var result = 10
let dice = 2
switch dice {
	case 1: result = -result
	case 2: result += 2
			fallthrough
	case 3: result *= 3
	case 4: result -= 4
	case 5: result /= 5
	case 6: result %= 6
	default: break
}

This fallthrough statement automatically enables the switch statement to go from the 2 case to the 3 case.

You may use fallthrough statements in as many cases of switch statements as you need.

var result = 10
let dice = 4
switch dice {
	case 1: result = -result
	case 2: result += 2
			fallthrough
	case 3: result *= 3
	case 4: result -= 4
			fallthrough
	case 5: result /= 5
	case 6: result %= 6
	default: break
}

The second fallthrough statement above enables the switch statement to go from the 4 case to the 5 case automatically as before. This generates a much smaller random whole number than before.

In certain situations, switch statements need to handle an unlimited number of cases. You will learn about these in the next section of this article.  

Cover an unlimited number of values in switch cases with ranges

Sometimes certain cases of switch statements should cover an unlimited number of values. You can easily do this with ranges that are intervals of whole numbers.

Switch statements use pattern matching techniques to determine which range a particular whole number belongs to.

0...10 ~= 10 // true
0..<10 ~= 10 // false
0... ~= 10 // true
...0 ~= 0 // true
..<0 ~= 0 // false

The ~= operator in this code is the pattern matching operator. It returns either true if the range on its left side contains the value on its right side or false otherwise.

A number guessing game uses switch statements with ranges to determine information about a whole number.

let number = 10
switch number {
	case ...(-10): print("\(number) is a negative number.")
	case -9..<0: print("\(number) is a negative digit.")
	case 0...9: print("\(number) is a positive digit.")
	case 10...: print("\(number) is a positive number.")
	default: break
}

This switch statement uses the following types of ranges to go through all of the possible whole numbers out there:

  1. The …right range is the one-sided range closed at its right side. It contains only the right side of the range and all of the other whole numbers up to it.
    ...0 ~= 0 // true
  2. The left..<right range is the half-open range on its right side. It contains only the left side of the range and all of the other whole numbers between the left side and up to the right side of the range.
    0..<10 ~= 10 // false
  3. The left…right range is the closed range on both sides. It contains the left and right sides of the range and the whole numbers between the two.

    0...10 ~= 10 // true
  4. The left… range is the one-sided range closed at its left side. It contains only the left side of the range and all other whole numbers from there.
    0... ~= 10 // true

Switch statements can use one other type of range if they need to.

let number = 10
switch number {
	case ..<(-9): print("\(number) is a negative number.")
	case -9..<0: print("\(number) is a negative digit.")
	case 0...9: print("\(number) is a positive digit.")
	case 10...: print("\(number) is a positive number.")
	default: break
}

The switch statement in this code uses the ..<right range, which is the one-sided range half open at its right side. It contains all the other whole numbers up to the right side of the range.

..<0 ~= 0 // false

The right side of any range should be placed between round brackets if and only if its right side is a negative number. This is so that the compiler knows that the minus sign isn’t part of the range operator.

...(-10) ~= -10 // true
..<(-9) ~= -10 // true
-20...(-10) ~= -10 // true
-20..<(-9) ~= -10 // true

The left side of both half-open ranges and closed ranges that aren’t one-sided must always be less than or equal to their right side.

10...0 ~= 10
10..<0 ~= 10

The compiler flags a runtime error for this code because the left sides of both ranges are greater than both of their right sides.

There are specific scenarios where switch statements don’t need any default cases. This is because they are exhaustive by default, as all possible cases are handled manually instead. You can learn about when this happens in the next section of this article.  

Cover only a specific set of switch cases with tuples of booleans

The easiest way to ensure switch statements are exhaustive by default is to use tuples of booleans to implement them. A tuple is simply an ordered list of different elements that make sense to group into a single data unit. 

The login feature of an app checks whether the username and password of a user are valid. You can quickly test the user’s credentials with boolean tuples. 

let validUser = true
let validPassword = true
switch (validUser, validPassword) {
	case (true, true): print("Valid credentials!")
	case (true, false): print("Invalid password!")
	case (false, true): print("Invalid user!")
	case (false, false): print("Invalid credentials!")
}

The switch statement in this code handles all possible cases since both validUser and validPassword are either true or false in this case. All cases define pairs of boolean values for the (validUser, validPassword) boolean tuple. The constants and values of the tuple are placed between round brackets and separated by commas.  

However, you can simplify the above switch statement by using compound cases.

let validUser = true
let validPassword = true
switch (validUser, validPassword) {
	case (true, true): print("Valid credentials!")
	case (true, false): print("Invalid password!")
	case (false, true), (false, false): print("Invalid user!")
}

The login implementation of the app doesn’t care whether the user’s password is invalid or not as long as the user’s username is not. 

You can simplify the previous switch statement further with wildcards.

let validUser = true
let validPassword = true
switch (validUser, validPassword) {
	case (true, true): print("Valid credentials!")
	case (true, false): print("Invalid password!")
	case (false, _): print("Invalid user!")
}

The underscore in this code covers all possible values of validPassword, which are both true and false.

Conclusion

This article covers switch statements’ most common use cases with tuples and ranges. Future articles will build on this and show you how to refine your code with switch statements further.

Architecting SwiftUI apps with MVC and MVVM

It’s easy to make an app by throwing some code together. But without best practices and robust architecture, you soon end up with unmanageable spaghetti code. In this guide I'll show you how to properly structure SwiftUI apps.

GET THE FREE BOOK NOW

Leave a Comment