Skip to content

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:

swift
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:

swift
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:

  1. A case label that lists one or more values to match the control expression against.
  2. 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:

swift
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:

swift
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 bound a up to, and including, its upper bound b.
  • A half-open range (a..<b) includes all values from its lower bound a up to, but not including, its upper bound b.
  • A one-sided range (a..., ...b, or ..<b) only has one bound.

Using ranges, the previous example becomes:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
let number = 7
let absoluteValue = number < 0 ? -number : number

This code is equivalent to:

swift
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 and value2 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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

swift
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:

  1. Every iteration, the for-in statement declares its loop constant and assigns it the next element in the collection.
  2. 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:

swift
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 a while or repeat-while loop) or the next element in the collection (in the case of a for-in loop).

The following example prints the greatest common divisor of two numbers a and b:

swift
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:

swift
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.

swift
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:

swift
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:

  1. You start by declaring the code’s parameters as constants.
  2. 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.