Control Flow, Functions & Closures

iOS Development Fundamentals

Chapter 3 · Control Flow, Functions & Closures

This chapter covers the last piece of core Swift before SwiftUI itself starts in Chapter 5: branching and looping, writing real functions, and closures — a feature that looks like a small syntax detail here, but turns out to be exactly what SwiftUI's own view code leans on constantly.

if/else and switch

let age = 15 if age < 13 { print("Child") } else if age < 18 { print("Teenager") } else { print("Adult") }

Swift's switch is genuinely more capable than the C-style version, and behaves differently in one important real way: it doesn't require a break, and it does not fall through to the next case by default.

let age = 15 switch age { case 0..<13: print("Child") case 13..<18: print("Teenager") case 18...: print("Adult") default: print("Unknown") }
A Real, Deliberate Difference from C/Java/JS
In Swift, each real case stops automatically once it finishes — a genuine, deliberate reversal of C-family fall-through behavior. The rarely-needed fallthrough keyword exists specifically for the rare case where continuing into the next case is actually wanted. A case can also match several values at once, comma-separated: case 1, 2, 3:.

for-in and while Loops

let scores = [12, 45, 8, 33] for score in scores { print("Score: \(score)") } var countdown = 3 while countdown > 0 { print(countdown) countdown -= 1 }

Functions

A Swift function parameter can have two real, independent names: an external name (used by the caller) and an internal name (used inside the function body). Writing just one name makes it both — writing _ as the external name suppresses it entirely at the call site.

func greet(_ name: String, formally isFormal: Bool = false) -> String { if isFormal { return "Good day, \(name)." } else { return "Hey \(name)!" } } greet("Sam") // "Hey Sam!" — isFormal uses its default greet("Sam", formally: true) // "Good day, Sam."
PieceWhat It Does
_ nameExternal name suppressed — the caller writes greet("Sam"), not greet(name: "Sam").
formally isFormalExternal name formally, internal name isFormal — the caller writes formally:, the body reads isFormal.
= falseA real default parameter value — the argument can be omitted entirely at the call site.

Closures

A closure is a real, self-contained block of functionality that can be passed around and used like any other value — assigned to a constant, passed as an argument, returned from a function.

let greetClosure = { (name: String) -> String in return "Hello, \(name)!" } greetClosure("Alice") // "Hello, Alice!"

When a closure is the last argument to a function, Swift allows a real, dedicated shorthand — trailing closure syntax — writing the closure outside the parentheses:

func combine(_ a: Int, _ b: Int, using operation: (Int, Int) -> Int) -> Int { return operation(a, b) } // Regular call: combine(5, 3, using: { a, b in a + b }) // Trailing closure syntax — the closure moves outside the parentheses: combine(5, 3) { a, b in a + b }
Why This Matters for the Next Chapter
That trailing-closure shape — a function call, followed by a block of code in braces — is exactly what a SwiftUI view builder looks like: VStack { Text("Hi") } is a real function call to VStack's own initializer, with a trailing closure describing its child views. Nothing about SwiftUI's own layout code in Chapter 7 onward is new syntax — it's this same trailing-closure pattern, applied to real view types.

Closures Capture Values from Their Surrounding Scope

func makeCounter() -> () -> Int { var count = 0 return { count += 1 // captures 'count' from makeCounter's own scope return count } } let counter = makeCounter() print(counter()) // 1 print(counter()) // 2
A Real Consequence of Capturing
Each call to makeCounter() creates its own genuinely independent count — the returned closure keeps that specific variable alive for as long as the closure itself exists, even after makeCounter has already returned. This same capturing behavior is what makes closures genuinely powerful for SwiftUI, and it's also the real source of a subtle memory-management topic — strong reference cycles — covered properly once Chapter 3 of Architecture & Data introduces classes.

Hands-On Exercises

Exercise 1

Write a function categorize(score: Int) that uses a switch statement with real ranges to print "Fail" (0-49), "Pass" (50-69), or "Distinction" (70 and above). Test it with three real values, one from each range.

📄 View solution
Exercise 2

Write a function repeatAction(times: Int, action: () -> Void) that calls action the given number of times, then call it using trailing closure syntax to print "Tap!" three times.

📄 View solution
Exercise 3

Explain, in your own words, why the two calls to makeCounter() in this chapter's own example each produce a genuinely independent counter, rather than sharing one underlying count value.

📄 View solution

Chapter 3 Quick Reference

  • Swift's switch needs no break and doesn't fall through by default — use fallthrough for the rare case that needs it, and ranges/comma-separated values in one case
  • Function parameters can have separate external/internal names, plus real default values
  • Closures are self-contained blocks of functionality that can be assigned, passed, and returned like any value
  • Trailing closure syntax (fn(args) { ... }) is exactly the shape SwiftUI's own view builders use — VStack { ... } is just a function call with a trailing closure
  • Closures capture variables from their surrounding scope, keeping them alive for as long as the closure itself exists