Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3. Control Flow

Swift’s control structures hold no surprises in their shapes. Conditions are not parenthesized, bodies are always braced, and nothing is truthy but a Bool. The two things that repay attention are guard, which changes how you lay a function out, and switch, which is the structure the rest of the language is designed around.

3.1 if

if temp > 30 {
    print("hot")
} else if temp > 15 {
    print("mild")
} else {
    print("cold")
}

The parentheses are optional and Swift programmers leave them off. The braces are not optional, not even for one statement, which retires the class of bug that made Apple’s own goto fail possible.

An if can also produce a value, which saves inventing a var and assigning to it in each branch:

let size = if n < 10 { "small" } else { "large" }

Every branch must produce a value of the same type and every case must be covered, so the else is required. switch works the same way as an expression. Where a branch needs several statements the last one is the value.

For a single condition the conditional operator is shorter still, and reads the way it does in C: let n = a > b ? a : b.

3.2 guard

guard states a condition that must hold for the rest of the function to make sense, and its else block must leave: return, throw, break, continue, or a call that never comes back.

// guard keeps the happy path flat and puts every exit at the top.
func check(_ text: String) -> String {
    guard !text.isEmpty else {
        return "no input"
    }
    guard let n = Int(text) else {
        return "\"\(text)\" is not a number"
    }
    guard n >= 0, n < 150 else {
        return "\(n) is not a plausible age"
    }
    return "age \(n)"
}

for input in ["", "abc", "-3", "41"] {
    print(check(input))
}
no input
"abc" is not a number
-3 is not a plausible age
age 41

Written with if this is four levels of nesting and the real work is at the bottom, squeezed against the right margin. Written with guard the failures are at the top in the order you think of them and the real work is at the left, and that is the shape most Swift functions take.

Two details make it work. A comma between conditions means “and”, and each one is checked in order, so n >= 0, n < 150 is a single guard. And a name bound by guard let stays in scope after the guard, for the rest of the enclosing block, which is the reason to write guard let rather than if let. Chapter 5 covers what let n = Int(text) is doing inside a condition.

3.3 Loops

while and repeat-while are C’s while and do-while under different spelling. for-in walks anything that can be walked.

// The forms a loop takes.
let names = ["ada", "grace", "alan", "edsger"]

for (i, name) in names.enumerated() {
    print(i, name)
}

for name in names where name.count > 4 {
    print("long:", name)
}

for n in stride(from: 0, to: 20, by: 6) {
    print(n, terminator: " ")
}
print()

var countdown = 3
repeat {
    print("tick \(countdown)")
    countdown -= 1
} while countdown > 0
0 ada
1 grace
2 alan
3 edsger
long: grace
long: edsger
0 6 12 18 
tick 3
tick 2
tick 1

enumerated() pairs each element with its position, and the pair is taken apart in the loop variable the way a tuple is. Reach for it rather than counting indices by hand.

A where clause on a for skips the elements that fail it, which is continue said earlier and more visibly.

stride(from:to:by:) is the loop C writes with three clauses. The to: form excludes the end and through: includes it, and the step may be negative, which is how you count down. A range cannot do that: 10...0 is a runtime error rather than a backwards range, so stride(from: 10, through: 0, by: -1) or (0...10).reversed() is how you go the other way.

3.4 break and continue

break leaves the nearest loop and continue starts its next turn. When the loop you want to leave is not the nearest one, label it:

// Labeled break: stop both loops at the first match.
let grid = [
    [3, 8, 1],
    [9, 4, 6],
    [7, 2, 5],
]

var found = "not found"

rows: for (r, row) in grid.enumerated() {
    for (c, value) in row.enumerated() {
        if value == 4 {
            found = "row \(r), column \(c)"
            break rows
        }
    }
}

print(found)
row 1, column 1

A label is a name and a colon in front of for, while, repeat or if, and break label or continue label names it. This is the one place Swift offers something that looks like a goto, it is confined to loops you can see on the screen, and it beats the flag variable that the alternative requires.

Exercise 3-1. Rewrite search without a label, returning from a function instead, and decide which you prefer.

Exercise 3-2. Print the numbers 1 to 100, replacing multiples of three with fizz, multiples of five with buzz, and multiples of both with fizzbuzz. Write it twice, once with if and once with a switch over a tuple of the two remainders.

3.5 switch

A switch compares a value against patterns and runs the first arm that matches. That much is C. Everything else about it is not.

// Classify a point by where it sits, using one switch over a tuple.
func describe(_ p: (x: Int, y: Int)) -> String {
    switch p {
    case (0, 0):
        return "the origin"
    case (_, 0):
        return "on the x axis"
    case (0, _):
        return "on the y axis"
    case let (x, y) where x == y:
        return "on the diagonal at \(x)"
    case let (x, y) where abs(x) <= 2 && abs(y) <= 2:
        return "inside the box at \(x),\(y)"
    default:
        return "somewhere else"
    }
}

for p in [(0, 0), (3, 0), (0, -4), (5, 5), (1, 2), (9, 4)] {
    print(describe(p))
}
the origin
on the x axis
on the y axis
on the diagonal at 5
inside the box at 1,2
somewhere else

Four things are happening that C’s switch cannot do.

It matches a tuple, position by position, with _ standing for any value in that position. It binds: case let (x, y) gives names to the parts of whatever matched, and those names are in scope in the arm. It filters: a where clause narrows an arm to the cases that also satisfy a condition. And it switches on things that are not integers, since strings, ranges, tuples and the enums of Chapter 8 are all matchable.

The two rules that change how you write code are these. Cases do not fall through, so there is no break to forget and no bug when you do; the fallthrough keyword exists for the rare time you want C’s behavior and it is rare. And a switch must be exhaustive. Over an Int or a String that means you need a default, which is why one is there above. Over a type with a fixed set of values it means the compiler counts the cases for you:

non_exhaustive.swift:6:5: error: switch must be exhaustive
 5 | func turn(_ d: Direction) -> String {
 6 |     switch d {
   |     |- error: switch must be exhaustive
   |     `- note: add missing case: '.west'

That message is the reason Chapter 8 argues for modeling your data as a fixed set of cases whenever the set really is fixed. Add a fifth direction a year from now and the compiler walks you to every place in the program that has to change, by file and line. No test suite gives you that and no amount of care replaces it.

Arms may list alternatives separated by commas, and may use ranges:

switch score {
case ..<0, 101...:
    print("out of range")
case 0..<60:
    print("fail")
case 60..<90:
    print("pass")
default:
    print("distinction")
}

An empty arm is not allowed. Where you want a case to do nothing you write break, which says you meant it.

Exercise 3-3. Write a function that takes an HTTP status code and returns its class as a string, using range patterns and no if.

Exercise 3-4. Rewrite describe so that the diagonal case reports whether the point is on the rising or falling diagonal, without adding an if to the body of an arm.

Exercise 3-5. Find out what fallthrough does to the value binding in the arm it falls into, and why the compiler refuses some of those cases.