Switch Statements in Swift: Selecting Among Multiple Options

Switch statements are a powerful tool for controlling the flow of your program based on the value of a variable or expression. Although they resemble if-else statements with multiple clauses, switch statements offer several advantages.



FREE GUIDE - SwiftUI App Architecture: Design Patterns and Best Practices

MVC, MVVM, and MV in SwiftUI, plus practical lessons on code encapsulation and data flow.

DOWNLOAD THE FREE GUIDE

Table of contents

What is a switch statement?

A switch statement is a conditional statement that allows you to execute different blocks of code depending on the value of a variable or expression. The syntax of a switch statement in Swift is as follows:

switch /*some value to consider*/ {
	case /*value 1*/:
		/*respond to value 1*/
	case /*value2*/,
		/*value 3*/:
		/*respond to value 2 or 3*/
	default:
		/*otherwise, do something else*/
}

The switch statement evaluates an expression and compares it with different values, referred to as “cases.” When the expression matches a case, the statements inside that case are executed. If none of the cases match, the statements inside the default case are executed.

Let’s consider an example of a switch statement that checks the value of a variable called dayOfWeek and prints the corresponding day name.

let dayOfWeek = 4

switch dayOfWeek {
	case 1: print("Sunday")
	case 2: print("Monday")
	case 3: print("Tuesday")
	case 4: print("Wednesday")
	case 5: print("Thursday")
	case 6: print("Friday")
	case 7: print("Saturday")
	default: print("Invalid day")
}

// Output: Wednesday

In Swift, switch statements must be exhaustive. This means you must account for every possible value of the expression using either a case or a default clause.

If you omit the default clause, you must ensure that every possible value is handled by a case. However, if a default case doesn’t require additional operations, you can leave it empty using the break keyword.

Remember that a value may match multiple cases, but only the first matching case will be executed.

When to use switch statements instead of if-else statements

You can achieve the same result using an if-else statement with multiple else clauses.

let dayOfWeek = 4
if dayOfWeek == 1 {
	print("Sunday")
} else if dayOfWeek == 2 {
	print("Monday")
} else if dayOfWeek == 3 {
	print("Tuesday")
} else if dayOfWeek == 4 {
	print("Wednesday")
} else if dayOfWeek == 5 {
	print("Thursday")
} else if dayOfWeek == 6 {
	print("Friday")
} else if dayOfWeek == 7 {
	print("Saturday")
} else {
	print("Invalid day")
}

However, switch statements offer several advantages over an equivalent if-else statement:

  • It is immediately clear that a switch statement compares a single value against multiple patterns, making it more concise and readable.
  • switch statements ensure that every possible value is handled by either a case or a default clause. This helps prevent errors caused by missing or incomplete conditions.
  • switch statements can use pattern matching to check for complex conditions such as ranges, tuples, types, and enums. This makes them more expressive and powerful compared to if-else statements.
  • switch statements can execute multiple consecutive blocks for a specific match using the fallthrough keyword.

It should be noted that the above points are not always applicable. In some situations, using a switch statement instead of an if-else statement may not be possible, especially when dealing with multiple unrelated conditions.

Matching intervals in switch statements using Swift ranges

Swift allows for the use of pattern matching within switch statements. This means that you can match not only specific values but also more complex patterns using closed and open.

For instance, you can employ a switch statement to examine a person’s position based on their work experience.

let workExperience = 3

switch workExperience {
	case ...0:
		print("Invalid experience")
	case 0...3:
		print("Junior")
	case 3..<6:
		print("Senior")
	case 6...10:
		print("Manager")
	default:
		print("Executive")
}

// Output: Junior

As the example above demonstrates, switch statements in Swift can accept open, half-open, and closed ranges. It is important to note that only the first case will be executed if a number falls within the ranges for multiple cases.

Swift ranges are not limited to numerical values. You can also use ranges with other types, such as characters, strings, and dates, or any type that conforms to the Comparable protocol. This allows you to perform pattern matching and interval checks on various data types.

You can also check if a number falls within a range and simultaneously matches other conditions, as we will see below.

Matching Swift tuples in switch statements

Swift switch statements can also use tuples to evaluate values that contain multiple components. An instance of this would be employing a switch statement to assess the coordinates of a point on a grid.

let sex = "male"
let age = 6

switch (sex, age) {
	case ("male", 0..<12):
		print("Boy")
	case ("female", 0..<12):
		print("Girl")
	case (_, 12...18):
		print("Teenager")
	default:
		print("Adult")
}

// Output: Boy

In the example above, I directly created the tuple within the switch clause, which is the usual practice. However, an alternate method is first to store the tuple in a variable, for instance, let tuple = (sex, age), and subsequently switch based on it.

In addition to matching specific values, you can use ranges in tuple switch statements. This allows you to check if the components of the tuple fall within a certain range. To match any value for a particular component in a tuple, the wildcard pattern _ can be used.

Binding values to use them in the code block of a case clause

Swift switch statements can also use value bindings to assign the value of the expression or a portion of it to a constant or variable, which can then be used within the case body. This feature proves useful when you need to access or manipulate a value corresponding to a particular case.

To use value bindings, you define a placeholder identifier using the let or var keyword followed by the desired name. When all the value bindings are of the same nature, it's more common to place a single let or var keyword immediately after each case clause instead of before each declaration.

let name = "Bob"
let role = "Student"
let field = "Software Engineering"

switch (name, role, field) {
	case let (name, "Student", field):
		print("\(name) is a student specializing in \(field)")
	case let (name, "Teacher", field):
		print("\(name) teaches \(field)")
	case let (name, "Engineer", field):
		print("\(name) is specialized in \(field)")
	default:
		print("Unknown role")
}

// Output: Bob is a student specializing in Software Engineering

Value bindings can also be used in conjunction with where clauses.

Grouping multiple patterns into a single compound case

Swift switch statements can use compound cases to verify multiple patterns in a single case, separated by commas. This feature enables checking multiple conditions within a single case, streamlining code and improving readability.

let color = "red"
switch color {
	case "red", "orange", "yellow":
		print("This is a warm color")
	case "green", "blue", "purple":
		print("This is a cool color")
	default:
		print("This is not a color")
}

A compound case can also be written across multiple lines to enhance readability further.

However, excessive use of complex patterns within a single case can lead to code that is difficult to understand and maintain. It is important to balance using compound cases effectively and keeping the codebase clean and maintainable.

In addition to using comma-separated patterns, Swift also allows using ranges and value bindings in compound cases.

Switching over enumeration values and binding associated values

Swift switch statements can also use enums to verify values within a predefined set of cases.

enum Planet {
	case mercury
	case venus
	case earth
	case mars
	case jupiter
	case saturn
	case uranus
	case neptune
}

let planet = Planet.mars
switch planet {
	case .mercury, .venus, .earth, .mars:
		print("This planet is located before the asteroid belt of the Solar system")
	case .jupiter:
		print("This planet is located within the outer bounds of the asteroid belt, together with the Jupiter trojan group")
	case .saturn, .uranus, .neptune:
		print("This planet is located beyond the asteroid belt")
}

Swift requires that all possible cases of an enum be handled in the switch statement. This means that if a new case is added to the enum in the future, the compiler will catch any switch statements that are not updated to handle the new case. This helps prevent bugs and ensures that the code remains robust and reliable.

Value bindings prove particularly valuable when working with enumerations that have associated values.

enum Animal {
	case cat(name: String)
	case dog(breed: String)
	case bird(species: String)
}

let myPet: Animal = .cat(name: "Whiskers")

switch myPet {
	case .cat(let name):
		print("My cat's name is \(name).")
	case .dog(let breed):
		print("My dog belongs to the \(breed) breed.")
	case .bird(let species):
		print("I have a pet bird of the \(species) species.")
}

Beware that an excessive use of enums in conjunction with switch statements could be a code smell indicating a violation of the open-closed principle.

Suppose you constantly update switch statements with enums when adding new functionality to your code. In that case, you should probably refactor it to use generics and protocol-oriented programming.

Verifying or casting subclasses and protocol-conforming types

Swift switch statements can also use types to verify values that belong to a specific type or subtype. For instance, you can employ a switch statement to examine the type of an animal.

class Animal {}
class Dog: Animal {}
class Cat: Animal {}

let animal: Animal = Dog()

switch animal {
	case is Dog:
		print("It's a dog")
	case is Cat:
		print("It's a cat")
	default:
		print("It's an animal")
}

// Output: It's a dog

In this example, we have defined a class called Animal, along with two subclasses named Dog and Cat. An instance of Dog has been assigned to the variable animal. The switch statement employs the is keyword to verify if the value of animal is an instance of Dog, Cat, or any other type. It discovers a match with is Dog and executes the statement print("It's a dog").

This does not work only with classes and subclasses but also with value types like structures and protocol conformance.

Moreover, if you need to use a type-specific interface instead of just checking if a value is a member of a specific type, switch statements in Swift can also use the powerful feature known as type-casting patterns.

protocol Animal {}

struct Dog: Animal {
	func bark() {
		print("Woof!")
	}
}
struct Cat: Animal {
	func meow() {
		print("Meow!")
	}
}

let animal: Animal = Dog()

switch animal {
	case let dog as Dog:
		dog.bark()
	case let cat as Cat:
		cat.meow()
	default:
		break
}

// Output: Woof!

Again, type casting works with both subclassing and protocol-conforming types.

Refining the matching of a pattern with where clauses

Swift switch statements can also use where clauses to include additional conditions for each case. For instance, you can employ a switch statement to determine if a number is even or odd and positive or negative.

let number = -4

switch number {
	case let x where x % 2 == 0 && x > 0:
		print("\(x) is even and positive")
	case let x where x % 2 == 0 && x < 0:
		print("\(x) is even and negative")
	case let x where x % 2 != 0 && x > 0:
		print("\(x) is odd and positive")
	case let x where x % 2 != 0 && x < 0:
		print("\(x) is odd and negative")
	default: break
}

// Output: -4 is even and negative

In this example, we use a where clause after each case to evaluate two conditions: whether the number is divisible by 2 (even or odd) and whether the number is greater than or less than zero (positive or negative).

Additionally, we use a let pattern to bind the value of the expression to a constant named x, which can be accessed within the where clause and the statements. The switch statement finds a match with the second case and executes the statement print("-4 is even and negative").

Falling into subsequent cases after a match

In languages like C, execution continues from the bottom of each case into the next one unless you explicitly prevent fallthrough.

In Swift, on the other hand, a switch statement only executes the first matching case. This approach is more concise and predictable, and it helps avoid unintentionally running multiple cases.

However, if needed, you can make a case fall through into the next one by explicitly using the fallthrough keyword.

For example, you can use a switch statement with the fallthrough keyword to print the numbers from 1 to 10.

let number = 3
switch number {
	case 1:
		print(1)
		fallthrough
	case 2:
		print(2)
		fallthrough
	case 3:
		print(3)
		fallthrough
	case 4:
		print(4)
		fallthrough
	case 5:
		print(5)
	default: break
}

// Output:
// 3
// 4
// 5

In this example, the switch statement finds a match with case 3 and executes the statement print(3). Subsequently, it encounters the fallthrough keyword, allowing it to move to the next case without checking its condition. It then executes the statement print(4) and repeats this process until it reaches the end of the switch statement.

It is important to note that using fallthrough is rarely necessary in Swift. There are better alternatives for sharing common code across cases, such as:

  • Using a combination of compound cases, value bindings, and where clauses.
  • Abstracting the common code using types and functions.

Conclusion

Switch statements are a powerful way to control the flow of your program based on the value of a variable or expression. They are more concise, readable, and expressive than using multiple if-else statements. They also ensure that every possible value is handled by either a case or a default clause.

SwiftUI App Architecture: Design Patterns and Best Practices

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