Lesson 4 • Beginner
Control Flow 🔀
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.
What You'll Learn in This Lesson
- Branch with if / else if / else and the ternary operator
- Use Swift's switch: exhaustive, no fall-through, ranges, tuples, where & value binding
- Loop with for-in over ranges and arrays
- Choose between while and repeat-while correctly
- Exit early and validate inputs with guard and guard let
- Safely unwrap optionals using optional binding (if let)
let, var, and basic types. Everything below is genuine Swift: paste it into the free SwiftFiddle (no install) or run it in Xcode on macOS.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)")temperature = 28
☀️ Warm — perfect for the beach!
canVote = true
beach day? false
relax day? true
label = AdultYour 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
// MinorPass
Minor___ blanks, then run it for free on SwiftFiddle and compare with the expected output.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
}📅 Weekday — time to work!
Score 85 → Grade B
Somewhere at (2, 4)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 → MeritMark 64 → Merit___ blanks (think about which range contains 64), then run it on SwiftFiddle.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!")1 squared = 1
2 squared = 4
3 squared = 9
4 squared = 16
5 squared = 25
🍎 Apple
🍎 Banana
🍎 Cherry
0: Apple
1: Banana
2: Cherry
3...
2...
1...
attempt 1
attempt 2
🚀 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)Parsed 42
Hello, Ada!
No name given
Welcome!
Too youngCommon Errors (and the fix)
- "Switch must be exhaustive" — you didn't cover every value. Add a
default:case, or (for an enum) list every case so the compiler knows nothing is left out. - Forgetting
defaulton an Int/String switch — these types have unlimited values, so they always need adefault:even if you think you covered everything. - Using
=instead of==—if x = 5is an assignment and won't compile as a condition. Comparing values needs the double-equals==. - Force-unwrapping with
!instead ofguard let—let n = Int(text)!crashes at runtime ("Unexpectedly found nil") the moment the text isn't a number. Unwrap safely withguard letorif let. - "'guard' body must not fall through" — every
guard ... elseblock must exit the scope. End it withreturn,break,continue, orthrow.
Pro Tips
- 💡 Prefer
switchover longif/else ifchains when you're testing one value against many cases — it's clearer and the compiler checks you covered everything. - 💡 Reach for
guard letat the top of a function to validate inputs and bail early. It keeps the "happy path" flat and unindented. - 💡 Use
wherein aforloop to filter as you go:for i in 1...100 where i % 2 == 0iterates only the even numbers. - 💡 Half-open ranges match array indices: an array of length
nhas valid indices0..<n, so you never run off the end.
📋 Quick Reference — Control Flow
| Construct | Swift Syntax |
|---|---|
| if / else | if x > 5 { ... } else { ... } |
| ternary | let y = cond ? a : b |
| switch | switch v { case 1: ...; default: ... } |
| switch range | case 80..<90: ... |
| for-in range | for i in 1...10 { ... } |
| for-in array | for item in array { ... } |
| while | while x > 0 { ... } |
| repeat-while | repeat { ... } while x > 0 |
| if let | if let n = Int(s) { ... } |
| guard let | guard let n = x else { return } |
Frequently Asked Questions
Q: 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.
Q: 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.
Q: 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.
Q: 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.
Q: 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.
Q: 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.
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!
- ✅
if/else if/elseand the ternary?:make decisions - ✅
switchis exhaustive with no fall-through, and matches ranges, tuples, value binding &where - ✅
for-initerates ranges and arrays;...includes the end,..<stops before it - ✅
whiletests before,repeat-whiletests after (runs at least once) - ✅
if letandguard letunwrap optionals safely — never force-unwrap user input with! - ✅ Next lesson: Functions & Closures — package logic into reusable, named blocks
Sign up for free to track which lessons you've completed and get learning reminders.