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
caseandif caseabove. -
Optionals —
if let/guard let, the optional-specific counterparts toif case/guard case. -
Error Handling —
throw/try/catch, which interact withdeferexactly as described above. -
Functions —
guard/deferas typically used inside a function body.