Advanced Control Flow β
In the previous chapter, you learned how to create branches and loops using the if
and while
statements. In this chapter, youβll learn that if
and while
arenβt always the best tools for the job, and that Swift offers additional control flow statements.
switch
β
In Exercise 2.4, you calculated the number of days in a month as follows:
let month = "july"
let year = 2024
let isLeapYear = year.isMultiple(of: 4) && !year.isMultiple(of: 100) ||
year.isMultiple(of: 400)
if month == "january" ||
month == "march" ||
month == "may" ||
month == "july" ||
month == "august" ||
month == "october" ||
month == "december" {
print("This month has 31 days.")
} else if month == "april" ||
month == "june" ||
month == "september" ||
month == "november" {
print("This month has 30 days.")
} else if month == "february" && isLeapYear {
print("This month has 29 days.")
} else if month == "february" {
print("This month has 28 days.")
} else {
print("This is not a valid month.")
}
These if
statements all have similar conditions: each compares the value of month
against one or more possible values. By inspecting the value of month
, the code decides which branch to follow. This decision-making process is known as switching.
Swift includes a switch
statement that makes switching easy:
switch month {
case "january", "march", "may", "july", "august", "october", "december":
print("This month has 31 days.")
case "april", "june", "september", "november":
print("This month has 30 days.")
case "february":
if isLeapYear {
print("This month has 29 days.")
} else {
print("This month has 28 days.")
}
default:
print("This is not a valid month.")
}
A switch
statement consists of the keyword switch
, a control expression, and a block of code containing one or more cases. Each case consists of:
- A case label that lists one or more values to match the control expression against.
- One or more statements that execute when the label contains a match.
The switch
statement evaluates its control expression and executes the first case that contains a match. All other cases are ignored.
The default
case you see in the previous example matches all values not yet matched by a previous case. A default case is often required, as switch
statements must be exhaustive, meaning every possible value of the control expression must have a matching case. When a default case is present, it must come last.
where
clauses β
A case label can include a where
clause to limit the values it matches. This clause specifies a condition that must be true for the case to match.
The following example uses a where
clause to match leap years:
switch month {
case "january", "march", "may", "july", "august", "october", "december":
print("This month has 31 days.")
case "april", "june", "september", "november":
print("This month has 30 days.")
case "february" where isLeapYear:
print("This month has 29 days.")
case "february":
print("This month has 28 days.")
default:
print("This is not a valid month.")
}
This example relies on the order of the cases. The specific case "february" where isLeapYear
year must come before the general case "february"
, otherwise the general case would consume all matches for "february"
, and the specific case would never execute.
Pattern matching β
Thanks to its support for pattern matching, the switch
statement can match the value of the control expression against several types of patterns, not just values.
Consider the following example:
let result = 15
switch result {
case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
print("You failed.")
case 10, 11:
print("You passed, but you were cutting it close.")
case 12, 13, 14, 15, 16:
print("You passed.")
case 17, 18, 19:
print("You passed with flying colors.")
case 20:
print("Perfect score.")
default:
print("Invalid score.")
}
In this example, all patterns are integer values. A case matches if it contains a value equal to the value of the control expression.
You can use ranges to improve this code:
- A closed range (
a...b
) includes all values from its lower bounda
up to, and including, its upper boundb
. - A half-open range (
a..<b
) includes all values from its lower bounda
up to, but not including, its upper boundb
. - A one-sided range (
a...
,...b
, or..<b
) only has one bound.
Using ranges, the previous example becomes:
switch result {
case 0..<10:
print("You failed.")
case 10...11:
print("You passed, but you were cutting it close.")
case 12...16:
print("You passed.")
case 17...19:
print("You passed with flying colors.")
case 20:
print("Perfect score.")
default:
print("Invalid score.")
}
A case that contains a range matches if the value of the control expression falls within that range.
You can achieve a similar result with where
clauses. For example, you can express the first case as result < 10
. However, you canβt write case where result < 10
, because you canβt use a where
clause on its own; it must follow a pattern.
This is where the wildcard pattern (_
) is useful. This pattern, which is a simple underscore, matches any value:
switch result {
case _ where result < 10:
print("You failed.")
case _ where result <= 11:
print("You passed, but you were cutting it close.")
case _ where result <= 16:
print("You passed.")
case _ where result <= 19:
print("You passed with flying colors.")
case 20:
print("Perfect score.")
default:
print("Invalid score.")
}
This example uses wildcards to match any value, then adds where
clauses to restrict these matches. This achieves the desired result of matching using a where
clause.
if
and switch
expressions β
Consider the following example:
let number = 7
let absoluteValue: Int
if number < 0 {
absoluteValue = -number
} else {
absoluteValue = number
}
In this example, the declaration of absoluteValue
must use a type annotation because its initial value depends on the value of number
.
You can improve this code by using an if
expression:
let number = 7
let absoluteValue = if number < 0 { -number } else { number }
The syntax of an if
expression is identical to that of an if
statement. However, they serve a different purpose:
- The branches of an
if
statement contain statements. The condition determines which branch will execute. - The branches of an
if
expression contain expressions. The condition determines the value of the expression.
switch
may also be used as an expression:
let daysInMonth = switch month {
case "january", "march", "may", "july", "august", "october", "december":
31
case "april", "june", "september", "november":
30
case "february":
if isLeapYear { 29 } else { 28 }
default:
0 // Invalid month!
}
This expression switches over month
and returns the number of days in that month.
Conditional operator β
The conditional operator (?:
) selects one of two values. This is similar to an if
expression but the conditional operator requires less syntax.
For example:
let number = 7
let absoluteValue = number < 0 ? -number : number
This code is equivalent to:
let number = 7
let absoluteValue = if number < 0 { -number } else { number }
The conditional operator has the form condition ? value1 : value2
and takes three operands:
condition
is a Boolean expression.value1
andvalue2
are values of the same type.
If condition
is true
, the operator returns value1
; if not, value2
.
The conditional operator is also called the ternary operator because itβs the only operator that takes three operands.
Only use the conditional operator for simple selections. Overusing it can lead to unreadable code, like this:
let x = 3
let y = 1
let z = 2
let max = x > y ? x > z ? x : z : y > z ? y : z
This code becomes much more readable when you add an if
expression:
let max = if x > y {
x > z ? x : z
} else {
y > z ? y : z
}
repeat-while
β
The repeat-while
statement is similar to the while
statement but checks the condition at the end of every iteration. This means a repeat-while
loop executes at least once.
For example:
var numerator = 315
var denominator = 420
var factor = 2
repeat {
if numerator.isMultiple(of: factor) && denominator.isMultiple(of: factor) {
numerator /= factor
denominator /= factor
} else {
factor += 1
}
} while factor <= numerator && factor <= denominator
This code simplifies a fraction. Every iteration either divides numerator
and denominator
by a common factor, or it increases the factor to search for a common one.
The following while
statement is equivalent to the previous repeat-while
statement:
while factor <= numerator && factor <= denominator {
if numerator.isMultiple(of: factor) && denominator.isMultiple(of: factor) {
numerator /= factor
denominator /= factor
} else {
factor += 1
}
}
In most cases, a while
loop is easier to understand than a repeat-while
loop because it specifies the condition at the start of the loop. If your algorithm feels like a good fit for the repeat-while
loop, use it; otherwise, prefer a while
loop.
for-in
β
In Exercise 2.9, you calculated the odds of rolling a number of pips using two six-sided dice, as follows:
let targetPips = 7
var combinationsFound = 0
var pipsOnFirstDie = 1
while pipsOnFirstDie <= 6 {
var pipsOnSecondDie = 1
while pipsOnSecondDie <= 6 {
if pipsOnFirstDie + pipsOnSecondDie == targetPips {
combinationsFound += 1
}
pipsOnSecondDie += 1
}
pipsOnFirstDie += 1
}
In this code, both while
statements iterate over the values 1 through 6 and use a variable to track their progress. Iterating over the elements of a collection β in this case, the range 1...6
β is a common task for loop statements. Swift includes a for-in
statement that makes this task easy.
Hereβs the same calculation again, this time using for-in
statements:
var combinationsFound = 0
for pipsOnFirstDie in 1...6 {
for pipsOnSecondDie in 1...6 {
if pipsOnFirstDie + pipsOnSecondDie == targetPips {
combinationsFound += 1
}
}
}
A for-in
statement consists of the keyword for
, a loop constant, the keyword in
, a collection of elements, and a body. The statement iterates over the elements in the collection and lets you access each element in turn. Hereβs how that works:
- Every iteration, the
for-in
statement declares its loop constant and assigns it the next element in the collection. - In the body of the loop, you access this element through the loop constant.
The loop constants pipsOnFirstDie
and pipsOnSecondDie
are similar to the variables they replace, but their scope is limited to the body of the loop. This is another advantage of using a for-in
loop to iterate over a collection.
where
clauses β
A for-in
statement can include a where
clause. If thatβs the case, the statement only executes its body for elements that satisfy the where
clause.
The following loop prints all even numbers between 0 and 100:
for number in 0...100 where number.isMultiple(of: 2) {
print(number)
}
break
and continue
β
The break
and continue
statements influence the execution of a loop:
- The
break
statement ends a loop prematurely. It causes control to break out of the loop and jump to the statement that follows the loop body. - The
continue
statement ends the current iteration of a loop. It causes control to continue to the loop condition (in the case of awhile
orrepeat-while
loop) or the next element in the collection (in the case of afor-in
loop).
The following example prints the greatest common divisor of two numbers a
and b
:
let a = 63
let b = 14
var divisor = a < b ? a : b
while divisor > 0 {
if a.isMultiple(of: divisor) && b.isMultiple(of: divisor) {
break
}
divisor -= 1
}
print("The greatest common divisor of \(a) and \(b) is \(divisor).")
Here, a while
loop iterates over all possible divisors, starting from the largest one. As soon as a divisor is found, a break
statement ends the loop.
You can also use the break
statement to create an empty switch
case. Recall that switch
cases must contain at least one statement. A break
statement can satisfy this requirement and simply ends the case.
The following switch
statement requires a default case to handle values less than 0 or greater than 20. Instead of printing an error message, this example uses a break
statement to ignore unmatched values:
switch result {
case 0..<10:
print("You failed.")
case 10...11:
print("You passed, but you were cutting it close.")
case 12...16:
print("You passed.")
case 17...19:
print("You passed with flying colors.")
case 20:
print("Perfect score.")
default:
break
}
Note
You also need a default case when the compiler is unable to infer that your switch
statement is exhaustive. In the previous example, you can replace case 0..<10
with case ..<10
and case 20
with case 20...
to make the cases exhaustive, but the compiler will still ask for a default case.
The next example prints all divisors of b
that are not also divisors of a
. It uses a for-in
loop to iterate over all possible divisors of b
. Inside this loop, a continue
statement skips values that are divisors of a
.
for divisor in 1...b {
if a.isMultiple(of: divisor) {
continue
}
if b.isMultiple(of: divisor) {
print("\(divisor) is a divisor of \(b) but not of \(a).")
}
}
You should note that break
and continue
are never required. You can always rewrite your loops in a way that doesnβt use these statements. The following example is equivalent to the previous one but uses a where
clause instead of continue
:
for divisor in 1...b where !a.isMultiple(of: divisor) {
if b.isMultiple(of: divisor) {
print("\(divisor) is a divisor of \(b) but not of \(a).")
}
}
Most programming problems have multiple solutions. As you gain more experience, youβll learn to compare the performance of these solutions. For now, focus on writing code thatβs easy to read and understand.
Up next β
Most of the examples and exercises youβve seen so far follow a similar structure:
- You start by declaring the codeβs parameters as constants.
- Then, you use these constants to perform a task or calculate a value.
In the next chapter, youβll formalize this structure and create reusable pieces of code called functions.