Skip to content
ARC and Memory Management
step 1/7

Reading — step 1 of 7

Learn

~4 min readGenerics, Concurrency, and Memory

Swift uses Automatic Reference Counting (ARC) to manage memory for class instances. ARC is fast, deterministic — and has its own gotchas, mainly retain cycles. Apple's Swift book treats this as required reading for any class-heavy code.

What ARC does

For every class instance, Swift maintains a reference count:

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

var p1: Person? = Person(name: "Ada")    // refcount = 1
var p2: Person? = p1                       // refcount = 2
p1 = nil                                    // refcount = 1
p2 = nil                                    // refcount = 0 → deinit runs
// prints: "Ada deinitialized"

When the count hits zero, deinit runs and the memory is freed. Deterministic — no GC pause, no finalizer mystery.

Structs and enums are value types — no reference count. They're copied at boundaries; no ARC overhead.

The retain cycle problem

Two objects referencing each other create a cycle ARC can't break:

class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    let unit: String
    var tenant: Person?
    init(unit: String) { self.unit = unit }
    deinit { print("unit \(unit) deinit") }
}

var john: Person? = Person(name: "John")
var unit: Apartment? = Apartment(unit: "4A")

john?.apartment = unit
unit?.tenant = john

john = nil       // refcount of Person: 1 (apartment.tenant)
unit = nil       // refcount of Apartment: 1 (person.apartment)

// Neither deinit runs — leak!

Both objects are kept alive only by each other. ARC can't tell — it just sees the count never dropped to zero.

weak references — break cycles

class Apartment {
    let unit: String
    weak var tenant: Person?      // weak — doesn't increment refcount
    init(unit: String) { self.unit = unit }
    deinit { print("unit \(unit) deinit") }
}

Now setting tenant doesn't keep the Person alive. When the strong reference drops, Person deinits, and the weak reference becomes nil automatically.

weak requires var and Optional — the value can vanish at any time.

unowned — when nil isn't possible

class CreditCard {
    let number: String
    unowned let owner: Customer    // unowned — non-optional, non-counting
    init(number: String, owner: Customer) {
        self.number = number
        self.owner = owner
    }
}

unowned is like weak but:

  • NON-optional (you say it can never be nil)
  • If you read it after the target deallocates → CRASH (or undefined behavior in older Swift)
  • Use when the relationship guarantees one outlives the other (CreditCard < Customer)

Closures and capture

Closures hold strong references to anything they capture. Common cycle:

class Worker {
    var onComplete: (() -> Void)?
    var name: String = ""
    
    func start() {
        onComplete = {
            print("\(self.name) done")    // self captured strongly
        }
    }
    deinit { print("deinit") }
}

let w = Worker()
w.start()
// w → onComplete → captures self → w. Cycle.

Fix with capture lists:

func start() {
    onComplete = { [weak self] in
        guard let self = self else { return }
        print("\(self.name) done")
    }
}

Or [unowned self] if you can guarantee Worker outlives the closure.

When to use which

  • Strong (default) — when you OWN the relationship
  • weak — for back-references (delegate patterns, parent pointers); allows the target to disappear
  • unowned — back-references where lifetime is guaranteed (closures inside an object referencing self for the whole object's life)

The Apple guideline:

  • Tree structures: parent → child strong, child → parent weak
  • Delegates: protocol-typed properties almost always weak
  • Closures capturing self: usually [weak self] for safety

Tools for finding cycles

  • Xcode Memory Graph Debugger — visualizes object references; circles surface immediately
  • Instruments → Allocations / Leaks — runtime detection
  • Print in deinit — quickest manual check during development

Common mistakes

  • Strong delegate references — the classic cycle. Always make delegates weak.
  • Forgetting [weak self] in closures stored on self — almost always a cycle.
  • Using unowned when weak is safer — unowned crashes when the target's gone; weak just becomes nil.
  • Reading weak properties without checking nil — use guard let or Optional chaining.
  • Cycles through three or more objects — A → B → C → A. Same fix: weak/unowned somewhere in the chain.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…