Swift closures – Part 2
  • July 24, 2025

 


1. Shorthand Argument Names

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:

  • Sorting, mapping, filtering when the logic is just a single comparison or transformation.
  • Keeps code focused on what you’re doing, not ceremony.

2. Operator Methods

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.


3. Trailing Closures

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).


4. Capturing Values

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:

  • Captured variables are reference-like for value types, so mutations persist across calls.
  • Be careful capturing self in classes; use [weak self] or [unowned self] to avoid retain cycles.

5. Closures Are Reference Types

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.


6. Escaping Closures

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.
  • In escaping closures, you must explicitly capture self (self.doSomething()) inside class instances.
  • Non-escaping closures let the compiler optimize and infer more.

7. Autoclosures

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
  • Autoclosures are non-escaping by default.
  • To store or call later, combine @autoclosure @escaping.
  • Use sparingly: they can obscure control flow if overused.

Summary Cheat Sheet

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!

YOU MIGHT ALSO LIKE...
🧭 NavigationKit

NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use. 💻 Installation 📦 Swift Package Manager Using Swift Package Manager, add ...

swiftui-navigation-stack

An alternative SwiftUI NavigationView implementing classic stack-based navigation giving also some more control on animations and programmatic navigation. NavigationStack Installation ...

Stinsen

Simple, powerful and elegant implementation of the Coordinator pattern in SwiftUI. Stinsen is written using 100% SwiftUI which makes it ...

SwiftUI Router

With SwiftUI Router you can power your SwiftUI app with path-based routing. By utilizing a path-based system, navigation in your app becomes ...

FlowStacks

This package takes SwiftUI's familiar and powerful NavigationStack API and gives it superpowers, allowing you to use the same API not just ...