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
- Usage scenarios and lifecycle of
@escapingclosures - Lazy evaluation with
@autoclosure - Closure capture lists and how they resolve retain cycles
- Avoiding memory leaks with
weakandunowned - Understanding when closures capture values
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:
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
// 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.
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
// ============================================
// 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 onlyStarting 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:
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
// ============================================
// @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 onlyLogging disabled, message skipped (not evaluated) [DEBUG] this will be loggedCommon Pitfall:
@autoclosurecan 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:
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
// ============================================
// 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 onlyReleasing ViewController NetworkManager deinit ViewController deinit Data fetchedTip: The output shows both
ViewControllerandNetworkManagerare properly deallocated.[weak self]in the closure ensures no retain cycle. When theasyncAftercallback executes,selfis already nil, so "UI updated" is never printed — exactly the safe behavior we want.
▶ Example: Common Pitfall with Closure Properties
// ============================================
// 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 onlyBefore release value = 0 Counter deinit Released
6. Complete Example: Safe Async Image Loader
// ============================================
// 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 onlyLoader released, async callback won't crash ImageLoader deinit ImageCache deinit
❓ FAQ
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.func foo(_ closure: @autoclosure () -> Void) with self.someMethod() causes the closure to hold self. Use with caution.@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
@escapingmarks closures that can escape function scope, used for async callbacks and closure storage@autoclosureautomatically wraps an expression in a closure for lazy evaluation- Capture lists with
[weak self]prevent strong reference cycles between closures and objects weakmakes the reference optional and requires unwrapping;unownedassumes the object never deallocates- Non-escaping closures can safely reference self implicitly; escaping closures must reference self explicitly
- Use the
guard let self = selfpattern to safely unwrap weak references
📝 Exercises
- Basic: Write a
delayPrintfunction that takes aStringand an@escaping () -> Voidclosure, delaying execution by 1 second usingDispatchQueue.main.asyncAfter. Call it passing a string and a closure that prints that string. - Intermediate: Write a
Loggerclass with alog(_ message: @autoclosure () -> String)method that only prints the message whenisEnabledis true. Demonstrate the effect of lazy evaluation. - Challenge: Create a
TaskManagerclass that internally stores a[() -> Void]array of escaping closures. ProvideaddTask(_:)andexecuteAll()methods. Use it in aViewController, ensuring no memory leak when theViewControlleris deallocated (must use a capture list).