Swift foundations · core

Closures

Functions without names, the shorthand syntax that shrinks them to nothing, and the capture behaviour that makes them powerful and occasionally dangerous.

12 min read15 min practice0/3 exercises5 recall cards

By the end you will be able to

  • Read closure syntax at every level of shorthand, from full form to `$0`
  • Explain what capturing means and predict what a captured variable holds
  • Use trailing closure syntax and say why SwiftUI depends on it
Guess firstAnswering before you read makes the explanation stick — even when you get it wrong.

makeCounter returns a closure. What do the two calls print?

func makeCounter() -> () -> Int {
    var count = 0
    return { count += 1; return count }
}
let next = makeCounter()
print(next())
print(next())

The same function, five ways

Start with the long form and remove one thing at a time. Every version below does the same job.

let names = ["Charlie", "alice", "Bob"]

// 1. Full form: parameter types, return type, explicit return
names.sorted(by: { (a: String, b: String) -> Bool in
    return a.lowercased() < b.lowercased()
})

// 2. Types inferred from context
names.sorted(by: { a, b in
    return a.lowercased() < b.lowercased()
})

// 3. Single expression: `return` is implicit
names.sorted(by: { a, b in a.lowercased() < b.lowercased() })

// 4. Shorthand argument names
names.sorted(by: { $0.lowercased() < $1.lowercased() })

// 5. Trailing closure: the last argument moves outside the parentheses
names.sorted { $0.lowercased() < $1.lowercased() }

Version 5 is what you will write, and version 1 is what it means. When a closure gets confusing, expanding it back toward version 1 is usually enough to see what is going on.

Swift

Trailing closure syntax

If the final argument is a closure, it can move outside the parentheses. If it is the only argument, the parentheses disappear entirely.

items.forEach({ print($0) })   // ordinary
items.forEach { print($0) }    // trailing

Multiple trailing closures get labels after the first:

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

This syntax is the reason SwiftUI reads the way it does. VStack { … } is a function call whose only argument is a closure that builds the contents. Once you see that, SwiftUI's syntax stops looking like a special language feature and starts looking like ordinary Swift.

What you write in SwiftUI
VStack(spacing: 12) {
    Text("Title")
    Text("Subtitle")
}
What it actually is
VStack(spacing: 12, content: {
    Text("Title")
    Text("Subtitle")
})

Identical. The left is the right with trailing-closure syntax applied. There is no SwiftUI-specific grammar involved — it is a function taking a closure.

Capturing

A closure captures the variables it refers to from its surrounding scope, and keeps them alive for as long as the closure lives.

Swift

Two things worth noticing:

  1. count outlives makeCounter. The closure holds it.
  2. first and second have separate counts. Each call to makeCounter creates a fresh count, and each closure captures its own.

Capture is by reference, not by value: the closure sees the variable, not a snapshot of it.

Predict, then runA wrong prediction you have committed to is worth more than a right answer you read.

Both closures are created before the loop finishes. What does this print?

var total = 0
var actions: [() -> Void] = []

for i in 1...3 {
    actions.append {
        total += i
    }
}

for action in actions {
    action()
}

print(total)

Escaping closures

By default, Swift assumes a closure is used and discarded before the function returns. If you store it for later — in a property, an array, a completion handler — you must mark it @escaping:

var handlers: [() -> Void] = []

func register(_ handler: @escaping () -> Void) {
    handlers.append(handler)   // stored: outlives the call
}

This is not bureaucracy. An escaping closure keeps everything it captured alive, which is where retain cycles come from — the subject of a later lesson. Marking it makes that cost visible.

When self is a class instance, an escaping closure also forces you to acknowledge the capture — write self. explicitly at each use, or put self in the capture list — precisely so the capture is impossible to miss. (Structs and enums are exempt: their self is a value, so implicit access stays allowed.)

Capture lists

A capture list, in square brackets before the parameters, changes how something is captured:

// Capture the value as it is now, not as it will be
let snapshot = { [count] in print(count) }

// Do not keep `self` alive — the standard fix for retain cycles
let handler = { [weak self] in self?.refresh() }

[weak self] makes self optional inside the closure, so the closure no longer prevents the object from being deallocated. You will write this constantly in UIKit and in any long-lived callback. Lesson S3-02 covers exactly when it is required.

Check yourself

Why does Swift require @escaping on a closure parameter that you store in a property?

Sort three ways

Write the code

Print the people sorted by age ascending, then by name alphabetically, then by age descending — one array per line.

Solution unlocks after 3 attempts

Build a running total

Write the code

Write makeAccumulator() which returns a closure that adds its argument to a running total and returns the new total.

Solution unlocks after 3 attempts

Take a function as a parameter

Write the code

Write transformAll(_:using:) which applies a transform to every element. Then call it twice with different closures.

Solution unlocks after 3 attempts
PredictSaved on this device. Never graded.

Before the next lesson: SwiftUI views are built almost entirely from trailing closures. Predict one problem this could cause — a place where the syntax would become hard to read or hard to type-check — and write down your guess. You will find out later whether you were right.

Checkpoint

You can now:

  • Read closure syntax at every level of shorthand, and expand it back when confused
  • Explain capture, and predict what a captured variable holds
  • Say what @escaping and [weak self] are for
  • See SwiftUI's { … } blocks as ordinary trailing closures

Next up: strings — which in Swift are not arrays of characters, and the reasons that turns out to be correct.

How well do you know this now? Rating yourself honestly, then being tested on it, is how you find out where your intuition is wrong.