Control Flow

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Every control-flow construct below is a Swift statement first, but several of them — if and switch chief among them — also double as expressions that produce a value, which is what lets a let be initialized directly from a multi-branch decision instead of through a mutable var set from every branch.

for-in Loops

for index in 1...5 { print("\(index) times 5 is \(index * 5)") }        // over a ClosedRange

let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names { print("Hello, \(name)!") }                            // over an Array

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs { print("\(animalName)s have \(legCount) legs") }  // over a Dictionary

for _ in 1...3 { print("knock!") }                                          // wildcard: index itself unused

for tickMark in stride(from: 0, to: 60, by: 5) { print(tickMark) }          // over stride(from:to:by:)

while and repeat-while

var square = 0
var diceRoll = 0
while square < 25 {
    diceRoll += 1
    if diceRoll == 7 { diceRoll = 1 }
    square += diceRoll
}

var count = 0
repeat {
    count += 1                 // the body always runs at least once, unlike `while`
} while count < 3

if/else and switch as Statements — and as Expressions

let temperatureInFahrenheit = 90
if temperatureInFahrenheit <= 32 {
    print("It's very cold, wear a scarf.")
} else if temperatureInFahrenheit >= 86 {
    print("It's really warm, don't forget to wear sunscreen.")
} else {
    print("It's not that cold, wear a t-shirt.")
}

// As an expression (Swift 5.9+): every branch must produce the same type, and every path must be covered.
let weatherAdvice = if temperatureInFahrenheit <= 32 {
    "Wear a scarf."
} else if temperatureInFahrenheit >= 86 {
    "Wear sunscreen."
} else {
    "A t-shirt will do."
}

let approximateCount = 62
let countedThings = "moons orbiting Saturn"
let naturalCount: String = switch approximateCount {
case 0: "no"
case 1..<5: "a few"
case 5..<12: "several"
case 12..<100: "dozens of"
default: "a lot of"
}
print("There are \(naturalCount) \(countedThings).")

Using if/switch as expressions removes the need for a var that every branch assigns into, or a function whose only job is to return from each branch — but every branch must agree on the produced type, and (for switch) the switch itself must still be exhaustive.

if case and guard case

let point = (1, 1)
if case (0, 0) = point {
    print("origin")
} else if case (_, 0) = point {
    print("on the x-axis")
} else {
    print("elsewhere")
}

func process(_ value: Int?) {
    guard case let .some(unwrapped) = value else {
        print("nothing to do")
        return
    }
    print("got \(unwrapped)")
}

if case/guard case match a single pattern against a value without a full switch — useful when only one shape out of several matters here; see Pattern Matching for every pattern kind these accept.

switch Exhaustiveness, Interval and Tuple Matching, Value Binding, where, and Compound Cases

let anotherPoint = (1, -1)
switch anotherPoint {
case (0, 0):
    print("at the origin")
case (let x, 0):
    print("on the x-axis, at x = \(x)")                     // value binding: x is bound from the tuple
case (0, let y):
    print("on the y-axis, at y = \(y)")
case let (x, y) where x == y:
    print("on the line x == y, at \(x)")                     // `where` adds an extra guard condition
case let (x, y) where x == -y:
    print("on the line x == -y, at \(x)")
case (1, 1), (-1, -1):
    print("a compound case -- matches either tuple")          // multiple patterns share one body
default:
    print("(\(anotherPoint.0), \(anotherPoint.1)) is just some arbitrary point")
}

A Swift switch must be exhaustive: every possible value of the switch expression’s type has to be covered, by explicit cases, a default, or (for an enum with every case listed) implicitly — there is no C-style fallthrough-by-default and no way to accidentally forget a case silently.

@unknown default

enum Temperature { case cold, mild, hot }

func advice(for temperature: Temperature) -> String {
    switch temperature {
    case .cold: return "Wear a coat."
    case .mild: return "A light jacket will do."
    @unknown default: return "Not sure -- dress in layers."   // guards against a future case added upstream
    }
}

@unknown default marks a default case as a forward-compatibility fallback rather than an intentional catch-all: the compiler still warns if the switch isn’t otherwise exhaustive today, but stays silent about future cases a library owner might add to a public (non-frozen) enum later, so the warning appears only where it is actionable.

continue, break, fallthrough, and Labeled Statements

let puzzleInput = "great minds think alike"
var puzzleOutput = ""
for character in puzzleInput {
    switch character {
    case "a", "e", "i", "o", "u", " ":
        continue                                              // skips straight to the next iteration
    default:
        puzzleOutput.append(character)
    }
}
print(puzzleOutput)                                             // "grtmndsthnklk"

let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " a prime number, and also"
    fallthrough                                                  // explicitly falls into the next case's body
default:
    description += " an integer."
}

gameLoop: while true {                                            // a labeled statement
    for move in ["north", "north", "east"] {
        if move == "east" { break gameLoop }                       // breaks the labeled while, not just the for
        print("moving \(move)")
    }
}

break on its own exits only its immediately enclosing loop or switch; a labeled statement (label: while, label: for) lets break label/continue label target an outer loop directly, which is the only way to skip past an enclosing switch from within one of its cases — break alone there would just exit the switch.

guard Early Exit

func greet(person: [String: String]) {
    guard let name = person["name"] else {
        print("No name provided.")
        return                                                   // the else branch must exit the enclosing scope
    }
    guard let location = person["location"] else {
        print("Hello \(name)! I hope the weather is nice near you.")
        return
    }
    print("Hello \(name)! I hope the weather is nice in \(location).")
}

guard’s condition must be true to continue past it, the inverse of `if let, and its else block is required to leave the current scope (return, break, continue, throw, or a call to a Never-returning function) — the compiler enforces this, which is what makes guard reliable for flattening validation into a straight-line "happy path" instead of nested `if let`s.

defer

func processFile(filename: String) throws {
    if exists(filename) {
        let file = open(filename)
        defer {
            close(file)                                           // runs when this scope exits, however it exits
        }
        while let line = try file.readline() {
            // process the line
        }
        // close(file) runs here, at the end of the scope --
        // and it would run here too if `try` above threw instead.
    }
}

A defer block always runs when execution leaves its enclosing scope — by falling off the end, by return, or by throwing — which makes it the idiomatic place for cleanup that must happen regardless of which exit path was taken; multiple `defer`s in the same scope run in reverse order of appearance (last written, first run).

#available and #unavailable

if #available(iOS 17, macOS 14, *) {
    // use an API introduced in these platform versions
} else {
    // fall back for older platforms
}

func configure() {
    guard #available(iOS 17, *) else { return }
    // API requiring iOS 17+ is safe to use for the rest of this function
}

if #unavailable(iOS 17) {
    // the negation of #available -- true when none of the listed versions are met
}

#available/#unavailable are compile-time-checked platform-version conditions (not ordinary boolean expressions), so the compiler can verify that code inside their true branch only calls APIs actually available there — the trailing * is required and stands for "any other platform."

See Also

  • Pattern Matching — the full pattern grammar behind every case and if case above.

  • Optionals — if let/guard let, the optional-specific counterparts to if case/guard case.

  • Error Handling — throw/try/catch, which interact with defer exactly as described above.

  • Functions — guard/defer as typically used inside a function body.