Closure Syntax
Closure Syntax
Closures are self-contained blocks of functionality that can be passed around. They are similar to functions but can capture and store references to variables from their surrounding context.
Basic Closure Syntax
{ (parameters) -> ReturnType in
// body
}
Full syntax:
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
return a + b
}
add(2, 3) // 5
Type inference:
let add: (Int, Int) -> Int = { a, b in
return a + b
}
Implicit return:
let add: (Int, Int) -> Int = { a, b in a + b }
Shorthand argument names:
let add: (Int, Int) -> Int = { $0 + $1 }
$0, $1, $2 refer to the first, second, third arguments.
Closures in Collections
Closures are heavily used with higher-order functions:
let numbers = [5, 3, 8, 1, 9]
// Full syntax
let sorted1 = numbers.sorted(by: { (a: Int, b: Int) -> Bool in return a < b })
// Type inference
let sorted2 = numbers.sorted(by: { a, b in a < b })
// Shorthand
let sorted3 = numbers.sorted(by: { $0 < $1 })
// Operator method
let sorted4 = numbers.sorted(by: <)
Closures as Variables
let greet: (String) -> String = { name in "Hello, \(name)" }
let doubled: (Int) -> Int = { $0 * 2 }
let isEven: (Int) -> Bool = { $0 % 2 == 0 }
print(greet("Alice")) // "Hello, Alice"
print(doubled(5)) // 10
print(isEven(4)) // true
Closures with No Parameters
let sayHello: () -> String = { "Hello!" }
let getRandom: () -> Int = { Int.random(in: 1...100) }
Closures with Multiple Statements
let process: (String) -> String = { input in
let trimmed = input.trimmingCharacters(in: .whitespaces)
return trimmed.uppercased()
}
Closures Returning Tuples
let getMinMax: ([Int]) -> (min: Int, max: Int)? = { array in
guard let min = array.min(), let max = array.max() else {
return nil
}
return (min, max)
}
if let result = getMinMax([3, 1, 4, 1, 5]) {
print("Min: \(result.min), Max: \(result.max)")
}
Understanding closure syntax is essential—it appears everywhere in Swift, from UI callbacks to functional programming patterns.
Capturing Values
Capturing Values
Closures can capture variables from their surrounding scope, maintaining references to those variables even after the scope has exited.
Basic Capturing
func makeCounter() -> () -> Int {
var count = 0
let counter: () -> Int = {
count += 1 // captures `count`
return count
}
return counter
}
let counter = makeCounter()
counter() // 1
counter() // 2
counter() // 3
count is captured by reference. The closure maintains a reference to the variable, so each call increments the same variable.
Capture by Reference
Closures capture variables by reference, not by value:
func makeMultiplier(_ factor: Int) -> (Int) -> Int {
var multiplier = factor
return { $0 * multiplier }
}
let triple = makeMultiplier(3)
triple(5) // 15
Mutable Captures
Captured variables can be mutated:
func makeAccumulator() -> (Int) -> Int {
var total = 0
return {
total += $0
return total
}
}
let accumulate = makeAccumulator()
accumulate(5) // 5
accumulate(3) // 8
accumulate(10) // 18
Capture Lists
To capture a value (copy) rather than a reference, use a capture list with []:
func makeCounter() -> () -> Int {
var count = 0
let counter: () -> Int = {
[count] in // captures the current value of count
count += 1 // ERROR — count is let
return count
}
return counter
}
With [count], the closure captures a copy. To make it mutable, capture a var binding:
func makeCounter() -> () -> Int {
var count = 0
let counter: () -> Int = {
[count = count] in // initial capture
var localCount = count
localCount += 1
return localCount
}
return counter
}
Weak Capture (Avoiding Retain Cycles)
When a closure captures self in a class, it can create a retain cycle. Use [weak self] to break it:
class ViewController {
var onComplete: (() -> Void)?
func setup() {
onComplete = { [weak self] in
guard let self = self else { return }
self.didComplete()
}
}
func didComplete() { print("Done!") }
}
[weak self] captures self as an optional, preventing the retain cycle.
Unowned Capture
[unowned] is like [weak] but assumes the value will never be nil. Crashes if the captured value is deallocated:
class Parent {
var child: Child?
init() { child = Child(parent: self) }
}
class Child {
unowned let parent: Parent
init(parent: Parent) { self.parent = parent }
}
Use [unowned] only when you are certain the captured value outlives the closure.
Escaping vs Non-Escaping
Escaping vs Non-Escaping
By default, closure parameters in Swift are non-escaping—the closure is guaranteed to execute before the function returns. An escaping closure can outlive the function call.
Non-Escaping (Default)
func process(_ transform: (Int) -> Int) -> Int {
return transform(5) // called synchronously
}
let result = process { $0 * 2 } // 10
Non-escaping closures:
- Cannot capture mutable variables (only
letbindings) - Can be optimized by the compiler (no heap allocation)
- Cannot be stored as properties
Escaping Closures
Mark a closure as @escaping when it might be called after the function returns:
func fetchData(completion: @escaping (Data?, Error?) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
completion(data, error) // called asynchronously
}.resume()
// function returns before completion is called
}
Escaping closures:
- Can capture mutable variables
- Must be called explicitly with
completion() - Can be stored in properties
- May create retain cycles (use
[weak self])
@autoclosure
@autoclosure automatically wraps an expression in a closure, deferring evaluation:
func logIfTrue(_ condition: Bool, _ message: @autoclosure () -> String) {
if condition {
print(message())
}
}
// message() is only called if condition is true
logIfTrue(debugMode, "Expensive computation: \(heavyOperation())")
This is useful for lazy evaluation and performance optimization.
Practical Patterns
Completion Handlers:
func save(_ data: Data, completion: @escaping (Result<Void, Error>) -> Void) {
DispatchQueue.global().async {
// save data...
completion(.success(()))
}
}
Delegation Pattern:
class SearchController {
var onResults: (([String]) -> Void)? // escaping stored property
func search(_ query: String) {
// async search...
onResults?(results) // called later
}
}
Threading:
func performOnMain(_ work: @escaping () -> Void) {
DispatchQueue.main.async(execute: work)
}
performOnMain { updateUI() }
Escaping Closure Requirements
Escaping closures must explicitly reference self:
class Manager {
var data: [Int] = []
func load() {
fetchData { [weak self] result in
self?.data = result // must use self.
}
}
}
Non-escaping closures can access self directly without self. prefix.
Understanding the distinction between escaping and non-escaping closures is critical for memory safety and avoiding retain cycles in iOS apps.
Quiz
1. What does `$0` refer to in a closure?
2. What is the default closure parameter behavior in Swift?
3. What does `[weak self]` do in a capture list?
4. When must you use `@escaping`?
5. What is `@autoclosure`?
Flashcards
Question
What are shorthand argument names ($0, $1)?
Click to reveal answer
Answer
Shorthand names for closure arguments: $0 is the first arg, $1 is the second, etc.
Question
What is trailing closure syntax?
Click to reveal answer
Answer
Placing the closure after the function call's parentheses when it's the last argument: `foo { $0 }`
Question
What is a capture list?
Click to reveal answer
Answer
The `[variable]` syntax in a closure that specifies how variables are captured (by value or weak/unowned).
Question
What is the difference between `[weak self]` and `[unowned self]`?
Click to reveal answer
Answer
[weak self] captures self as optional (can be nil). [unowned self] assumes self is never nil (crashes if deallocated).
Question
What is an escaping closure?
Click to reveal answer
Answer
A closure marked @escaping that can be called after the enclosing function returns (e.g., completion handlers).
Revision Notes
Key Takeaways
- 1. Closures are self-contained code blocks that can capture surrounding context
- 2. Shorthand names ($0, $1) make closures concise
- 3. Closures are non-escaping by default
- 4. @escaping is required when closures outlive the function
- 5. [weak self] prevents retain cycles in escaping closures
Interview Tips
- • Explain the difference between escaping and non-escaping closures
- • Know when to use [weak self] vs [unowned self]
- • Be able to write a closure with shorthand syntax
- • Understand capture semantics and retain cycles
Cheat Sheet
Closures Cheat Sheet
Syntax:
{ (params) -> RetType in body }
{ $0 + $1 } // shorthand
Trailing Closure:
numbers.map { $0 * 2 }
Capture:
{ [weak self] in self?.doWork() }
{ [unowned self] in self.doWork() }
Escaping:
@escaping— called after function returns- Used for completion handlers, async callbacks
- Must reference self explicitly
Non-Escaping (default):
- Executes before function returns
- No heap allocation
- Can access self without
self.