Automatic Reference Counting

This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline.

This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Automatic Reference Counting (ARC) is how Swift manages the memory of class instances: it tracks, for every instance, how many strong references currently point to it, and deallocates the instance the moment that count reaches zero — structs and enums, being value types, are unaffected by ARC entirely (see Structures and Classes for the value-vs-reference distinction this depends on).

How ARC Works

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

var reference1: Person? = Person(name: "John Appleseed")   // strong reference count: 1
var reference2 = reference1                                 // count: 2
var reference3 = reference1                                 // count: 3

reference1 = nil                                             // count: 2
reference2 = nil                                              // count: 1
reference3 = nil                                              // count: 0 -- deinit runs now

Every let/var of class type is, by default, a strong reference: assigning it bumps the referenced instance’s reference count, and setting it to nil (or letting it go out of scope) decrements that count. ARC performs this bookkeeping automatically at compile time by inserting the retain/release calls — no manual retain/release calls are ever written in Swift — and frees the instance’s memory the instant its count reaches zero, which is why reference3 = nil above is what actually triggers deinit, not any of the earlier assignments.

Strong Reference Cycles Between Class Instances

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

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

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

john!.apartment = unit4A     // Apartment 4A: +1 strong reference (from john.apartment)
unit4A!.tenant = john         // Person John: +1 strong reference (from unit4A.tenant)

john = nil                    // Person's count drops to 1 (unit4A.tenant still holds it) -- deinit does NOT run
unit4A = nil                   // Apartment's count drops to 1 (john.apartment still held it) -- deinit does NOT run
// both instances leak: nothing external references either, yet neither's count ever reaches zero

When two class instances hold strong references to each other — john.apartment pointing at the apartment and unit4A.tenant pointing back at the person — setting the outside variables to nil removes only the external references. Each instance still holds a strong reference to the other, so neither count ever reaches zero and deinit never runs for either: a strong reference cycle, and a permanent memory leak for as long as the program runs.

Resolving Cycles with weak and unowned

class Person3 {
    let name: String
    var apartment: Apartment2?
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}

class Apartment2 {
    let unit: String
    weak var tenant: Person3?          // weak: the tenant may legitimately become nil (move out) independently
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) is being deinitialized") }
}

var john3: Person3? = Person3(name: "John")
var unit4B: Apartment2? = Apartment2(unit: "4B")
john3!.apartment = unit4B
unit4B!.tenant = john3               // this reference does NOT increment Person3's strong count

john3 = nil                           // Person3's count reaches 0 immediately -- deinit runs, apartment.tenant becomes nil
unit4B = nil                          // Apartment2's count reaches 0 -- deinit runs too, no leak

Declaring one side of the relationship weak (always as an Optional var, since ARC must be able to set it to nil automatically the instant the referenced instance is deallocated) breaks the cycle: that reference no longer counts toward the referenced instance’s strong count, so the instance’s lifetime is governed entirely by its other references. weak is the right choice whenever the referenced instance can legitimately reach a shorter lifetime and become nil on its own — a tenant moving out while the apartment persists.

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer     // unowned: a card can never outlive its customer, so this is never nil
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) is being deinitialized") }
}

var customer: Customer? = Customer(name: "Sam")
customer!.card = CreditCard(number: 1234_5678_9012_3456, customer: customer!)
customer = nil                          // both Customer and CreditCard deinit here -- no cycle, no leak

unowned instead declares that the referenced instance is expected to always exist for as long as the referencing one does — a credit card cannot outlive its customer, so CreditCard.customer is a non-optional, non-weak unowned let: cheaper than weak (no Optional wrapping, no runtime nil-check machinery) but unsafe if that invariant is ever violated — accessing an unowned reference after its instance has been deallocated traps at runtime, exactly like force-unwrapping a nil Optional.

class Country {
    let name: String
    var capitalCity: City!             // implicitly unwrapped: guaranteed non-nil once both inits finish
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)   // `self` is fully initialized by this point
    }
}

class City {
    let name: String
    unowned let country: Country       // unowned var also exists for cases where the property itself may change
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.capitalCity!.name) is the capital of \(country.name)")

class Department {
    let name: String
    unowned var currentManager: Employee?    // unowned optional: may be reassigned/nil'd, still no ARC bump when set
    init(name: String) { self.name = name }
}
class Employee {
    let name: String
    init(name: String) { self.name = name }
}

Two variations complete the toolkit. Two mutually-dependent classes whose properties must not be optional on either side (a Country always has a capitalCity; a City always has a country) use the implicitly unwrapped optional property pattern: one side (Country.capitalCity) is declared City! so it can start as nil only for the instant between the two initializers running, letting self be passed to City’s initializer before `Country’s own initializer has finished, while still behaving as a guaranteed non-optional everywhere else in the program. Unowned optional references (`unowned var currentManager: Employee?) cover the case where an unowned relationship’s target genuinely can be reassigned or absent over time (unlike a weak reference, the caller remains responsible for never leaving it pointing at a deallocated instance) — useful when the "no strong-count contribution" property of unowned is wanted alongside the ability to reassign or clear it.

A Person and an Apartment holding strong references to each other, forming a retain cycle that leaks both; below it, the same relationship with the Apartment’s reference to the tenant marked weak, breaking the cycle so both instances deinitialize once external references are cleared.

Strong Reference Cycles in Closures and Capture Lists

class HTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: () -> String = { [unowned self] in     // without [unowned self], self is captured strongly
        if let text = self.text {
            "<\(self.name)>\(text)</\(self.name)>"
        } else {
            "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    deinit { print("\(name) is being deinitialized") }
}

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello")
print(paragraph!.asHTML())
paragraph = nil                    // deinit runs: the closure's [unowned self] doesn't keep the instance alive

A closure stored as a property, like asHTML above, captures any instance property or method it references — including self — strongly by default, exactly like a class-to-class strong reference. Since asHTML is itself a property of HTMLElement, and its closure body captures self to read name/text, that closure and its owning instance hold strong references to each other: the same reference-cycle shape as before, just with a closure standing in for one of the two classes. The fix is the same tool covered in Closures' capture lists — [unowned self] or [weak self] inside the closure’s capture list, written before its parameter list, overrides the default strong capture for just the names listed.

weak vs. unowned Decision Guidance

weak and unowned both avoid contributing to a strong reference count, but they answer a different question about the other instance’s lifetime relative to the one holding the reference:

  • Use weak whenever the referenced instance can legitimately reach a shorter lifetime than the referring one and become absent while the referring instance is still alive — a delegate, a parent-to-child-observer back-reference, or (as above) a tenant who may move out while the apartment persists. weak is always Optional, is set to nil automatically by ARC the instant its target deallocates, and costs a small amount of extra runtime bookkeeping to make that automatic nil-ing safe.

  • Use unowned whenever the referenced instance is guaranteed to have the same or longer lifetime than the referring one — a credit card’s customer, a city’s country — so the reference is expected to always be valid whenever it is actually accessed. unowned is cheaper (no Optional, no automatic nil-out machinery) but unsafe if that invariant is ever wrong: accessing a dangling unowned reference traps.

When in doubt about which instance truly outlives the other, default to weak: a stray force-unwrap of a nil weak reference is at least an Optional-shaped bug that surfaces where the unwrap happens, whereas a wrong unowned assumption traps unpredictably at whatever later point the dangling reference happens to be touched.

Diagnosing Leaks: Xcode Memory Graph and Instruments

Two Xcode-integrated tools find reference cycles ARC itself cannot detect on its own (ARC only ever frees an instance whose count reaches zero — a cycle’s count never does, so no runtime warning fires automatically): Xcode’s Memory Graph Debugger (the icon in the debug bar, or Debug → Debug Workflow → View Memory Graph) snapshots every live object and its reference graph at a breakpoint, highlighting cycles it detects among instances that should already be deallocated; Instruments' Leaks and Allocations templates instead profile a running app over time, reporting instances whose count never reaches zero and pinpointing the retaining reference. Both are Apple-platform, Xcode-based tooling — there is no equivalent bundled with the open-source toolchain on Linux, where the usual substitute is careful deinit logging (as this page’s own examples do) or third-party memory-graph tooling.

See Also