Skip to content
intermediate Phase 1 · Swift Foundations

Closures

Understand closure syntax, capturing values, escaping vs non-escaping, and trailing closures.

45m
3 problems
Topic Progress 0%

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 let bindings)
  • 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?

Question 1 options

2. What is the default closure parameter behavior in Swift?

Question 2 options

3. What does `[weak self]` do in a capture list?

Question 3 options

4. When must you use `@escaping`?

Question 4 options

5. What is `@autoclosure`?

Question 5 options

Flashcards

Question

What are shorthand argument names ($0, $1)?

Answer

Shorthand names for closure arguments: $0 is the first arg, $1 is the second, etc.

Question

What is trailing closure syntax?

Answer

Placing the closure after the function call's parentheses when it's the last argument: `foo { $0 }`

Question

What is a capture list?

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]`?

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?

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.