Closures
|
This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A closure is a self-contained block of functionality that can be passed around and used in code — like a function value from Functions, but written inline and, crucially, able to capture constants and variables from the scope where it was created.
Closure Expressions and the sorted(by:) Progression
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool { s1 > s2 }
var reversed = names.sorted(by: backward) // a named function works fine...
reversed = names.sorted(by: { (s1: String, s2: String) -> Bool in s1 > s2 }) // ...as does a full closure expression
reversed = names.sorted(by: { s1, s2 in s1 > s2 }) // inferred from context: no types, no `-> Bool`
reversed = names.sorted(by: { s1, s2 in return s1 > s2 }) // single expression: `return` may be dropped
reversed = names.sorted(by: { s1, s2 in s1 > s2 })
reversed = names.sorted(by: { $0 > $1 }) // shorthand argument names replace s1, s2
reversed = names.sorted(by: >) // an operator method IS a function value
Each rewrite above is equivalent; Swift infers a closure’s parameter and return types from context (here, from
sorted(by:)’s expected `(String, String) → Bool), which is what lets the full six-line closure expression
shrink down to > alone once every inferable piece is inferred.
Trailing Closures
func someFunctionThatTakesAClosure(closure: () -> Void) { closure() }
someFunctionThatTakesAClosure(closure: { // without trailing closure syntax
print("inside the closure")
})
someFunctionThatTakesAClosure { // with trailing closure syntax: the () disappears too
print("inside the closure")
}
reversed = names.sorted { $0 > $1 } // sorted(by:) called with a trailing closure
func loadPicture(named name: String,
completion: (Image) -> Void,
onFailure: () -> Void) { /* ... */ }
loadPicture(named: "sunset") { picture in // multiple trailing closures: first is unlabeled,
print("loaded \(picture)")
} onFailure: { // every one after is labeled at the call site
print("couldn't load the picture")
}
struct Image {}
When a function’s last parameter is a closure, the call can move that closure outside the parentheses as a
trailing closure; if it’s the function’s only argument, the parentheses can be dropped entirely (as with
someFunctionThatTakesAClosure above). A function with several trailing-closure parameters supports multiple
trailing closures: the first keeps no label, and every one after it is written with its argument label, as
onFailure: is above.
Capturing Values and Reference Semantics
func makeIncrementer(incrementAmount amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount // captures both `runningTotal` and `amount` by reference
return runningTotal
}
return incrementer
}
let incrementByTen = makeIncrementer(incrementAmount: 10)
print(incrementByTen()) // 10
print(incrementByTen()) // 20 -- runningTotal persisted between calls, even though makeIncrementer returned
let incrementBySeven = makeIncrementer(incrementAmount: 7)
print(incrementBySeven()) // 7 -- its own, independent runningTotal
print(incrementByTen()) // 30 -- unaffected by incrementBySeven's separate capture
A closure captures the variables and constants from its surrounding context by reference (not a snapshot of
their value at creation time), and Swift keeps whatever storage those captures need alive for as long as the
closure itself is alive — which is exactly how runningTotal survives makeIncrementer returning: the closure
holds the only remaining reference to it, so Swift’s memory management (see
Automatic Reference Counting) keeps it on the
heap instead of freeing it with the rest of that stack frame. Two separate calls to makeIncrementer produce two
closures with two independent captured variables, as incrementByTen and incrementBySeven show.
Escaping vs. Non-Escaping Closures
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler) // stored beyond the function call -- must be @escaping
}
func someFunctionWithNonEscapingClosure(closure: () -> Void) {
closure() // called and done before the function returns: the default
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 } // must write `self.` explicitly -- a signal it may escape
someFunctionWithNonEscapingClosure { x = 200 } // no `self.` required
}
}
A closure parameter is non-escaping by default: the compiler can guarantee it is called before the function it
was passed to returns, and can therefore optimize its storage more aggressively. Marking a parameter @escaping
tells the compiler the closure may be stored and called after the function returns (into an array, a property,
a completion handler dispatched later) — and inside such a closure, referencing a captured class instance’s
members requires writing self. explicitly, a deliberate reminder that the closure may be keeping that instance
alive well past the current scope.
Autoclosures
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.removeFirst()) // reads like an ordinary argument, not a closure literal
func serveMany(customerProvider: @autoclosure @escaping () -> String) {
DispatchQueue.main.async {
print("Now serving \(customerProvider())!") // an autoclosure can also escape, marked the same way
}
}
@autoclosure automatically wraps an ordinary-looking argument expression (customersInLine.removeFirst()) in a
closure, deferring its evaluation until the parameter is actually called inside the function body — useful for
APIs like assert(::) where the argument expression should only run (and only pay its cost) when actually
needed, while keeping the call site free of explicit { } braces.
Capture Lists
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = { [unowned self] in // `self` captured unowned: never outlives self here
if let text = self.text {
"<\(self.name)>\(text)</\(self.name)>"
} else {
"<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
}
class ImageLoader {
var onComplete: (() -> Void)?
func start() {
onComplete = { [weak self] in // `self` captured weak: may become nil, so Optional
guard let self else { return }
self.finish()
}
}
func finish() { print("done") }
}
var multiplier = 3
let capturedByValue = { [multiplier] in print(multiplier) } // [multiplier] alone captures a copy, not a reference
multiplier = 100
capturedByValue() // still prints 3
A capture list — [weak self], [unowned self], or a plain name like [multiplier] — overrides a closure’s
default by-reference capture for the names listed. weak produces an Optional reference that becomes nil if
the referenced object is deallocated (safe, requires unwrapping); unowned produces a non-optional reference
that traps if accessed after deallocation (use only when the closure’s lifetime is provably no longer than the
captured object’s); a plain name captures a copy of that value’s state at closure-creation time instead of a
live reference. Capture lists exist chiefly to avoid the strong reference cycles covered in
Automatic Reference Counting.
@Sendable Closures
func runConcurrently(_ work: @escaping @Sendable () -> Void) {
Task { work() } // safe to hand to a different isolation domain/thread
}
var counter = 0
runConcurrently {
// counter += 1 // error: a @Sendable closure cannot capture a mutable var from outside by reference
print("running")
}
A @Sendable closure is one the compiler has checked is safe to pass across concurrency-isolation boundaries: it
can only capture values that are themselves Sendable, and it cannot capture a mutable variable by reference
(only an immutable let, or a value captured by copy). Closures passed to Task { } and most concurrency APIs
are implicitly required to be @Sendable — see
Actors, Isolation and Sendable for the full
Sendable model this checking is built on.
Closures vs. Named Functions
A named function, as covered in Functions, is really a special
case of a closure: one with a name, defined at a fixed location, that captures nothing from an enclosing scope
(a global function) or captures its enclosing function’s locals (a nested function). A closure expression is
the same underlying construct written inline, anonymously, specifically so it can be created on the fly and
handed to something else — sorted(by:), a completion handler, a Task — as this page’s examples do
throughout; reach for a named function once a piece of logic is reused across call sites or benefits from a name
that documents its intent.
See Also
-
Functions — function types, and named functions as this page’s non-anonymous counterpart.
-
Automatic Reference Counting — strong reference cycles through closures, and the full
weak/unownedmodel behind capture lists. -
Actors, Isolation and Sendable —
Sendablein full, and how it governs what a closure may capture across isolation domains. -
Functional Programming — closures as the building block behind
map/filter/reduceand function composition.