Swift Closures Cheat Sheet
Covers Swift closure syntax, trailing closures, capture semantics, and common functional patterns like map, filter, and reduce.
Closure Syntax
Basic closure declarations and shorthand argument names.
// Basic closure syntaxlet greet: (String) -> String = { name in return "Hello, \(name)!"}print(greet("Swift")) // Hello, Swift!// Shorthand argument nameslet add: (Int, Int) -> Int = { $0 + $1 }// No parameters, no return valuelet sayHi: () -> Void = { print("Hi!")}
Trailing Closures
Passing closures as the final argument to a function.
func fetchData(completion: (Data?) -> Void) { // ... async work completion(nil)}// Trailing closure syntaxfetchData { data in print(data ?? "no data")}// Multiple trailing closures (Swift 5.3+)func animate(duration: Double, animations: () -> Void, completion: (Bool) -> Void) { animations() completion(true)}animate(duration: 0.3) { view.alpha = 0} completion: { finished in print("Done: \(finished)")}
Capture Semantics
How closures capture surrounding variables and self.
- Capture list [self]- Explicitly captures variables at closure creation time, e.g. { [self] in ... }
- [weak self]- Captures self as an Optional to avoid strong reference cycles: { [weak self] in guard let self else { return } }
- [unowned self]- Captures self without optional wrapping; crashes if self is deallocated before the closure runs
- @escaping- Marks a closure parameter that outlives the function call, e.g. stored as a property or called asynchronously
- @autoclosure- Automatically wraps an expression argument in a closure, e.g. the condition in assert(condition:)
- Value capture- Closures capture a reference to variables, not a copy — mutating a captured var inside affects the outer scope
Common Closure Patterns
Using closures with standard collection methods.
let numbers = [5, 3, 8, 1]let doubled = numbers.map { $0 * 2 } // [10, 6, 16, 2]let evens = numbers.filter { $0 % 2 == 0 } // [8]let sum = numbers.reduce(0) { $0 + $1 } // 17let sorted = numbers.sorted { $0 < $1 } // [1, 3, 5, 8]// forEachnumbers.forEach { print($0) }
Closures Are Reference Types
A closure value is backed by a heap-allocated context; assigning it copies a reference, not the captured state.
func makeCounter() -> () -> Int { var count = 0 return { count += 1 // mutates the SAME captured storage on every call return count }}let counterA = makeCounter()let counterB = counterA // copies the reference, not a fresh captureprint(counterA()) // 1print(counterB()) // 2 -- shares state with counterAlet counterC = makeCounter() // independent capture contextprint(counterC()) // 1
Capture Lists as Value Snapshots
Using a capture list to freeze a variable's value at closure-creation time instead of capturing it by reference.
var status = "pending"// Default capture is by reference -- reads the CURRENT value when calledlet reportLive = { print("Live: \(status)")}// [status] snapshots the value at closure creationlet reportSnapshot = { [status] in print("Snapshot: \(status)")}status = "complete"reportLive() // "Live: complete"reportSnapshot() // "Snapshot: pending"// Capture lists can also rename: [status = status.uppercased()]let reportUpper = { [status = status.uppercased()] in print(status)}
Storing @escaping Closures Safely
The pattern for holding onto a completion handler as a property without leaking or dangling.
final class ImageLoader { private var completions: [(UIImage?) -> Void] = [] func load(url: URL, completion: @escaping (UIImage?) -> Void) { completions.append(completion) URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in let image = data.flatMap(UIImage.init) self?.completions.forEach { $0(image) } self?.completions.removeAll() }.resume() }}// Sendable closures (Swift 6 strict concurrency) additionally require// captured state to be safe to send across isolation domains:func runInBackground(_ work: @escaping @Sendable () -> Void) { Task.detached { work() }}
Function Composition & Currying
Building pipelines of closures beyond map/filter/reduce.
infix operator >>>: AdditionPrecedence// Compose two functions into one: (A -> B) >>> (B -> C) = A -> Cfunc >>> <A, B, C>(_ f: @escaping (A) -> B, _ g: @escaping (B) -> C) -> (A) -> C { { g(f($0)) }}let trim: (String) -> String = { $0.trimmingCharacters(in: .whitespaces) }let lowercase: (String) -> String = { $0.lowercased() }let normalize = trim >>> lowercaseprint(normalize(" Hello World ")) // "hello world"// Manual currying: a function returning a functionfunc curriedAdd(_ a: Int) -> (Int) -> Int { { b in a + b }}let add5 = curriedAdd(5)print(add5(3)) // 8
Closure Memory & Concurrency Pitfalls
Failure modes that don't show up until runtime or under Swift 6 strict concurrency.
- Retain cycle via self- A class storing a closure that captures self strongly (e.g. as a stored property) creates a cycle neither side can break
- Delayed capture in loops- Appending `{ print(i) }` inside a for-loop into an array captures each loop iteration's own `i` correctly in Swift (unlike older languages), but shared mutable loop variables via var still bite
- @Sendable closures- Required for closures crossing actor/task isolation boundaries; the compiler rejects capturing non-Sendable mutable state
- withoutActuallyEscaping- Lets you pass a non-escaping closure to an API expecting @escaping when you can prove it won't outlive the call, avoiding a heap allocation
- Autoclosure evaluation order- @autoclosure arguments are lazily evaluated at the use site inside the function body, not at the call site -- easy to misjudge side-effect timing
- Closures capturing self.property- Writing `{ self.name }` still captures the whole `self` reference, not just the property -- same retain-cycle risk as capturing self directly
Prefer [weak self] over [unowned self] in closures stored as properties (like completion handlers) — unowned crashes on deallocation, while weak degrades gracefully with optional binding.