Skip to content

7. Memory management & ARC

What & why

Swift has no garbage collector and no manual free. Instead it uses Automatic Reference Counting (ARC): every class instance tracks how many strong references point to it, and is destroyed the moment that count hits zero. It's deterministic — deinit runs at a predictable time — but you must avoid reference cycles, which ARC can't break.

Value types (structs, enums, tuples) aren't reference counted: they live inline on the stack or inside their owner and are freed when that goes away. ARC concerns classes, closures, and actors.

How ARC counts

class Person {
    let name: String
    init(name: String) { self.name = name; print("\(name) created") }
    deinit { print("\(name) destroyed") }
}

var ref1: Person? = Person(name: "Ada")     // count = 1   → "Ada created"
var ref2 = ref1                              // count = 2
ref1 = nil                                   // count = 1
ref2 = nil                                   // count = 0   → "Ada destroyed"

The compiler inserts the retain/release calls. You see only the results.

Scope matters too:

func scoped() {
    let p = Person(name: "Grace")   // created
    print("using \(p.name)")
}                                    // p goes out of scope → destroyed
scoped()

The optimizer may release an object earlier than the end of scope, right after its last use, so don't depend on a variable keeping an object alive. If you need that, use withExtendedLifetime(p) { ... }.

Reference cycles

Two objects that hold each other strongly can never reach zero:

class Author {
    var book: Book?
    deinit { print("Author freed") }
}
class Book {
    var author: Author?
    deinit { print("Book freed") }
}

var a: Author? = Author()
var b: Book? = Book()
a?.book = b
b?.author = a
a = nil
b = nil
// Nothing printed — both objects leaked!

weak references

A weak reference does not increase the count. It's automatically set to nil when the target is destroyed, so it must be an optional var:

class Book2 {
    weak var author: Author2?       // breaks the cycle
    deinit { print("Book2 freed") }
}
class Author2 {
    var book: Book2?
    deinit { print("Author2 freed") }
}

var a2: Author2? = Author2()
var b2: Book2? = Book2()
a2?.book = b2
b2?.author = a2
a2 = nil          // Author2 freed (nothing else owns it)
print(b2?.author as Any)   // nil — weak ref was cleared
b2 = nil          // Book2 freed

Use weak when the other object may legitimately go away first (delegates, parent back-references, observers).

unowned references

unowned doesn't retain, isn't optional, and crashes if used after the target is gone. Use it only when the referenced object is guaranteed to outlive you:

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
}
class CreditCard {
    let number: String
    unowned let customer: Customer      // a card can't exist without its customer
    init(number: String, customer: Customer) { self.number = number; self.customer = customer }
}

Rule: if in doubt, choose weak. A nil is easier to handle than a crash.

Closures and capture lists

Closures that capture self and are stored on self form a cycle:

class ViewModel {
    var title = "Hello"
    var onChange: (() -> Void)?

    func bind() {
        onChange = { [weak self] in      // weak capture breaks the cycle
            guard let self else { return }
            print(self.title)
        }
    }
    deinit { print("ViewModel freed") }
}

You don't always need [weak self]:

  • Non-escaping closures (map, filter, forEach) can't outlive the call — no cycle.
  • One-shot escaping closures that don't get stored on self (a Task that finishes, DispatchQueue.main.async) only extend the lifetime until they run; a cycle needs a stored loop.
  • When you want the object kept alive until the work finishes, strong capture is correct.

Capture lists can capture more than self:

let vm = ViewModel()
let name = "Ada"
let c = { [name, weak vm] in print(name, vm?.title ?? "gone") }   // snapshot one, weak another

Diagnosing leaks

  • Add deinit { print(...) } to suspect classes. If it never prints, something still holds a strong reference.
  • Xcode's Memory Graph Debugger and Instruments → Leaks show cycles visually (on Apple platforms).
  • On any platform, isKnownUniquelyReferenced(&obj) tells you if exactly one reference exists, and CFGetRetainCount (Apple) exposes counts for experiments.
  • Heap growth in a long-running tool? Suspect closures stored in arrays/dictionaries and observer lists that never get removed.

Value types and ARC

Structs that contain class references copy the reference, not the object:

struct Wrapper {
    var person: Person        // reference inside a value type
}
var w1 = Wrapper(person: Person(name: "Linus"))
var w2 = w1                   // copies the struct; both share the same Person
w2.person = Person(name: "Ken")   // reassign w2's reference only

A struct is only as "value-like" as its stored properties. Include a class property and you inherit its reference semantics.

Copy-on-write

Array, Dictionary, String, and Set are structs that wrap a reference-counted buffer. Copying shares the buffer; the first mutation of a non-uniquely-referenced copy makes a real copy:

var original = [1, 2, 3]
var copy = original        // O(1): shares storage
copy.append(4)             // now copy gets its own storage

You can implement the same for your own types with isKnownUniquelyReferenced — rarely needed.

Unmanaged memory (a peek)

For C interop and low-level work there are UnsafePointer, UnsafeMutablePointer, and Unmanaged<T> — escape hatches that bypass ARC and safety. See the Advanced level.

Common mistakes

  • Delegate properties declared strong. Declare them weak var delegate: SomeDelegate? (and make the protocol AnyObject-constrained).
  • Parent ↔ child links both strong — make the back-reference weak.
  • Blanket [weak self] everywhere, leading to silent early exits (guard let self else { return }) when the work should have completed. Think about ownership instead.
  • unowned with uncertain lifetimes — crashes in production.
  • Capturing self in a Timer/NotificationCenter closure and never invalidating it/removing the observer.
  • Assuming deinit ordering across objects — release order is deterministic per reference but not something to build designs on.

Exercises

  1. Reproduce a retain cycle between Parent and Child classes. Prove it with deinit prints, then break it with weak.
  2. Create a Node class for a doubly-linked list; decide which of next / prev should be weak.
  3. Write a class with a stored closure property and demonstrate the cycle and fix.
  4. Use isKnownUniquelyReferenced to implement a copy-on-write Buffer struct wrapping a class.
  5. Make a Customer/CreditCard pair like above and print when each is freed.
  6. Show that a closure passed to map that captures self doesn't leak.

Recap

  • ARC retains/releases class instances automatically; deinit runs when the count hits zero.
  • Strong cycles leak. Break them with weak (optional, auto-nils) or unowned (non-optional, crashes if wrong).
  • Closures capture by reference; use capture lists ([weak self]) for stored escaping closures.
  • Structs aren't reference counted, but any class reference inside them is.
  • Verify with deinit prints and the memory-graph tools.

Next: Error handling in depth →