How it works
The explanation, with examples you can run.
When counting isn't enough
Reference counting is predictable (no pauses, deinit runs at a known moment), but it has one blind spot: a strong reference cycle. If two objects hold strong references to each other, both counts stay above zero after everyone else lets go, and the pair leaks.
final class Owner { var pet: Pet? deinit { print("Owner freed") }} final class Pet { weak var owner: Owner? // weak: doesn't keep the owner alive deinit { print("Pet freed") }}Swift's two tools for breaking the cycle:
weak: doesn't add to the count, must be an optionalvar, and is set tonilautomatically when the object goes away. Use it when the other object can disappear first: delegates, parents seen from children, observers.unowned: doesn't add to the count and is not optional. Use it only when the other object always lives at least as long as this one, like a credit card pointing at its customer. Touching it after the object is gone is a runtime trap, not anil.
Closures capture
The most common leak in iOS code is a stored closure that uses self:
final class FeedViewModel { var onUpdate: (() -> Void)? private var timer: Timer? func start() { // Without [weak self], the timer keeps the closure, the closure keeps self, // and self keeps the timer: nothing is ever freed. timer = Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { [weak self] _ in guard let self else { return } self.refresh() } } func refresh() { onUpdate?() } deinit { timer?.invalidate() }}A closure that escapes (is stored, or runs later) captures every reference it mentions strongly unless you write a capture list such as [weak self] or [unowned self]. Non-escaping closures (map, filter, withAnimation) are safe, because they finish before the call returns.
Tasks in Swift concurrency capture too. A Task { await self.load() } holds self until the task ends, which is usually fine for short work but keeps an object alive for the whole length of an endless for await loop. Keep a handle to such tasks and cancel them when the owner goes away.
Finding leaks
Add a deinit with a print while developing: if dismissing a screen doesn't print, something still holds it. Xcode's Memory Graph Debugger shows every live object and the references that keep it alive, and Instruments' Leaks and Allocations templates track growth over time. SwiftUI views are structs, so cycles there usually live in the models and the closures they store.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.