Swift: Advanced Swift Closures

Closures play a central role in asynchronous programming but also introduce challenges around lifecycle management and memory leaks. This lesson goes deep into advanced closure usage so you can use them safely and efficiently in complex application scenarios.

1. What You'll Learn


2. A Real-World Mobile Developer Story

(1) Pain Point: Network Callback Crashes and Memory Leaks

Charlie is building a social app where users post comments, requiring an API call followed by a UI refresh:

SWIFT
func postComment(text: String, onComplete: () -> Void) {
    // Simulate network request
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        // Compiler error! Non-escaping closure cannot be used in async callback
        onComplete()
    }
}

The compiler complains — the closure executes after postComment returns, but onComplete is non-escaping by default. Worse, even after using an escaping closure, he encounters a memory leak: the view controller is destroyed, but the closure still holds a reference to it, causing a leak.

(2) Solution: @escaping + Capture Lists

SWIFT
// Mark as escaping closure to allow async execution
func postComment(text: String, onComplete: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
        guard let self = self else { return }
        self.showSuccess()
        onComplete()
    }
}

Three changes: add @escaping to mark it escaping, add [weak self] for a weak reference, and use guard let for safe unwrapping.

(3) Benefit: Safe Memory Management

Dimension Before After
Async callback Compiler error Runs normally
Memory leak ViewController cannot be freed weak self auto-breaks reference
Crash risk Object already deallocated on callback guard let detects and returns safely
Code intent Closure lifecycle implicit @escaping explicitly declares escaping semantics

3. Escaping Closures (@escaping)

By default, closure parameters are non-escaping — they execute before the function returns. Marking a closure with @escaping allows it to escape the function scope.

100%
sequenceDiagram
    participant Caller
    participant Func as Function
    participant Closure
    Caller->>Func: Pass in closure
    Note over Func: Non-escaping: executes inside function
    Caller->>Func: Pass @escaping closure
    Func->>Closure: Store in external variable
    Func-->>Caller: Function returns
    Note over Closure: Can be called after function returns
    Caller->>Closure: Execute in async callback
Feature Non-escaping @escaping
Execution timing Before function returns Before or after function returns
Store externally Not allowed Can store in variable/property
self. implicit reference Can be omitted Must be explicit
Compiler optimization No memory management overhead Requires ARC management
Performance Better Slight extra overhead

▶ Example: Using Escaping Closures

SWIFT
// ============================================
// Escaping vs non-escaping closures comparison
// ============================================
import Foundation
var completionHandlers: [() -> Void] = []
// Non-escaping closure — synchronous execution inside function
func syncOperation(task: () -> Void) {
    print("Starting sync task")
    task()
    print("Sync task finished")
}
syncOperation {
    print("  Running...")
}
// Escaping closure — stored in external array
func asyncOperation(task: @escaping () -> Void) {
    print("Adding async task")
    completionHandlers.append(task)  // Would error without @escaping
}
asyncOperation {
    print("  Async task executed")
}
print("Function has returned, closure not yet executed")
// Execute stored closure later
completionHandlers.first?()

Output:

TEXT 📖 Display only
Starting sync task
  Running...
Sync task finished
Adding async task
Function has returned, closure not yet executed
  Async task executed

4. Autoclosures (@autoclosure)

@autoclosure automatically wraps an expression in a closure, enabling lazy evaluation:

100%
graph TB
    A["assert(condition: 2 > 1)"] --> B["Normal evaluation: computed immediately"]
    C["assert(condition: 2 > 1, message: \"error\")"] --> D["@autoclosure: evaluated only when condition is false"]
    D --> E["Avoids string concatenation overhead"]
Scenario Normal Parameter @autoclosure
Evaluation timing Evaluated immediately at call site Evaluated only when closure is called
Performance optimization Always computed Computed on demand
Syntax Requires { } closure literal Write a normal expression

▶ Example: Lazy Evaluation with Autoclosures

SWIFT
// ============================================
// @autoclosure for deferred log output
// ============================================
var debugEnabled = false
func log(_ message: @autoclosure () -> String) {
    if debugEnabled {
        print("[DEBUG] \(message())")
    } else {
        print("Logging disabled, message skipped (not evaluated)")
    }
}
// Even expensive string concatenation is not executed when disabled
debugEnabled = false
log("expensive " + "string " + "operation " + "skipped")
debugEnabled = true
log("this " + "will " + "be " + "logged")

Output:

TEXT 📖 Display only
Logging disabled, message skipped (not evaluated)
[DEBUG] this will be logged

Common Pitfall: @autoclosure can easily hide performance issues. Only use it when deferred evaluation is truly needed — don't overuse it in public APIs. Excessive use reduces code readability.


5. Capture Lists and Retain Cycles

(1) Strong Reference Cycles

When a closure captures an external variable, the capture is a strong reference by default. When a closure and an object hold each other, a strong reference cycle (retain cycle) occurs:

100%
graph TB
    A[ViewController] -->|strong reference| B[Closure property]
    B -->|strong reference| A
    C[Mutual holding → Memory leak]
    A --> C
    B --> C

(2) Capture List Syntax

Declare a capture list at the beginning of a closure using [ ]:

Declaration Meaning Use Case
[weak self] Weak reference, self becomes optional Most common, safest
[unowned self] Unowned reference, self is not optional but must remain alive Only when certain self won't be nil
[weak delegate = self.delegate] Capture expression Capturing a specific property instead of self

▶ Example: Retain Cycles and Solutions

SWIFT
// ============================================
// Closure retain cycles: weak vs unowned
// ============================================
import Foundation
class NetworkManager {
    var onComplete: (() -> Void)?
    func fetchData() {
        // Simulate async request — escaping closure
        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
            guard let self = self else { return }
            print("Data fetched")
            self.onComplete?()
        }
    }
    deinit {
        print("NetworkManager deinit")
    }
}
class ViewController {
    let manager = NetworkManager()
    var data: String?
    func loadData() {
        // Use capture list to avoid retain cycle
        manager.onComplete = { [weak self] in
            guard let self = self else { return }
            self.data = "New data"
            print("UI updated")
        }
        manager.fetchData()
    }
    deinit {
        print("ViewController deinit")
    }
}
// Simulate usage and destruction
var vc: ViewController? = ViewController()
vc?.loadData()
// Release the view controller
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
    print("Releasing ViewController")
    vc = nil  // Will not leak
}

Output:

TEXT 📖 Display only
Releasing ViewController
NetworkManager deinit
ViewController deinit
Data fetched

Tip: The output shows both ViewController and NetworkManager are properly deallocated. [weak self] in the closure ensures no retain cycle. When the asyncAfter callback executes, self is already nil, so "UI updated" is never printed — exactly the safe behavior we want.

▶ Example: Common Pitfall with Closure Properties

SWIFT
// ============================================
// Retain cycles in closure properties
// ============================================
class Counter {
    var value = 0
    var incrementHandler: (() -> Void)?
    func setup() {
        // Correct: use weak self to break retain cycle
        incrementHandler = { [weak self] in
            self?.value += 1
        }
    }
    deinit {
        print("Counter deinit")
    }
}
var counter: Counter? = Counter()
counter?.setup()
print("Before release value = \(counter?.value ?? 0)")
counter = nil  // Properly deallocated
print("Released")

Output:

TEXT 📖 Display only
Before release value = 0
Counter deinit
Released

6. Complete Example: Safe Async Image Loader

SWIFT
// ============================================
// Complete example: Async image loader
// Features: @escaping + capture lists + memory safety
// ============================================
import Foundation
// 1. Image cache
class ImageCache {
    private var cache: [String: Data] = [:]
    func get(_ key: String) -> Data? { return cache[key] }
    func set(_ key: String, data: Data) { cache[key] = data }
    deinit { print("ImageCache deinit") }
}
// 2. Image loader (using escaping closures)
class ImageLoader {
    let cache = ImageCache()
    func loadImage(from url: String, completion: @escaping (Data?) -> Void) {
        // Check cache
        if let cached = cache.get(url) {
            completion(cached)
            return
        }
        // Simulate network request — escaping closure runs async
        DispatchQueue.global().asyncAfter(deadline: .now() + 1) { [weak self] in
            guard let self = self else {
                // self already deallocated, safely return
                completion(nil)
                return
            }
            // Simulate downloading data
            let mockData = Data([0x01, 0x02, 0x03])
            self.cache.set(url, data: mockData)
            DispatchQueue.main.async {
                completion(mockData)
            }
        }
    }
    deinit { print("ImageLoader deinit") }
}
// 3. Usage (safe release test)
var loader: ImageLoader? = ImageLoader()
loader?.loadImage(from: "https://example.com/photo.jpg") { data in
    if let _ = data {
        print("Image loaded successfully")
    }
}
// Immediately release — weak self in closure guarantees no crash
loader = nil
print("Loader released, async callback won't crash")
// Keep running to wait for async completion
RunLoop.main.run(until: Date(timeIntervalSinceNow: 2))

Output:

TEXT 📖 Display only
Loader released, async callback won't crash
ImageLoader deinit
ImageCache deinit

❓ FAQ

Q When must I use @escaping?
A When a closure is stored in an external variable (like an array or property) or used in an async callback (DispatchQueue, URLSession), you must mark it @escaping. The compiler enforces this.
Q How do I choose between weak and unowned?
A Prefer weak. weak makes the reference optional, safe but requiring unwrapping. unowned assumes the object never deallocates — if it does, your app crashes. Unless you are 100% sure self outlives the closure, don't use unowned.
Q Can @autoclosure cause retain cycles?
A Yes, it can. Because @autoclosure implicitly captures external variables. For example, calling func foo(_ closure: @autoclosure () -> Void) with self.someMethod() causes the closure to hold self. Use with caution.
Q Why don't non-escaping closures need [weak self]?
A Non-escaping closures finish executing before the function returns and won't outlive self's lifecycle. The Swift compiler knows this, allowing implicit self references. This is one of the safety advantages of non-escaping closures.
Q Must self be written explicitly inside escaping closures?
A Yes. Inside @escaping closures, all references to self must be explicit. This is a Swift-enforced syntax reminder — prompting you to consider whether [weak self] is needed. It's an important memory safety design.

📖 Summary


📝 Exercises

  1. Basic: Write a delayPrint function that takes a String and an @escaping () -> Void closure, delaying execution by 1 second using DispatchQueue.main.asyncAfter. Call it passing a string and a closure that prints that string.
  2. Intermediate: Write a Logger class with a log(_ message: @autoclosure () -> String) method that only prints the message when isEnabled is true. Demonstrate the effect of lazy evaluation.
  3. Challenge: Create a TaskManager class that internally stores a [() -> Void] array of escaping closures. Provide addTask(_:) and executeAll() methods. Use it in a ViewController, ensuring no memory leak when the ViewController is deallocated (must use a capture list).
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏