Skip to content
AgileLogics
7 min read

Reference Cycles: weak and unowned

Prerequisites: Memory management with ARC

A strong reference cycle happens when two class instances hold strong references to each other. Each keeps the other's count above zero, so neither is ever freed — a memory leak.

The problem

final class Owner {
    let name: String
    var pet: Pet?
    init(name: String) { self.name = name }
    deinit { print("\(name) freed") }
}

final class Pet {
    let name: String
    var owner: Owner?          // strong: creates a cycle
    init(name: String) { self.name = name }
    deinit { print("\(name) freed") }
}

do {
    let ada = Owner(name: "Ada")
    let rex = Pet(name: "Rex")
    ada.pet = rex
    rex.owner = ada
}
print("End of scope")
End of scope

Neither deinit runs. The local constants are gone, but Ada still holds Rex and Rex still holds Ada.

The fix: weak

Make one side of the relationship weak. A weak reference doesn't increase the count, and it's automatically set to nil when the instance it points to is freed — so it must be an optional var:

final class Pet {
    let name: String
    weak var owner: Owner?     // weak: no cycle
    init(name: String) { self.name = name }
    deinit { print("\(name) freed") }
}

With that one change, the same code prints:

Ada freed
Rex freed
End of scope

The rule of thumb: the "parent" or "owner" holds its children strongly; a child's reference back to its parent is weak. Delegates are almost always weak for the same reason.

unowned

unowned also doesn't increase the count, but it isn't optional. Use it only when the referenced instance is guaranteed to outlive the reference — for example, a credit card that can never exist without its customer. Accessing an unowned reference after the instance is freed crashes, so weak is the safer default.

Cycles through closures

The most common cycle in real apps: an object stores a closure, and the closure uses self:

final class Downloader {
    var onComplete: (() -> Void)?
    var fileCount = 0

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

Without [weak self], the closure would hold self strongly while self holds the closure. The capture list [weak self] breaks the cycle; guard let self then unwraps it for the rest of the closure.

You don't need [weak self] for closures that run and finish without being stored — such as map or filter — because no cycle can form.

Finding leaks

Xcode's Memory Graph Debugger (the three-circle button in the debug bar) shows every live object and highlights leaks. Instruments' Leaks tool finds them during a profiling run.