A closure captures the variables it references, keeping them alive. When a closure captures self and self also holds the closure (a stored property), they keep each other alive forever — a that leaks memory. A like breaks that cycle.
A closure captures the variables it references, keeping them alive. When a closure captures self and self also holds the closure (a stored property), they keep each other alive forever — a that leaks memory. A like breaks that cycle.
[weak self]ARC frees an object when its reference count hits zero. A strong capture increments that count.
final class Downloader {
var onDone: (() -> Void)? // stored closure -> strong
func start() {
// BAD: closure captures self strongly, self holds closure -> cycle
onDone = { self.cleanup() }
// GOOD: weak capture, self can deallocate
onDone = { [weak self] in
guard let self else { return } // safely unwrap; bail if gone
self.cleanup()
}
}
func cleanup() {}
}
[weak self] — self becomes an optional; it's nil if the object was freed. Safe default when the object may outlive the closure.[unowned self] — non-optional, no ARC overhead, but crashes if self is already gone. Use only when self is guaranteed to outlive the closure.Task { [weak self] in
let data = await self?.fetch() // self? short-circuits if deallocated
}
Non-escaping closures (e.g. map, filter, forEach) run synchronously and release captures immediately — no cycle, no [weak self] needed. The risk is with escaping, stored closures: completion handlers, Timer, NotificationCenter, Combine sinks, delegate blocks.
Retain cycles are the most common memory leak in iOS, and they are invisible until Instruments shows growing memory. Knowing exactly when a closure escapes and why [weak self] is needed — versus reaching for it reflexively everywhere — separates engineers who understand ARC from those who cargo-cult it.
A library of IT interview questions with detailed answers — from Junior to Senior.
Donate