Closures
Functions without names, the shorthand syntax that shrinks them to nothing, and the capture behaviour that makes them powerful and occasionally dangerous.
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
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.
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.
VStack(spacing: 12) {
Text("Title")
Text("Subtitle")
}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.
Two things worth noticing:
countoutlivesmakeCounter. The closure holds it.firstandsecondhave separate counts. Each call tomakeCountercreates a freshcount, and each closure captures its own.
Capture is by reference, not by value: the closure sees the variable, not a snapshot of it.
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.
Why does Swift require @escaping on a closure parameter that you store in a property?
Sort three ways
Print the people sorted by age ascending, then by name alphabetically, then by age descending — one array per line.
Build a running total
Write makeAccumulator() which returns a closure that adds its argument to a running total and returns the new total.
Take a function as a parameter
Write transformAll(_:using:) which applies a transform to every element. Then call it twice with different closures.
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.
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
@escapingand[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.