Closures
This lesson starts with the syntax and its many abbreviations, then explains what a closure captures from its surroundings, and finishes with the escaping rules that decide when a closure outlives the call that created it.
What Is a Closure?
Every function is technically a closure. The word usually means an unnamed, inline block of code that can be stored in a variable or passed as an argument, and that remembers the context in which it was written.
Closure Syntax & Trailing Closures
A closure literal lives between braces and uses in to separate its parameter list from its body. When a closure is the final argument, Swift lets you move it outside the parentheses — the trailing closure form that makes call sites read like control flow.
// A closure stored in a variable: parameters and body inside braces
let multiply = { (a: Int, b: Int) -> Int in
return a * b
}
print(multiply(3, 4)) // prints: 12
// The same idea as a function argument
func apply(_ value: Int, using transform: (Int) -> Int) -> Int {
transform(value)
}
print(apply(5, using: { $0 * $0 })) // prints: 25
// Trailing closure form: the parentheses around the argument disappear
print(apply(5) { $0 + 100 }) // prints: 105
Shorthand Argument Names
Type inference usually already knows the parameter types, so the parameter list and in can be dropped. Then $0, $1, … refer to the arguments by position — a progression Swift code uses constantly.
let names = ["charlie", "alice", "bob"]
// Full form: explicit types and an explicit return
let sorted1 = names.sorted(by: { (a: String, b: String) -> Bool in
return a < b
})
// Types inferred, so only the names remain
let sorted2 = names.sorted { a, b in a < b }
// Shorthand: $0 and $1 are the first and second arguments
let sorted3 = names.sorted { $0 < $1 }
// An operator function is itself a closure, so this works too
let sorted4 = names.sorted(by: <)
print(sorted1, sorted2, sorted3, sorted4) // all: [alice, bob, charlie]
Capturing Values
A closure is not just a block of code — it also carries the variables it uses from the surrounding scope. That is what lets a small function remember state between calls, and it is also the source of the subtle bugs in this chapter.
Capturing by Reference
Swift captures the variable, not a copy of the value it held when the closure was written. So the closure always sees the latest value, and mutations made inside it persist after the surrounding function has returned.
func makeCounter() -> () -> Int {
var count = 0 // captured, and it outlives the call
return {
count += 1 // each call mutates the same variable
return count
}
}
let next = makeCounter()
print(next()) // prints: 1
print(next()) // prints: 2 — the state persisted
// A second counter captures its own separate variable
let other = makeCounter()
print(other()) // prints: 1 — independent state
Capture Lists
A capture list in square brackets changes the rule: it snapshots the value at the moment the closure is created, or renames it. The [weak self] and [unowned self] forms are the standard way to break reference cycles.
var message = "first"
let snapshot = { [message] in message } // copied right now
let live = { message } // still refers to the variable
message = "second"
print(snapshot()) // prints: first — frozen at creation
print(live()) // prints: second — sees the update
// A capture list can also rename what it captures
let quoted = { [text = message] in "\"\(text)\"" }
print(quoted()) // prints: "second"
// [weak self] avoids a retain cycle between object and closure
final class Ticker {
var tick = 0
func start() {
// `self?.` keeps this safe even though the reference is weak
let body = { [weak self] in self?.tick += 1 }
body()
body()
print(tick) // prints: 2
}
}
Ticker().start()
Escaping & Non-Escaping
A closure parameter is non-escaping by default: the function promises to finish with it before returning. When a closure is stored, or called after the function has already returned, that promise cannot hold — and the compiler requires you to say so.
Escaping Closures
The @escaping attribute marks a closure that may outlive its call. It is required for stored callbacks and asynchronous completion handlers, and inside such a closure you must refer to class members through an explicit self.
// The closure is stored, so it outlives this call: mark it @escaping
func store(_ action: @escaping () -> Void, in box: inout [() -> Void]) {
box.append(action)
}
var box: [() -> Void] = []
store({ print("remembered") }, in: &box)
box[0]() // prints: remembered — invoked much later
// A completion handler is the classic escaping closure
func fetchName(completion: @escaping (String) -> Void) {
// imagine asynchronous work that finishes after this call returns
completion("Ada")
}
fetchName { name in print("got \(name)") } // prints: got Ada
Non-Escaping by Default
Keeping the default is worth it: the compiler can skip a heap allocation, and no retain cycle is possible because the closure cannot survive the call. Add @escaping only when the design truly needs it.
// Non-escaping: the closure is consumed during the call
func twice(_ action: () -> Void) {
action()
action()
}
twice { print("tick") } // prints: tick, then tick
// Because it cannot escape, there is no `self.` requirement and no risk
struct Counter {
var value = 0
mutating func bump(by amount: Int, log: (Int) -> Void) {
value += amount
log(value) // guaranteed to run before this method returns
}
}
var counter = Counter()
counter.bump(by: 5) { print("now \($0)") } // prints: now 5
Closures in the Standard Library
The standard library is built on closures. Sorting, transforming, filtering, and every callback API accept a block that describes what to do, while the library hides the loop that does it.
sorted, map, filter
These higher-order functions take the decision as a closure and keep the iteration for themselves — which is why they compose so easily with each other.
let numbers = [5, 3, 8, 1]
// sorted(by:) takes a closure returning true when the first value comes first
print(numbers.sorted(by: { $0 < $1 })) // [1, 3, 5, 8]
print(numbers.sorted(by: >)) // [8, 5, 3, 1] — descending
// map transforms, filter selects, reduce folds
print(numbers.map { $0 * 10 }) // [50, 30, 80, 10]
print(numbers.filter { $0 % 2 == 1 }) // [5, 3, 1]
print(numbers.reduce(0) { $0 + $1 }) // 17
// A closure may also capture a value from the surrounding scope
let threshold = 4
print(numbers.filter { $0 > threshold }) // [5, 8]
Completion Handlers
An asynchronous API reports its result by calling a closure later. That closure must be @escaping, and when it touches an object from outside it usually needs a capture list as well.
import Foundation
// A small asynchronous-style API built on DispatchQueue
func loadValue(completion: @escaping (Int) -> Void) {
DispatchQueue.global().async {
let result = 21 * 2
DispatchQueue.main.async {
completion(result) // hand the result back on the main queue
}
}
}
// The closure runs long after loadValue returns, so it must be escaping
loadValue { value in
print("loaded \(value)") // prints: loaded 42
}
Common Pitfalls
Retain Cycles
When an object stores an escaping closure that also refers strongly to that object, the two keep each other alive and neither is ever released. A [weak self] capture list breaks the loop.
final class Model {
var value = 0
var onChange: (() -> Void)?
func observe() {
// ❌ A strong `self` here keeps the Model alive forever:
// onChange = { self.value += 1 }
// ✅ `[weak self]` breaks the cycle; `self?.` covers the nil case
onChange = { [weak self] in self?.value += 1 }
}
deinit { print("Model released") } // runs only with a broken cycle
}
var model: Model? = Model()
model?.observe()
model?.onChange?()
print(model?.value ?? -1) // prints: 1
model = nil // prints: Model released
Trailing Closure Placement
With several closure parameters the trailing slot is reserved for the last one, so the earlier closures must still be labelled. When that reads badly, pass every closure labelled — clarity beats brevity.
func combine(_ names: [String],
filter: (String) -> Bool,
transform: (String) -> String) -> [String] {
names.filter(filter).map(transform)
}
// The trailing closure binds to `transform`; `filter:` stays labelled
print(combine(["ada", "bo", "alan"], filter: { $0.count > 2 }) { $0.uppercased() })
// prints: ["ADA", "ALAN"]
// Writing both labels is often the more readable choice
print(combine(["ada", "bo"], filter: { $0.count > 0 }, transform: { $0 }))
// prints: ["ada", "bo"]
Closures give Swift its functional flavour and make callbacks natural. Next, master the type they are most often paired with in Optionals.