1.4 — Control flow, functions, and the closure that ate the internet

Opening scenario

You’re reading a SwiftUI tutorial and see:

Button("Save") { try? await viewModel.save() }
    .disabled(viewModel.items.allSatisfy { $0.isComplete })
    .onChange(of: search) { _, new in viewModel.filter(new) }

Three different closures. Three different shapes ({ … }, { $0.isComplete }, { _, new in … }). One language. If you can’t write these from memory by the end of this chapter, every SwiftUI sample you read for the next month will look like noise.

Concept → Why → How → Code

Concept: a function is a named closure; a closure is an anonymous function

Swift makes this duality first-class. Anywhere a function is accepted, you can pass a closure literal; anywhere a closure is accepted, you can pass a function reference. They’re the same kind of value ((Args) -> Result).

Why this design

Because the language was designed in the trailing-closure era (HTML/JS-style callbacks, Ruby blocks, Rust closures). Apple’s APIs are deeply callback-oriented (UIKit delegates, completion handlers, SwiftUI view builders). Treating functions as values keeps that ergonomic.

How: functions — the boring foundation

// (1) Basic function
func greet(name: String) -> String {
    "Hello, \(name)"
}

// (2) External and internal parameter names
func move(from origin: Point, to destination: Point) { … }
move(from: a, to: b)  // reads like English at the call site

// (3) Omit external name with underscore
func square(_ x: Int) -> Int { x * x }
square(4)             // not square(x: 4)

// (4) Default values
func log(_ msg: String, level: LogLevel = .info) { … }

// (5) Variadic parameters
func sum(_ numbers: Int...) -> Int { numbers.reduce(0, +) }
sum(1, 2, 3, 4)       // 10

// (6) inout parameters (mutate the caller's value)
func double(_ x: inout Int) { x *= 2 }
var n = 3; double(&n); print(n)  // 6

The call-site argument labels are Swift’s signature ergonomic choice. move(from: a, to: b) reads naturally; move(a, b) doesn’t. Embrace it.

Code: closure syntax, from longhand to shorthand

Every line below is the same closure:

// 1. Full form
let doubled1 = [1, 2, 3].map({ (x: Int) -> Int in
    return x * 2
})

// 2. Type inference — drop the types
let doubled2 = [1, 2, 3].map({ x in return x * 2 })

// 3. Implicit return when the body is a single expression
let doubled3 = [1, 2, 3].map({ x in x * 2 })

// 4. Shorthand argument names ($0, $1, …)
let doubled4 = [1, 2, 3].map({ $0 * 2 })

// 5. Trailing closure syntax (when the closure is the last argument)
let doubled5 = [1, 2, 3].map { $0 * 2 }

By line 5 you have the form you’ll write 95% of the time. The other forms exist for moments when you genuinely need the clarity.

Code: multiple trailing closures (Swift 5.3+)

UIView.animate(withDuration: 0.3) {
    button.alpha = 0
} completion: { _ in
    button.removeFromSuperview()
}

The first closure is unnamed (the “primary” trailing closure); subsequent ones use their argument label. This is heavily used in SwiftUI:

Button {
    save()
} label: {
    Text("Save").bold()
}

Control flow: only the surprising bits

You already know if, while, for. Swift adds nuances:

// for-in with where clause
for n in 1...100 where n.isMultiple(of: 7) { print(n) }

// switch is exhaustive and pattern-matches richly
switch httpStatus {
case 200..<300:        print("ok")
case 301, 302:         print("redirect")
case let code where code >= 500:  print("server bork: \(code)")
default:               print("???")
}

// if-let / guard-let — see previous chapter

// switch can destructure tuples and enum associated values
switch result {
case .success(let value):       process(value)
case .failure(let error as URLError):  retry(after: error)
case .failure(let other):       log(other)
}

// if and switch are EXPRESSIONS since Swift 5.9
let label = if status == 200 { "OK" } else { "Error" }

let pricing = switch tier {
case .free: 0
case .pro:  9
case .team(let seats): seats * 5
}

The if/switch-as-expression form is one of the modern Swift features most likely to upgrade the readability of code you write daily.

In the wild

  • SwiftUI’s entire view body is closures. var body: some View { … } is a closure under the hood (a @ViewBuilder-attributed one, which we’ll meet in Phase 4).
  • URLSession’s completion-handler API uses closures; the modern async API replaces them but you’ll still maintain both styles in real codebases.
  • Combine’s sink { value in … } and .map { … } chains are closures all the way down.
  • Test frameworks (XCTest, Swift Testing) use trailing closures for assertions: #expect { try parser.parse(input) }.

Common misconceptions

  1. return is always required.” Not when the closure (or function) body is a single expression. func square(_ x: Int) -> Int { x * x } is valid Swift since 5.1.

  2. $0, $1 are magic — I have to use them.” No. They’re shorthand. You can name parameters: .map { value in value * 2 }. Use named parameters when there are 2+ arguments or when the closure body is more than a single line.

  3. “Trailing closures only work with one closure parameter.” Multi-trailing-closure syntax has been around since Swift 5.3 (2020). Use it. Don’t paren-nest closures into oblivion.

  4. if and switch are statements, not expressions.” They are both in modern Swift. Use them as expressions to flatten chained-assignment ladders.

  5. for x in 0..<array.count { array[x] … } is the idiomatic loop.” No. for item in array { … } is. Index iteration is for when you genuinely need the index (use array.enumerated() for (index, element) pairs).

Seasoned engineer’s take

The closure-syntax progression (full form → trailing) is Swift’s most polarizing onboarding hurdle. New engineers see .map { $0.title } and feel locked out. Old engineers see .map({ (item: Item) -> String in return item.title }) and feel pity. Spend an evening writing the same closure five ways in a Playground until shorthand becomes invisible — you’ll save years of code-reading friction.

Two opinions you should form early:

  • Default to func for anything that needs documentation or testing in isolation; default to closures inline when the logic is incidental. A function deserves a name when calling it twice would feel right. Closures are for one-shot transformations.
  • Long closures are a smell. When .map { … } exceeds ~5 lines, extract a func and pass it by reference: .map(transform). Your future self thanks you.

Also: func is overloadable on argument labels, not just on types. move(from:to:) and move(by:) are different functions and that’s normal. Embrace the labels; they document call sites better than any comment.

TIP: When Xcode autocompletes a SwiftUI modifier and inserts { <#code#> }, that’s a trailing closure placeholder. Press Tab to fill it in.

WARNING: Capturing self in a closure that outlives self is the #1 cause of memory leaks in Swift apps. We’ll fix this properly in Memory Management. For now: when in doubt, write [weak self] in at the top of any closure stored as a property or passed to a long-lived callback.

Interview corner

Question: “Explain trailing-closure syntax. Why is [1,2,3].map { $0 * 2 } valid?”

Junior answer:map takes a closure, and trailing-closure syntax lets you write the closure outside the parens. $0 is the first argument.” → Correct. They’ll push: ‘why is this useful?’

Mid-level answer: All of the above, plus: “It’s mostly a readability win — SwiftUI’s view builder would be unbearable without it. Multi-trailing-closure syntax (Swift 5.3) extended this to APIs like UIView.animate(duration:animations:completion:).” → Solid.

Senior answer: Plus: “It’s also a hint about Swift’s design priorities. The language deliberately makes the callee (the API author) work harder to produce ergonomic call sites, instead of pushing complexity onto the caller. That’s why we have argument labels, default parameters, variadics, result builders. Closures and trailing-closure syntax are part of that same design philosophy: optimize the read path, even if writing the API is a little fiddlier. When designing my own APIs I think about which arguments callers will fill in dynamically (label them clearly) versus which can have sensible defaults (give them =).” → That’s the signal of someone who’s designed real APIs, not just consumed them.

Red-flag answer: “Trailing closures are just syntactic sugar — they don’t matter.” → Tells the interviewer you don’t read SwiftUI code.

Lab preview

Lab 1.C (Protocol-oriented calculator) uses closures heavily — you’ll pass arithmetic operations as (Double, Double) -> Double values and compose them at runtime. By the time you finish, the syntax will be muscle memory.


Next: collections. Arrays, dictionaries, sets, and the higher-order functions that make Swift feel like a functional language. → Collections