Swift & Objective-C

Swift ARC: Verify Cycles and weak/unowned Lifetimes

Create and break a strong cycle, observe weak references becoming nil, and distinguish an optional unowned reference from automatic zeroing.

2 min read
Cover image for Swift ARC: Verify Cycles and weak/unowned Lifetimes

weak and unowned do not own their targets, but behave differently after deallocation. weak becomes nil; accessing a deallocated target through safe unowned causes a runtime error. Choose according to lifetime guarantees, not just optional syntax.

Mechanism and design choice

ARC combines compiler-generated ownership operations with runtime reference management. Objects in a strong cycle may remain after external owners disappear. An unowned reference can be optional, but the programmer must keep it pointing to a live object or explicitly set it to nil. Primary source

Reproduce the experiment

The first experiment makes two Nodes own each other, removes external strong references, observes survival weakly, and breaks one edge. The second uses a strong parent-to-child edge and a weak reverse edge. The final example manually clears an optional unowned reference while its Owner remains alive.

This standalone example was run on 2026-10-23 with Apple Swift 6.4 targeting arm64 macOS, in Swift 6 language mode. It needs no network or additional packages.

Download the complete Swift file

Run the command in the download directory. Preconditions stop execution if a check fails. Do not use -Ounchecked: it removes these checks.

swift -swift-version 6 swift-arc-weak-unowned.swift
final class Node { var next: Node? }
func breakCycle(_ node: Node) { node.next = nil }

weak var observedA: Node?
weak var observedB: Node?
do {
    let a = Node()
    let b = Node()
    a.next = b
    b.next = a
    observedA = a
    observedB = b
}
precondition(observedA != nil && observedB != nil)
breakCycle(observedA!)
precondition(observedA == nil && observedB == nil)

final class Owner { var child: Child? }
final class Child { weak var owner: Owner? }
weak var observedOwner: Owner?
weak var observedChild: Child?
do {
    let owner = Owner()
    let child = Child()
    owner.child = child
    child.owner = owner
    observedOwner = owner
    observedChild = child
}
precondition(observedOwner == nil && observedChild == nil)

final class Borrower {
    unowned var owner: Owner?
    init(_ owner: Owner) { self.owner = owner }
}
do {
    let owner = Owner()
    let borrower = Borrower(owner)
    withExtendedLifetime(owner) {
        precondition(borrower.owner === owner)
        borrower.owner = nil
        precondition(borrower.owner == nil)
    }
}
print("cycle=retained broken=released weak=nil unowned-optional=manually-cleared")

Recorded output and interpretation

cycle=retained broken=released weak=nil unowned-optional=manually-cleared

The strong cycle survives its block but both observations become nil after one edge is broken. The weak-backed parent-child relationship is released when strong owners disappear. The optional unowned reference becomes nil because the example assigns nil, not because the runtime zeroes it.

Scope and limitations

This program does not access a dangling unowned reference and does not claim to have measured that crash. Exact deallocation at a particular source line is not a general guarantee. Optimization and lifetime extension matter; a missing deinit log alone does not prove a leak because caches or asynchronous work may legitimately retain objects.

What to check next

Draw the ownership graph and decide whether a reverse edge should own its target. Use weak when disappearance is valid; consider unowned only with a lifetime guarantee during access. Continue with closure capture when a stored closure retains its owner.

Related experiment

Sources and verification