Lesson 16 / 25

Automatic Reference Counting

Explain ARC, strong, weak and unowned references, and fix retain cycles.

How Swift frees class instances

Swift manages memory for class instances with Automatic Reference Counting (ARC). Each instance keeps a count of strong references; when the count reaches zero, the instance is deallocated immediately and its deinit runs. There is no garbage collector pausing your app, and value types (structs, enums) are not reference-counted in the same way. ARC's weakness is retain cycles: two objects holding strong references to each other (a view controller and a closure that captures it, or a parent and child that point to each other) never reach zero and leak. Break cycles with weak references, which do not increase the count and become nil automatically when the object is freed (so they are optional vars), or unowned references, which also do not increase the count but assume the object outlives the reference and crash if it does not. Typical rule: delegates are weak, closures stored by the object they capture use [weak self], and child-to-parent links are weak or unowned. Xcode's Memory Graph Debugger and Instruments' Leaks tool find cycles.

A retain cycle

Two objects holding strong references to each other are never freed; a weak link breaks the cycle.

Two circles connected by two solid arrows forming a loop, and below them the same circles with one arrow drawn dashed.
Figure 6.1 — A strong reference cycle and the weak reference that breaks it.

A delegate cycle and the weak fix

The weak delegate lets both objects be freed.

protocol DownloaderDelegate: AnyObject {          // AnyObject: class-only, so it can be weak
    func downloadFinished(_ data: Data)
}

final class Downloader {
    weak var delegate: DownloaderDelegate?        // weak: does not keep the screen alive
    var onProgress: ((Double) -> Void)?

    func start() { /* ... */ }
}

final class ProductScreen: DownloaderDelegate {
    let downloader = Downloader()
    var progressText = ""

    init() {
        downloader.delegate = self
        downloader.onProgress = { [weak self] value in   // avoid screen -> downloader -> closure -> screen
            self?.progressText = "\(Int(value * 100))%"
        }
    }

    func downloadFinished(_ data: Data) { /* update UI */ }

    deinit { print("ProductScreen freed") }        // prints when the screen goes away
}

Missing deinit output is a clue

Add a temporary deinit { print(...) } to a screen or controller and close it. If nothing prints, something still holds a strong reference, very often a closure capturing self.

Quick check: What happens to a weak reference when the object it refers to is deallocated?

  • The app crashes
  • It keeps the object alive
  • It automatically becomes nil
  • It points to a copy of the object
Answer

It automatically becomes nil — Weak references are zeroed automatically, which is why they must be optional variables.