Lesson 45Closures

Escaping Closures

Escaping vs Non-Escaping

A closure "escapes" when it's called after the function returns. Use @escaping to allow this!

Comparison

Non-Escaping (default)

Called during function execution

Example: map, filter, sorted

@escaping

Called after function returns

Example: async callbacks, stored

When to Use @escaping

  • Storing closure in a property or array
  • Async operations (network, timers)
  • Completion handlers
  • Passing to another escaping closure

Using self in Escaping Closures

Must explicitly use self in escaping closures. Use [weak self] to avoid retain cycles!

Common Pattern

func fetch(completion: @escaping (Result) -> Void) {
  // async work...
  completion(result)
}
main.swift
// ESCAPING vs NON-ESCAPING CLOSURES

// NON-ESCAPING (default)
// Closure is called BEFORE the function returns
func doMath(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)  // Called immediately
}

let sum = doMath(5, 3) { $0 + $1 }
print(sum)  // 8

// ESCAPING CLOSURES
// Closure is called AFTER the function returns
// Must be marked with @escaping

var completionHandlers: [() -> Void] = []

func addHandler(handler: @escaping () -> Void) {
    completionHandlers.append(handler)  // Stored for later
}

addHandler { print("Handler 1 called") }
addHandler { print("Handler 2 called") }

// Call them later
completionHandlers[0]()  // Handler 1 called
completionHandlers[1]()  // Handler 2 called

// ASYNC OPERATIONS (most common use)
func fetchData(completion: @escaping (String) -> Void) {
    // Simulate network delay
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        // This closure runs AFTER fetchData returns
        completion("Data loaded!")
    }
}

print("Starting fetch...")
fetchData { result in
    print(result)  // Prints after 1 second
}
print("Fetch started (but not completed)")

// OUTPUT:
// Starting fetch...
// Fetch started (but not completed)
// Data loaded!  (after 1 second)

// WITH CLASSES - must use self explicitly
class DataManager {
    var data: String = ""

    func loadData(completion: @escaping (String) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
            self.data = "Loaded"  // Must use 'self'
            completion(self.data)
        }
    }
}

// WEAK SELF TO AVOID RETAIN CYCLES
class ViewController {
    var name = "Main"

    func fetchData() {
        fetchData { [weak self] result in
            // 'self' might be nil if ViewController was deallocated
            guard let self = self else { return }
            print("\(self.name): \(result)")
        }
    }
}

// WHEN TO USE @escaping
// 1. Storing closure in a property
// 2. Async operations (network, timers)
// 3. Passing to another escaping closure
// 4. Completion handlers

Try It Yourself!

Create a function that stores a closure and calls it later. Remember to use @escaping!