Memory Management

Swift has no garbage collector. Class instances are kept alive by counting references to them: every new reference adds one, every reference that goes away subtracts one, and the instance is destroyed the moment the count reaches zero. This lesson teaches you to read that count in your head — and to notice the one pattern that breaks it.

Structs have predictable lifetimes because they live in the scope that created them. Classes do not: an instance outlives the variable that made it whenever anything else still points at it. The bookkeeping that decides this is called ARC — Automatic Reference Counting.

ARC & Reference Counting

ARC is a compile-time decision, not a runtime scan. The compiler already knows every place a reference is created, copied, or destroyed, so it inserts a retain before each new reference and a release after each one ends. Nothing sweeps memory in the background, and nothing pauses your program.

How ARC Counts

You never call retain or release yourself; you observe them through the lifetime of the object. The example below prints when an instance appears and when it disappears, so the count is visible.

// A class is the only kind of type with a reference count, so it is the only
// one that can be observed this way. `final` says "no subclass may change this".
final class Logger {
    let name: String

    init(name: String) {
        self.name = name
        // Runs when the instance is created — count goes from 0 to 1.
        print("create \(name)")
    }

    deinit {
        // Runs when the count reaches 0. Structs have no deinit at all.
        print("release \(name)")
    }
}

var a: Logger? = Logger(name: "A")   // count 1   -> create A
var b = a                            // count 2   -> a second holder
a = nil                              // count 1   -> the object is still alive
print(b!.name)                       // A         -> proof it survived
b = nil                              // count 0   -> release A

Two names referred to one object, and the object died only when the last name let go. That is the whole model: the count measures holders, not scope. Leaving a function is simply one way to stop being a holder.

A class instance starts at retain count one, gains and loses holders as references are made and dropped, and is deinitialised when the count returns to zero

Figure 1 — the count rises with each new holder and falls with each release; deinit runs exactly when the count reaches zero.

Deinit & Object Lifetime

deinit is the counterpart of init. It takes no arguments, cannot be called directly, and runs once — immediately before the memory is reclaimed. It is the right place to unregister the object somewhere else or to close a resource that ARC itself cannot close.

final class OpenFile {
    let path: String

    init(path: String) {
        self.path = path
        print("open \(path)")
    }

    func write(_ text: String) {
        print("write to \(path): \(text)")
    }

    deinit {
        // ARC frees the memory, but a file descriptor, a socket, or an observer
        // registration must be closed by hand. That is what deinit is for.
        print("close \(path)")
    }
}

do {
    let handle = OpenFile(path: "/tmp/log.txt")
    handle.write("hello")
    // `handle` goes out of scope at this closing brace: its reference ends,
    // the count reaches 0, and deinit runs.
}

// A reference that is kept elsewhere outlives its scope, which is why lifetime
// and scope are not the same thing.
var kept: OpenFile? = OpenFile(path: "/tmp/keep.txt")
print("still open")                  // this handle has NOT been closed yet
kept = nil                           // now it closes

Lifetime ends at the last release, not at the closing brace of the function that created the object. When timing matters — closing a file, stopping a timer — do not rely on scope; end the reference explicitly, or use defer for work that must happen when leaving the current scope.

What ARC Costs

Retain and release are cheap but not free, and they happen more often than beginners expect: passing a class instance to a function, storing it in a property, or putting it in an array all count as new references. Knowing where those operations land explains most performance questions about Swift class code.

Operation Effect on the count Typical cost
Assign to another variable or property +1, released when that holder is overwritten or destroyed Atomic increment; the release happens at the end of the scope
Store in an array, dictionary, or set +1 per element Counted element by element; removing an element releases it
Capture strongly in a stored closure +1 for as long as the closure lives The most common source of accidental long lifetimes
Copy a struct that holds a class reference +1 for the class inside each copy Value semantics on the outside, counted references inside

Two consequences follow. First, a struct made only of value types costs nothing extra to copy; a struct holding a class reference does not have that property. Second, because the count is shared state, the increments are atomic — the one place where a value-oriented language pays for thread safety on every reference.

Strong, Weak & Unowned References

A reference created by a plain assignment is a strong reference: it keeps the object alive. That default is what you want almost everywhere, and it is exactly what you must sometimes override, because two objects that hold each other strongly can never be released.

Strong References

Ownership is expressed through strong references. A profile owning its account, an invoice owning its lines, a view owning its model — all are strong, and all mean "while I exist, you exist".

final class Account {
    let id: String
    init(id: String) { self.id = id }
    deinit { print("account \(id) released") }
}

final class Profile {
    let account: Account          // strong by default: the profile owns it

    init(account: Account) {
        self.account = account    // count +1 — the account now has two holders
    }

    deinit { print("profile released") }
}

var account: Account? = Account(id: "A-1")            // count 1
var profile: Profile? = Profile(account: account!)    // count 2

account = nil                      // count 1 — nothing is released
print(profile!.account.id)         // A-1 — still reachable through the profile
profile = nil                      // count 0 — "profile released", then "account A-1 released"

Dropping the local name did not destroy the account, because the profile had become a second owner. Ownership is the point of a strong reference: it removes the question "is this still here?" from the caller's mind.

Weak References

A weak reference points at an object without keeping it alive. It must be declared var and optional, because ARC sets it to nil automatically the instant the object is destroyed. That auto-nil behaviour is what makes a weak reference safe in one direction of a two-way relationship.

final class Node {
    let value: Int
    var children: [Node] = []

    // The link back to the parent must not count as ownership: a strong
    // `parent` here would create a cycle. `weak` breaks it.
    weak var parent: Node?

    init(value: Int) { self.value = value }
}

let root = Node(value: 0)
let leaf = Node(value: 1)
leaf.parent = root                  // does NOT raise root's count
root.children.append(leaf)          // DOES raise leaf's count

print(leaf.parent?.value ?? -1)     // 0 — the parent is alive, so the link works
print(root.children.count)          // 1 — the child is owned by the parent

Note the asymmetry, and the reason for it: the parent owns its children (strong), while a child merely knows its parent (weak). Trees, graphs, and delegation all use exactly this split. A weak reference can never cause a cycle, and a dangling weak reference is impossible — it becomes nil rather than invalid.

Unowned References

An unowned reference also does not raise the count, but it is not optional: Swift assumes the object is still there. That assumption is safe only when the object's lifetime is a strict superset of the reference's lifetime — a credit card never outliving its customer, a child node never outliving its parent.

final class Customer {
    let name: String
    var card: CreditCard?          // strong: the card belongs to the customer

    init(name: String) { self.name = name }
    deinit { print("customer \(name) released") }
}

final class CreditCard {
    let number: Int
    unowned let owner: Customer    // unowned: a card cannot exist without an owner

    init(number: Int, owner: Customer) {
        self.number = number
        self.owner = owner         // count unchanged — a link, not ownership
    }

    var ownerName: String { owner.name }   // no unwrapping, no optional chaining
}

var ada: Customer? = Customer(name: "Ada")
ada!.card = CreditCard(number: 4111, owner: ada!)
print(ada!.card!.ownerName)        // Ada — reachable both ways, with no cycle
ada = nil                          // "customer Ada released": the cycle never existed

Both links are one-directional in ownership terms: the customer owns the card, and the card merely refers back to its owner. Reading it as a sentence is the test — "a card always has an owner, and the owner always outlives the card" holds here, so unowned is correct and the non-optional property keeps the code clean.

Choosing a Reference Kind

The three keywords are not interchangeable styles; each one encodes a claim about lifetimes. Getting the claim wrong shows up as a crash (unowned used after death) or as a leak (an unintended strong cycle), so decide deliberately.

Keyword Keeps the object alive? Type must be When it is the right choice
(default) strong Yes Anything You own the object, or you simply need it to stay alive
weak No — set to nil when the object dies Optional var The other object may outlive this one: delegates, back-references
unowned No — a trap if the object already died Non-optional Lifetime is guaranteed to be longer; you want to avoid optionals

Default to weak when you only need to avoid a cycle, and reach for unowned only when the non-optional access genuinely simplifies the code and the lifetime claim is airtight. If you are unsure, weak fails softly while unowned fails loudly.

Reference Cycles

A reference cycle is a ring of strong references with no way in from the outside. The count of every object in the ring stays above zero because each member holds the next one, so no deinit ever runs and no memory is ever returned. ARC cannot detect this for you — this is the price of avoiding a tracing collector.

A Cycle by Hand

The smallest useful cycle is two class instances pointing at each other. Read the example and count the holders of each object; the leak is visible before you run it.

final class Department {
    let name: String
    var head: Employee?                  // strong

    init(name: String) { self.name = name }
    deinit { print("department \(name) released") }
}

final class Employee {
    let name: String
    var department: Department?          // strong — this is the other half of the cycle

    init(name: String) { self.name = name }
    deinit { print("employee \(name) released") }
}

var d: Department? = Department(name: "Engineering")   // department count 1
var e: Employee? = Employee(name: "Ada")               // employee count 1

d?.head = e            // employee count 2 — the department holds the employee
e?.department = d      // department count 2 — and the employee holds the department

d = nil                // department count 1 — still alive
e = nil                // employee count 1 — still alive
// Neither deinit ever prints: the two instances hold each other forever.

The fix is a one-word change: make department in Employee a weak var department: Department?. The employee then knows its department without owning it, the ring is broken, and both deinit methods run. The rule of thumb: in a two-way relationship, exactly one side owns.

Two instances holding each other strongly keep both retain counts above zero so neither is ever deinitialised, while replacing one side with a weak reference breaks the ring

Figure 2 — two strong links form a ring that ARC cannot break; a single weak link on the back-reference turns it into a tree.

Cycles in Closures

The second classic cycle does not look like a cycle at all, because a closure does not have a visible property pointing back. A closure that captures self strongly, stored in a property of that same object, is a cycle: the object owns the closure, and the closure owns the object.

final class Ticker {
    var seconds = 0
    var tick: (() -> Void)?          // the object owns the closure

    func start() {
        // The closure body mentions `self`, so it captures `self` STRONGLY.
        // Combined with `tick` above, that is a cycle.
        tick = { self.seconds += 1 }
    }

    deinit { print("ticker released") }
}

var t: Ticker? = Ticker()
t?.start()
t = nil
// Nothing prints. The ticker is still alive because its own property holds a
// closure that holds the ticker. Use `[weak self]` to break the ring.

The tell-tale sign is a closure stored in a property of the type it captures — event handlers, completion blocks, timers, observers. A closure that is passed and used immediately (like a call to map) is not stored, so it cannot form this cycle.

Capture Lists

A capture list is written in square brackets before the parameter list and decides how each captured value enters the closure. Naming self there changes ownership from strong to weak or unowned and breaks the ring.

final class Ticker {
    var seconds = 0
    var tick: (() -> Void)?

    func start() {
        // [weak self] — self enters as an Optional. If the object died, the
        // optional chain silently skips the body, so there is nothing to crash.
        tick = { [weak self] in
            self?.seconds += 1
        }
    }
}

final class Counter {
    var value = 0
    var reported: (() -> Void)?

    func start() {
        // [unowned self] — no optional to unwrap. Correct only when the closure
        // cannot outlive the object; it traps if the object is already gone.
        reported = { [unowned self] in
            value += 1          // explicit capture also lets us use `value` directly
        }
    }
}

// A capture list can also copy a VALUE at creation time, which is a neat way to
// freeze a loop variable for use later.
var handlers: [() -> Int] = []
for i in 1...3 {
    handlers.append { [i] in i * 10 }    // each closure keeps its own `i`
}
print(handlers.map { $0() })             // [10, 20, 30]

Three uses, one syntax: [weak self] for a reference that may disappear, [unowned self] for one that cannot, and [i] to snapshot a value. When a closure is stored, reach for the capture list first; strong is what you get only by saying nothing.

Value Types & Copy-on-Write

Not all memory is managed by counting. Structs, enums, tuples, and the standard collections are value types: they are copied when assigned, and the copy is independent by construction. Understanding where each kind of value lives explains both the performance and the safety of Swift code.

Stack & Heap

A value type is stored where it is used — inline in a local variable, or inside the struct that contains it. A class instance is allocated on the heap and reached through a reference, which is why only class instances need retention counting.

Question Value type (struct, enum, tuple) Reference type (class, closure)
What does assignment do? Copies the value; the two variables are independent Copies the reference; both names see one instance
Who frees the memory? The scope that owns the value ARC, at the last release
Can it be a cycle? No — value semantics cannot form a ring Yes, through strong references or stored closures
Can it be mutated from elsewhere? No — mutation is visible only through the local copy Yes — anyone holding the reference shares the state

The practical rule this table encodes: prefer structs for data, and use classes only when shared identity, inheritance, or reference counting is genuinely required. Fewer classes means fewer cycles, fewer lifetime questions, and quieter concurrency.

Copy-on-Write

Copying on assignment sounds expensive for an array of a million elements, so Swift's collections use copy-on-write: a copy shares the buffer until one side mutates, and only then is the buffer duplicated. The result is value semantics with the performance of a reference.

// Arrays, dictionaries, sets, and strings share storage until one copy is mutated.
var a = [1, 2, 3]        // one buffer, one owner
var b = a                // two variables, still ONE buffer — nothing was copied
b.append(4)              // b is about to change, so it takes its own copy first

print(a)                 // [1, 2, 3] — untouched, as value semantics promises
print(b)                 // [1, 2, 3, 4]

// Passing an array to a function is likewise cheap: it is shared, not copied.
func total(_ numbers: [Int]) -> Int {
    numbers.reduce(0, +)             // read-only: the buffer is never duplicated
}
print(total(a))                      // 6

You can build the same behaviour for your own struct: keep the shared part in a class, and duplicate it only when the struct is no longer the sole owner of that object.

final class Storage {
    var values: [Int]
    init(values: [Int]) { self.values = values }
}

struct IntBox {
    private var box: Storage

    init(_ values: [Int]) { box = Storage(values: values) }

    mutating func append(_ value: Int) {
        // `isKnownUniquelyReferenced` is true only while this struct is the sole
        // owner of the storage object. If a copy exists, duplicate the storage
        // first so the other copy keeps its own values.
        if !isKnownUniquelyReferenced(&box) {
            box = Storage(values: box.values)     // right side is read before the assign
        }
        box.values.append(value)
    }

    var count: Int { box.values.count }
}

var first = IntBox([1, 2, 3])
var second = first                       // shares the storage for now
second.append(4)                         // second is no longer the unique owner -> copy

print(first.count)                       // 3
print(second.count)                      // 4

This is the pattern behind the standard library's containers, and it is the reason a struct containing only value types can be copied freely while a struct containing shared mutable state needs a plan. If you write isKnownUniquelyReferenced yourself, every mutating accessor must check it — one missed check silently shares state between copies.

Pitfalls & Practice

Most memory bugs are not exotic; they are one of a handful of patterns that look harmless in a diff. Learn to recognise them by shape, because the compiler will not warn you about any of them.

Common Pitfalls

// 1. Storing a closure that touches `self` without a capture list.
//    If that closure lives in a property of `self`, you built a cycle.
// controller.onDone = { self.finish() }           // leaks
controller.onDone = { [weak self] in self?.finish() }   // fixed

// 2. A delegate held strongly. Delegates are almost always back-references:
//    the object delegate is owned by someone else, so the link must be weak.
weak var delegate: RouterDelegate?

// 3. Two-way "parent" links left strong by accident. One side owns the other;
//    choose which one, and make the other weak or unowned.

// 4. Expecting deinit to run at a particular moment. It runs at the last
//    release, which may be much later than the end of the scope — or never,
//    if a cycle exists. Use `defer` when the timing must be guaranteed.

// 5. Assuming a copied struct is fully independent when it holds a class.
//    struct Wrapper { var cache: Cache }      // the copy shares the class
//    var w2 = w1                              // mutating w2.cache affects w1 too

// 6. Caching without a policy. A dictionary that only ever grows is a leak
//    even though ARC is working perfectly — use a size limit or weak keys.

Notice that only the last item is about the language. The rest are design decisions: who owns what, and for how long. Write the answer to that question down next to the property, and the leak disappears.

Practice Lab

Try these on a playground (or in the Lab Examples) — each one is short enough to run in a single file, and each one teaches a different half of the model.

  1. Count the holders. Write three classes that log creation and destruction, link them so that the middle one is owned by both others, and predict the order of the log lines before running it.
  2. Break a ring. Build the Department/Employee cycle above, prove the leak by watching for the missing deinit output, then fix it with a single weak and prove the fix.
  3. Closure ownership. Give a class a stored closure that captures self. Run it, then change the closure to [weak self] and compare what the program prints at teardown.
  4. Own your COW. Implement IntBox with isKnownUniquelyReferenced, then remove the uniqueness check and show that two boxes now share state.
  5. Measure the copy. Create an array of one hundred thousand integers, assign it to a second variable, and reason about why the assignment is instant while the first append costs time.

Reference counting is the foundation of everything that comes next. Phase 4 continues with Concurrency, where the same ownership rules reappear as the question every concurrent design must answer: which piece of state belongs to which thread.