- July 26, 2025
- Mins Read
When the compiler knows a closure’s parameter types and return type, you can omit the parameter list and refer to parameters by positional names: $0
, $1
, $2
, etc. This makes tiny closures extremely concise.
let numbers = [3, 1, 4, 2, 5]
// Full closure:
let sorted1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in
return a < b
})
// Remove types & return (inferred):
let sorted2 = numbers.sorted(by: { a, b in a < b })
// Use shorthand names, drop “by:” label:
let sorted3 = numbers.sorted { $0 < $1 }
When to use:
Swift lets you use existing operators as functions—great for concise closures. For example, the <
operator for Int
has the function type (Int, Int) -> Bool
. You can pass it directly to any API expecting that signature:
let nums = [5, 2, 9, 1, 7]
// Instead of writing `{ $0 < $1 }`, use the operator itself:
let ascending = nums.sorted(by: <)
print(ascending) // [1, 2, 5, 7, 9]
You can also refer to custom operators or methods:
struct Point {
var x, y: Int
static func +(lhs: Point, rhs: Point) -> Point {
Point(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
}
let p1 = Point(x: 1, y: 2), p2 = Point(x: 3, y: 4)
let sum = [p1, p2].reduce(Point(x: 0, y: 0), +)
Benefit: avoids boilerplate closures for simple operator-based transformations.
If the last parameter of a function is a closure, you can write that closure outside the parentheses. If it’s the only parameter, you can omit the parentheses entirely.
// Without trailing closure
let filtered1 = numbers.filter({ $0 % 2 == 0 })
// With trailing closure
let filtered2 = numbers.filter { $0 % 2 == 0 }
// For multiple params:
func perform(repeat count: Int, action: () -> Void) { /* ... */ }
perform(repeat: 3) {
print("Hello")
}
You can even chain multiple trailing closures (Swift 5.3+) by naming them:
func loadData(
success: (Data) -> Void,
failure: (Error) -> Void
) { /* ... */ }
loadData { data in
print("Got data:", data)
} failure: { error in
print("Error:", error)
}
Why it helps: keeps the focus on the closure body, especially for DSL-style APIs (animations, async handlers).
Closures capture constants and variables from their surrounding context. They store references to those values so they can be used later, even if the original scope has gone out of existence.
func makeCounter(startAt start: Int) -> () -> Int {
var count = start
return {
count += 1
return count
}
}
let counter = makeCounter(startAt: 10)
print(counter()) // 11
print(counter()) // 12
Here, the closure captures both count
and start
. Each call to makeCounter
produces a fresh count
variable that’s closed over.
Key points:
self
in classes; use [weak self]
or [unowned self]
to avoid retain cycles.Unlike Swift’s arrays, sets, and dictionaries (which are value types), closures are reference types. That means:
let c1 = counter
let c2 = c1
print(c2()) // 13 — both c1 and c2 share the same captured `count`
Assigning a closure to a new constant or variable doesn’t copy its capture list; both references point to the same closure instance. Any state inside the closure is shared.
By default, function parameters of function type are non-escaping: the closure must be called before the function returns. If you need to store the closure to call later (e.g., async callbacks), mark the parameter @escaping
:
var completionHandlers: [() -> Void] = []
func fetchData(completion: @escaping () -> Void) {
completionHandlers.append(completion)
}
fetchData {
print("Data loaded!")
}
// Later...
completionHandlers.forEach { $0() }
@escaping
tells the compiler that the closure may outlive the call’s stack frame.self
(self.doSomething()
) inside class instances.An autoclosure automatically wraps an expression in a closure, delaying its evaluation until – and only if – the closure is executed. Used to make syntax cleaner for APIs like assertions or custom control flow.
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
if predicate() {
print("✅ True!")
}
}
logIfTrue(2 > 1) // You pass an expression, not a closure literal
// More powerful example: “or-else” API
func or(_ lhs: Bool, _ rhs: @autoclosure () -> Bool) -> Bool {
return lhs ? true : rhs()
}
or(false, expensiveCheck()) // expensiveCheck() only runs if needed
@autoclosure @escaping
.Feature | Syntax | Use When… |
---|---|---|
Shorthand args | { $0 + $1 } |
Single-expression closures where types are known |
Operator methods | sorted(by: <) |
You need a simple operator function |
Trailing closures | funcCall { … } / multi { … } label: { … } |
Last (or only) parameter is a closure; keeps code visually streamlined |
Capturing values | Captures surrounding vars by reference | You want your closure to remember and mutate external state |
Closures as references | Assign one closure var to another; both share state | Remember that two handles mean one shared closure instance |
Escaping closures | func foo(completion: @escaping () -> Void) |
You store or call the closure after the function returns (e.g., async callbacks) |
Autoclosures | func foo(_ predicate: @autoclosure () -> Bool) |
You want to delay evaluation of a simple expression without requiring { … } around it |
Mastering these patterns will let you write concise, expressive Swift code—especially when working with asynchronous APIs, collection transforms, and DSL-style frameworks. Feel free to ask for more examples or common pitfalls!
NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use. 💻 Installation 📦 Swift Package Manager Using Swift Package Manager, add ...
An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations and programmatic navigation. NavigationStack Installation ...
With SwiftUI Router you can power your SwiftUI app with path-based routing. By utilizing a path-based system, navigation in your app becomes ...
This package takes SwiftUI's familiar and powerful NavigationStack API and gives it superpowers, allowing you to use the same API not just ...