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.
- This article is part of the Learn Swift series.
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 GUIDETable of contents
- What is a switch statement?
- When to use switch statements instead of if-else statements
- Matching intervals in switch statements using Swift ranges
- Matching Swift tuples in switch statements
- Binding values to use them in the code block of a case clause
- Grouping multiple patterns into a single compound case
- Switching over enumeration values and binding associated values
- Verifying or casting subclasses and protocol-conforming types
- Refining the matching of a pattern with where clauses
- Falling into subsequent cases after a match
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 thefallthrough
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.
Matteo has been developing apps for iOS since 2008. He has been teaching iOS development best practices to hundreds of students since 2015 and he is the developer of Vulcan, a macOS app to generate SwiftUI code. Before that he was a freelance iOS developer for small and big clients, including TomTom, Squla, Siilo, and Layar. Matteo got a master’s degree in computer science and computational logic at the University of Turin. In his spare time he dances and teaches tango.