Control Flow

Reviewed & published by Brayan K

By the end of this lesson you'll make decisions with if/else, wield Swift's exhaustive switch with ranges and pattern matching, repeat work with for-in, while and repeat-while, and safely unwrap optionals with guard let and if let — the logic skeleton of every real Swift program.

Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn in This Lesson

1️⃣ Conditionals: if / else

A conditional runs code only when a test is true. The test must be a Bool — a true/false value — which you build with comparisons like >, >=, == (equal) and != (not equal), and combine with &&, ||, and !. Read this worked example and run it, then you'll write your own.

// if / else if / else — your program makes a decision.
// Swift needs NO parentheses around the test, but braces { } are ALWAYS required.
let temperature = 28
print("temperature = \(temperature)")

if temperature > 35 {
    print("🔥 Extremely hot — stay hydrated.")
} else if temperature > 25 {
    print("☀️ Warm — perfect for the beach!")   // 28 > 25, so THIS line runs
} else if temperature > 15 {
    print("🌤️ Mild — grab a light jacket.")
} else {
    print("🧥 Cold — wear a coat.")
}

// A condition is anything that evaluates to a Bool (true / false).
let age = 20
let canVote = age >= 18            // >= means "greater than or equal to" → true
print("canVote = \(canVote)")

// Combine conditions with && (AND), || (OR), ! (NOT).
let isWeekend = true
let isSunny = false
print("beach day? \(isWeekend && isSunny)")   // both must be true → false
print("relax day? \(isWeekend || isSunny)")   // either is enough  → true

// Ternary operator: condition ? valueIfTrue : valueIfFalse
let label = age >= 18 ? "Adult" : "Minor"
print("label = \(label)")

Your turn. The program below is almost finished — fill in the blanks marked ___ using the // 👉 hints, then run it and check the expected output.

// 🎯 YOUR TURN — replace each ___ then run it on SwiftFiddle.

let score = 72

// 1) Print "Pass" if score is 50 or more, otherwise "Fail".
//    Fill in the condition (use >= for "greater than or equal to").
if score ___ 50 {            // 👉 replace ___ with the >= operator
    print("Pass")
} else {
    print("Fail")
}

// 2) Set message to "Adult" when age is 18+, else "Minor", using a ternary.
let age = 16
let message = age >= 18 ? ___ : ___   // 👉 two strings in "double quotes"
print(message)

// ✅ Expected output:
//    Pass
//    Minor

2️⃣ Swift's Powerful switch

Swift's switch is in a different league from C or Java. Two rules shape everything: it must be exhaustive (cover every possible value — usually via a default: case), and there is no implicit fall-through (only the matching case runs, so you never write break). On top of that you can match ranges (90...100), match and destructure tuples, bind matched values with let, and add extra conditions with where.

// Swift's switch is exhaustive (must cover every case) and has
// NO implicit fallthrough — only the matching case runs, no break needed.

// (a) Match several values in one case.
let day = "Wednesday"
switch day {
case "Saturday", "Sunday":
    print("🎉 Weekend — relax!")
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
    print("📅 Weekday — time to work!")   // Wednesday matches here
default:
    print("❓ Unknown day")               // default makes it exhaustive
}

// (b) Match RANGES. 1...5 is closed (incl. 5); 1..<5 is half-open (excl. 5).
let scoreValue = 85
let grade: String
switch scoreValue {
case 90...100: grade = "A"
case 80..<90:  grade = "B"     // 80 up to but NOT including 90 → 85 lands here
case 70..<80:  grade = "C"
default:       grade = "F"
}
print("Score \(scoreValue) → Grade \(grade)")

// (c) Match TUPLES, bind values with let, and refine with where.
let point = (2, 4)
switch point {
case (0, 0):
    print("At the origin")
case (let x, 0):
    print("On the x-axis at \(x)")        // value binding: capture x
case (let x, let y) where x == y:
    print("On the diagonal")
case (let x, let y):
    print("Somewhere at (\(x), \(y))")    // binds both coordinates
}

Now you finish a grade classifier. Fill in the missing range and the catch-all keyword:

// 🎯 YOUR TURN — finish the grade switch by filling in the ranges.

let mark = 64
let grade: String

switch mark {
case 70...100: grade = "Distinction"
case ___:      grade = "Merit"        // 👉 60 up to (not incl.) 70  → 60..<70
case 50..<60:  grade = "Pass"
___:           grade = "Fail"         // 👉 the catch-all keyword that makes it exhaustive
}

print("Mark \(mark) → \(grade)")

// ✅ Expected output:
//    Mark 64 → Merit

3️⃣ Loops: for-in, while & repeat-while

Loops repeat work. for-in walks through a sequence — a range like 1...5 or an array. while repeats while a condition holds, checking before each pass (so it can run zero times). repeat-while checks after, so its body always runs at least once. A range with ... includes its end; with ..< it stops just before it.

// for-in over a CLOSED range (1 through 5 inclusive).
for i in 1...5 {
    print("\(i) squared = \(i * i)")
}

// for-in over an ARRAY.
let fruits = ["Apple", "Banana", "Cherry"]
for fruit in fruits {
    print("🍎 \(fruit)")
}

// enumerated() gives (index, value) pairs.
for (index, fruit) in fruits.enumerated() {
    print("\(index): \(fruit)")
}

// while — checks the condition BEFORE each pass (may run zero times).
var countdown = 3
while countdown > 0 {
    print("\(countdown)...")
    countdown -= 1
}

// repeat-while — runs the body FIRST, then checks. Always runs at least once.
var attempts = 0
repeat {
    attempts += 1
    print("attempt \(attempts)")
} while attempts < 2

print("🚀 Liftoff!")

4️⃣ Optionals: guard let & if let

An optional is a value that might be missing — its type ends in ?, and "missing" is written nil. You can't use an optional directly; you must unwrap it. if let (optional binding) runs a block only when a value is present. guard let is the early-exit twin: if the value is missing it forces you to leave the scope right away, and otherwise the unwrapped value stays available for the rest of the function — keeping your main logic flat instead of buried in nested ifs.

// An OPTIONAL may hold a value OR nothing (nil). Its type ends with ?.
// You must safely unwrap it before using the value.

let input = "42"

// if let — runs the block ONLY if the conversion succeeds (binds 'number').
if let number = Int(input) {
    print("Parsed \(number)")        // Int("42") succeeds → 42
} else {
    print("Not a number")
}

// guard let — the "early exit". If unwrapping fails, you MUST leave the scope.
// The unwrapped value then stays available for the REST of the function.
func greet(_ name: String?) {
    guard let name = name else {
        print("No name given")        // guard's else must exit (return here)
        return
    }
    print("Hello, \(name)!")          // 'name' is a real String from here on
}

greet("Ada")
greet(nil)

// guard also works on plain conditions — fail fast, keep the happy path flat.
func checkAge(_ age: Int) {
    guard age >= 18 else {
        print("Too young")
        return
    }
    print("Welcome!")
}

checkAge(20)
checkAge(15)

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Control Flow

ConstructSwift Syntax
if / elseif x > 5 { ... } else { ... }
ternarylet y = cond ? a : b
switchswitch v { case 1: ...; default: ... }
switch rangecase 80..<90: ...
for-in rangefor i in 1...10 { ... }
for-in arrayfor item in array { ... }
whilewhile x > 0 { ... }
repeat-whilerepeat { ... } while x > 0
if letif let n = Int(s) { ... }
guard letguard let n = x else { return }

Mini-Challenge: FizzBuzz

No blanks this time — just a brief and an outline. Write it yourself, run it, and check your output against the expected lines in the comments. FizzBuzz combines a loop, the remainder operator, and an if/else chain — exactly this lesson's toolkit.

// 🎯 MINI-CHALLENGE: FizzBuzz (a classic interview warm-up)
// Loop over the numbers 1 through 15 and, for each one, print:
//   • "Fizz"     if it's divisible by 3   (use  n % 3 == 0)
//   • "Buzz"     if it's divisible by 5
//   • "FizzBuzz" if it's divisible by BOTH 3 and 5  (check this FIRST)
//   • the number itself otherwise
//
// Hints: use  for n in 1...15  and an if / else if / else chain.
//        % is the remainder operator;  15 % 5 == 0  is true.
//
// ✅ Expected output (first 6 lines):
//    1
//    2
//    Fizz
//    4
//    Buzz
//    Fizz

// your code here

🎉 Lesson Complete!

Practice quiz

Does Swift require parentheses around an if condition?

  • Yes, always
  • Only with &&
  • No, but braces are always required
  • Only for Bool

Answer: No, but braces are always required. Swift needs no parentheses around the test, but the { } braces are mandatory.

A Swift switch over an Int must be...

  • Sorted
  • In one line
  • Wrapped in a loop
  • Exhaustive (cover every value)

Answer: Exhaustive (cover every value). Switch must handle every possible value, usually via a default: case.

Does Swift fall through to the next case automatically?

  • No, only the matching case runs
  • Yes, like C
  • Only for strings
  • Only with break

Answer: No, only the matching case runs. There is no implicit fallthrough; only the matched case runs.

What does the range 1..<5 include?

  • 1, 2, 3, 4, 5
  • 1, 2, 3, 4
  • 2, 3, 4
  • Nothing

Answer: 1, 2, 3, 4. ..< is half-open: it stops before 5, giving 1, 2, 3, 4.

Which loop always runs its body at least once?

  • for-in
  • while
  • repeat-while
  • guard

Answer: repeat-while. repeat-while checks the condition after the body, so it runs at least once.

What must every guard ... else block do?

  • Print an error
  • Return a Bool
  • Loop again
  • Exit the scope (return/break/throw)

Answer: Exit the scope (return/break/throw). A guard's else must leave the current scope.

Which operator tests equality in a condition?

  • ==
  • =
  • ===
  • :=

Answer: ==. == compares values; a single = is assignment.

What does the ternary cond ? a : b do?

  • Loops
  • Picks a if cond is true, else b
  • Throws an error
  • Declares a constant

Answer: Picks a if cond is true, else b. The ternary yields a when cond is true and b otherwise.

Which combines two conditions with logical AND?

  • and
  • +
  • &&
  • &

Answer: &&. && is logical AND in Swift.

What does if let number = Int(input) do?

  • Forces the value
  • Throws if nil
  • Declares a global
  • Runs the block only if the conversion succeeds

Answer: Runs the block only if the conversion succeeds. Optional binding runs the block only when a value is present.

Continue this course

Frequently asked questions

Do I need parentheses around an if condition in Swift?

No. Swift does not require parentheses around the condition (unlike C or Java), so write 'if x > 5 { ... }'. The curly braces, however, are always required — even for a single-line body.

Why does my switch say it must be exhaustive?

A Swift switch has to handle every possible value. For types with unlimited values (like Int or String) you cover the rest with a 'default:' case. When switching over an enum you can instead list every case, and then no default is needed.

Does Swift fall through to the next case like C does?

No. Swift runs only the matching case and then stops — there is no implicit fallthrough and you never need 'break'. If you genuinely want the next case to run too, opt in with the explicit 'fallthrough' keyword.

What is the difference between 1...5 and 1..<5?

1...5 is a closed range and includes 5, giving 1, 2, 3, 4, 5. 1..<5 is a half-open range and stops before 5, giving 1, 2, 3, 4. Half-open ranges are handy for array indices because an array of length n has indices 0..<n.

When should I use guard instead of if let?

Use guard let for early exit: it unwraps a value and, if that fails, you must leave the scope (return/break/throw). The unwrapped value then stays in scope for the rest of the function, keeping your main logic flat and unindented. Use if let when you only need the value inside one small block.

What is the difference between while and repeat-while?

A while loop checks its condition before each pass, so it can run zero times. A repeat-while loop runs the body once and then checks, so it always runs at least once — use it when the work must happen before the test makes sense.