- July 26, 2025
- Mins Read
Control flow is about choosing paths (branches), repeating work (loops), and bailing out early (early exits). Swift emphasizes clarity and safety: booleans must be explicit, conditions can bind values, and many features (like switch
patterns or guard
) help you express intent succinctly.
Use for-in
to iterate over sequences (arrays, ranges, dictionaries, sets, strings, async sequences).
let scores = [10, 15, 8]
for score in scores {
print(score)
}
for i in 0..<scores.count { // half-open range
print("Index \(i): \(scores[i])")
}
let capitals = ["UK": "London", "FR": "Paris"]
for (country, city) in capitals {
print("\(country): \(city)")
}
Key points:
Sequence
.for _ in
ignores the loop variable.for-in
over C-style for
(which Swift removed).for try await
lets you iterate over an AsyncSequence
.Swift offers while
and repeat { ... } while
. Use them when you don’t know the exact iteration count beforehand.
var x = 3
while x > 0 {
x -= 1
}
var input: String
repeat {
input = readLine() ?? ""
} while input.isEmpty
while
checks the condition before each iteration.repeat-while
runs the body once, then checks.if
, else if
, else
)Classic branching:
let temperature = 28
if temperature > 30 {
print("Hot")
} else if temperature > 15 {
print("Mild")
} else {
print("Cold")
}
Swift requires a Boolean in if
—no implicit integer-to-bool conversions. You can also bind optionals:
if let first = scores.first, first > 10 {
print("First score is big")
}
Or use guard
for early exits (see §7).
Ternary operator (condition ? a : b
) is available but keep it readable.
switch
switch
in Swift is powerful: exhaustive by default, supports complex patterns, value binding, where
clauses, and doesn’t fall through implicitly.
let point = (2, 0)
switch point {
case (0, 0):
print("Origin")
case (let x, 0):
print("On x-axis at \(x)")
case (0, let y):
print("On y-axis at \(y)")
case (-2...2, -2...2):
print("Near the origin")
default:
print("Somewhere else")
}
Notes:
default
to catch the rest).fallthrough
; each case is its own scope.case "a", "e", "i", "o", "u":
.enum Direction { case north, south, east, west }
let d: Direction = .north
switch d {
case .north, .south:
print("Vertical")
case .east, .west:
print("Horizontal")
}
Swift’s pattern matching extends beyond switch
:
let x, let y
).1...5
, ..<10
, Character("a")..."z"
._
.where
clauses: add extra conditions.let number = 42
switch number {
case let n where n.isMultiple(of: 2):
print("\(n) is even")
default:
break
}
case .some(let value)
or simply case let value?
.if case
/ guard case
/ for case
: pattern match outside switch.if case let .some(v) = Optional(5) { print(v) }
for case let .success(value) in results {
print("Success:", value)
}
Patterns make code expressive: instead of checking and unwrapping manually, you tell the compiler the shape you expect.
These statements alter normal flow inside loops/switches/functions.
continue
Skips the current iteration, moves to the next.
for n in 1...10 {
if n.isMultiple(of: 2) { continue }
print(n) // prints odd numbers
}
break
switch
: ends the case early (rarely needed, since cases don’t fall through).while true {
let cmd = readLine() ?? ""
if cmd == "quit" { break }
}
fallthrough
Forces execution to continue to the next switch
case. Use sparingly (mainly for simple grouping logic).
let ch: Character = "a"
switch ch {
case "a":
print("First letter")
fallthrough
case "b":
print("One of the first two letters")
default:
break
}
return
Exits a function/closure. Can return a value:
func square(_ x: Int) -> Int {
return x * x
}
For single-expression functions, return
can be omitted: func square(_ x: Int) -> Int { x * x }
.
guard
(below) typically ends with a return
, throw
, continue
, or break
to satisfy flow rules.
throw
Signals an error has occurred; control jumps to the nearest catch
. Functions that can throw must be marked throws
.
enum ValidationError: Error { case empty }
func validate(_ s: String) throws {
if s.isEmpty { throw ValidationError.empty }
}
do {
try validate("")
} catch {
print("Invalid:", error)
}
try?
to convert errors into optionals, try!
to assert no error (crashes if wrong).guard
for early exits on failure.guard
guard
is Swift’s “happy path first” tool. You assert what must be true to continue; otherwise you exit.
func greet(_ name: String?) {
guard let name = name, !name.isEmpty else {
print("No name to greet"); return
}
print("Hello, \(name)!")
}
else
block.name
above) remain available after the guard.On Apple platforms, you often need to gate code by OS version. Swift provides compile-time syntax:
if #available(iOS 17, macOS 14, *) {
// Use new API
} else {
// Fallback for older systems
}
*
means “on all other platforms, assume availability”.guard
:guard #available(iOS 16, *) else {
// Provide alternative path
return
}
useNewStuff()
You can also mark declarations:
@available(iOS 15, *)
func newFeature() { /* ... */ }
Calling newFeature()
from code that runs on earlier iOS requires an availability check.
enum FetchError: Error { case networkDown, badData }
func process(scores: [Int]?) throws -> Double {
// Early exit if no data
guard let scores, !scores.isEmpty else { throw FetchError.badData }
var total = 0
for score in scores {
if score < 0 { continue } // skip invalid
total += score
if total > 1_000 { break } // cap work
}
let avg = Double(total) / Double(scores.count)
switch avg {
case 0..<50: print("Low")
case 50..<80: print("Medium")
case 80...100: print("High")
default: print("Out of range")
}
return avg
}
if #available(iOS 17, *) {
// New APIs only if iOS 17+
}
This snippet shows: optional binding, guard
exit, for-in
with continue
/break
, switch
with ranges, and error throwing.
if/else
, switch
, pattern matching (if case
, guard case
).for-in
(preferred), while
, repeat-while
.guard
+ return/throw/break/continue
.continue
, break
, fallthrough
, return
, throw
.if #available
, @available
.
NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use. 💻 Installation 📦 Swift Package Manager Using Swift Package Manager, add ...
An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations and programmatic navigation. NavigationStack Installation ...
With SwiftUI Router you can power your SwiftUI app with path-based routing. By utilizing a path-based system, navigation in your app becomes ...
This package takes SwiftUI's familiar and powerful NavigationStack API and gives it superpowers, allowing you to use the same API not just ...