Beginner

Control Flow in Swift

Master conditionals, loops, and pattern-matching control flow in Swift with if/else, switch, for-in, while, guard, and Docker-ready examples

Control flow is how a program decides what to do and how many times to do it. Swift offers a familiar set of constructs — if/else, for-in, while — but pairs them with features that reflect its safety-first, expressive design. The switch statement is far more powerful than the C-style switch most developers know, supporting full pattern matching, value binding, and range checks. And the guard statement provides an idiomatic way to handle the “unhappy path” early and keep the main logic flat and readable.

As a multi-paradigm language, Swift lets you write imperative loops when they’re clearest, but also encourages a more functional style where switch matches on shape and value rather than chaining conditionals. A standout safety feature is that switch statements must be exhaustive — the compiler forces you to handle every possible case, eliminating a whole class of “I forgot that branch” bugs.

In this tutorial you’ll learn Swift’s conditionals (if/else if/else and the ternary operator), its pattern-matching switch, the various loop forms (for-in, while, repeat-while), loop control with break and continue, and the early-exit guard statement. Every example is a complete, runnable script.

Conditionals: if, else if, and the Ternary Operator

The if statement runs a block when its condition is true. Unlike C, Swift does not require parentheses around the condition, but braces are always mandatory — even for a single statement.

Create a file named conditionals.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let temperature = 18

// if / else if / else
if temperature <= 0 {
    print("Freezing")
} else if temperature < 15 {
    print("Cold")
} else if temperature < 25 {
    print("Mild")
} else {
    print("Hot")
}

// The ternary conditional operator: condition ? valueIfTrue : valueIfFalse
let score = 72
let result = score >= 60 ? "Pass" : "Fail"
print("Result: \(result)")

// Conditions must be Bool — Swift has no "truthy" integers
let isReady = true
if isReady {
    print("Ready to go")
}

Note that Swift conditions must evaluate to a Bool. You cannot write if temperature and expect a non-zero number to be “truthy” — this strictness prevents a common source of bugs.

The switch Statement and Pattern Matching

Swift’s switch is one of its most powerful features. Cases don’t “fall through” by default (no break needed), each case can match ranges or tuples, and the whole statement must be exhaustive.

Create a file named switch_demo.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let statusCode = 404

// Matching against ranges
switch statusCode {
case 200:
    print("OK")
case 300..<400:
    print("Redirect")
case 400..<500:
    print("Client error")
case 500..<600:
    print("Server error")
default:
    print("Unknown status")
}

// Matching tuples with value binding and a where clause
let point = (x: 3, y: 0)
switch point {
case (0, 0):
    print("Origin")
case (let x, 0):
    print("On the x-axis at \(x)")
case (0, let y):
    print("On the y-axis at \(y)")
case let (x, y) where x == y:
    print("On the diagonal")
default:
    print("Somewhere at \(point)")
}

// Multiple values in one case
let letter: Character = "e"
switch letter {
case "a", "e", "i", "o", "u":
    print("\(letter) is a vowel")
default:
    print("\(letter) is a consonant")
}

Here the case (let x, 0) syntax binds the matched value to a constant you can use in the body, and where adds an extra condition. Because the cases above cover every possibility (with default), the switch compiles.

For-In Loops

The for-in loop iterates over any sequence: ranges, arrays, dictionaries, strings, and more. Swift’s range operators make numeric loops concise.

Create a file named for_loops.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Closed range: 1 through 5 inclusive
for i in 1...5 {
    print("Count: \(i)")
}

// Half-open range: 0 up to (but not including) 3
print("---")
for i in 0..<3 {
    print("Index: \(i)")
}

// Iterating an array
let languages = ["Swift", "Rust", "Go"]
print("---")
for language in languages {
    print("Language: \(language)")
}

// enumerated() gives both index and value
print("---")
for (index, language) in languages.enumerated() {
    print("\(index): \(language)")
}

// stride(from:to:by:) for custom steps
print("---")
for even in stride(from: 0, to: 10, by: 2) {
    print("Even: \(even)")
}

// Use _ when you only need to repeat, not the value
print("---")
var total = 0
for _ in 1...3 {
    total += 10
}
print("Total: \(total)")

The underscore _ is Swift’s “I don’t care about this value” placeholder — useful when you want to repeat an action a fixed number of times.

While and Repeat-While Loops

When you don’t know the iteration count in advance, use a while loop (checks the condition before each pass) or a repeat-while loop (checks after, so the body always runs at least once). Swift’s repeat-while is the equivalent of C’s do-while.

Create a file named while_loops.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// while: condition checked before each iteration
var countdown = 3
while countdown > 0 {
    print("T-minus \(countdown)")
    countdown -= 1
}
print("Liftoff!")

print("---")

// repeat-while: body runs at least once, condition checked after
var attempts = 0
repeat {
    attempts += 1
    print("Attempt \(attempts)")
} while attempts < 3

print("Done after \(attempts) attempts")

Loop Control: break and continue

break exits a loop immediately; continue skips to the next iteration. Swift also supports labeled statements, which let you break out of an outer loop from within a nested one.

Create a file named loop_control.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// continue: skip odd numbers
print("Even numbers:")
for i in 1...10 {
    if i % 2 != 0 {
        continue
    }
    print(i)
}

// break: stop at the first number over 4
print("Stopping early:")
for i in 1...10 {
    if i > 4 {
        break
    }
    print(i)
}

// Labeled loops: break out of the outer loop
print("Searching grid:")
outer: for row in 1...3 {
    for col in 1...3 {
        if row == 2 && col == 2 {
            print("Found target at (\(row), \(col))")
            break outer
        }
        print("Checking (\(row), \(col))")
    }
}

The label outer: names the outer loop, so break outer terminates both loops at once — much cleaner than juggling a flag variable.

Early Exit with guard

The guard statement is Swift’s idiomatic tool for early returns. It runs a block (which must exit the current scope) when its condition is false, letting you handle preconditions up front and keep the rest of the function unindented. Crucially, values bound with guard let remain in scope after the guard.

Create a file named guard_demo.swift:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func describeAge(_ input: String) {
    // guard requires the else branch to exit scope (return/break/continue/throw)
    guard let age = Int(input) else {
        print("'\(input)' is not a valid number")
        return
    }

    guard age >= 0 else {
        print("Age cannot be negative")
        return
    }

    // `age` is available here because guard let binds into the outer scope
    if age >= 18 {
        print("\(age): adult")
    } else {
        print("\(age): minor")
    }
}

describeAge("25")
describeAge("15")
describeAge("-4")
describeAge("hello")

Compare this to nesting everything inside if let blocks — guard keeps the “happy path” at the top indentation level, which is why Swift developers reach for it constantly.

Running with Docker

You can run every example above without installing Swift locally by using the official image.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Pull the official Swift image
docker pull swift:6.0

# Run each example
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift conditionals.swift
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift switch_demo.swift
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift for_loops.swift
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift while_loops.swift
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift loop_control.swift
docker run --rm -v $(pwd):/app -w /app swift:6.0 swift guard_demo.swift

Expected Output

Running conditionals.swift:

Mild
Result: Pass
Ready to go

Running switch_demo.swift:

Client error
On the x-axis at 3
e is a vowel

Running for_loops.swift:

Count: 1
Count: 2
Count: 3
Count: 4
Count: 5
---
Index: 0
Index: 1
Index: 2
---
Language: Swift
Language: Rust
Language: Go
---
0: Swift
1: Rust
2: Go
---
Even: 0
Even: 2
Even: 4
Even: 6
Even: 8
---
Total: 30

Running while_loops.swift:

T-minus 3
T-minus 2
T-minus 1
Liftoff!
---
Attempt 1
Attempt 2
Attempt 3
Done after 3 attempts

Running loop_control.swift:

Even numbers:
2
4
6
8
10
Stopping early:
1
2
3
4
Searching grid:
Checking (1, 1)
Checking (1, 2)
Checking (1, 3)
Checking (2, 1)
Found target at (2, 2)

Running guard_demo.swift:

25: adult
15: minor
Age cannot be negative
'hello' is not a valid number

Key Concepts

  • Conditions must be Bool — Swift has no “truthy” values, so if 1 is a compile error; this strictness prevents subtle bugs.
  • switch is exhaustive and pattern-matching — cases can match ranges, tuples, and bind values with let, refined by where clauses, and the compiler forces you to handle every case.
  • No implicit fallthrough — Swift switch cases don’t fall through, so you never need break to stop one case bleeding into the next (use the explicit fallthrough keyword if you actually want it).
  • Rich range operators1...5 (closed) and 0..<3 (half-open), plus stride(from:to:by:), make numeric for-in loops concise and clear.
  • repeat-while runs at least once — it’s Swift’s do-while, checking the condition after the body executes.
  • Labeled loops — naming a loop (e.g. outer:) lets break/continue target an outer loop from inside a nested one, avoiding flag variables.
  • guard enforces early exit — its else block must leave the current scope, and bindings from guard let stay in scope afterward, keeping the main logic flat and readable.

Running Today

All examples can be run using Docker:

docker pull swift:6.0
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining