Automatic Reference Counting
Swift's Automatic Reference Counting (ARC) manages your app's memory by tracking object lifetimes and relationships. In most scenarios, ARC handles memory automatically, so you rarely need to intervene. It deallocates class instances when they're no longer in use, freeing up space efficiently.
ARC applies only to class instances, which are reference types. Value types like structs and enums are copied rather than referenced.
However, certain code relationships require explicit guidance for ARC to work optimally. This guide covers those cases and how to configure ARC properly. It's akin to ARC in Objective-C but tailored for Swift.
How ARC Works
When you instantiate a class, ARC allocates memory for its type info and properties. Once the instance is unused, ARC releases that memory to prevent leaks.
Critically, ARC avoids deallocating in-use instances to prevent crashes or invalid access. It achieves this by counting strong references—links from properties, variables, or constants to the instance. As long as one strong reference exists, the instance persists.
Assigning an instance to a property or variable creates a strong reference, maintaining the instance until the reference is removed (e.g., by setting to nil).
ARC in Action
Consider a simple Student class with a name property:
class Student {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}Now define three optional variables:
var ref1: Student?
var ref2: Student?
var ref3: Student?Create and assign an instance:
ref1 = Student(name: "Alex Rivera")
// Prints "Alex Rivera is being initialized"This establishes one strong reference. Adding more:
ref2 = ref1
ref3 = ref1Now there are three. Removing two:
ref1 = nil
ref2 = nilThe instance survives due to the remaining reference. Only the final one triggers deallocation:
ref3 = nil
// Prints "Alex Rivera is being deinitialized"Strong Reference Cycles Between Class Instances
ARC excels with simple references but can fail if two instances reference each other strongly, forming a strong reference cycle. Neither deallocates, leaking memory.
Break cycles using weak or unowned references, which don't count toward the strong reference tally.
Creating a Cycle: An Example
Define Student and DormRoom classes:
class Student {
let name: String
init(name: String) { self.name = name }
var dorm: DormRoom?
deinit { print("\(name) is being deinitialized") }
}
class DormRoom {
let number: String
init(number: String) { self.number = number }
var occupant: Student?
deinit { print("Dorm \(number) is being deinitialized") }
}Set up variables:
var alex: Student?
var room101: DormRoom?Assign instances:
alex = Student(name: "Alex Rivera")
room101 = DormRoom(number: "101")Link them:
alex!.dorm = room101
room101!.occupant = alexSetting variables to nil fails to deallocate due to the cycle:
alex = nil
room101 = nilNo deinit messages print—memory leaks.
Weak References
Use weak for references where the target may outlive the source (shorter lifetime). Mark with weak; it's always optional and auto-nil on deallocation.
Adapt the example: Make DormRoom.occupant weak:
class DormRoom {
let number: String
init(number: String) { self.number = number }
weak var occupant: Student?
deinit { print("Dorm \(number) is being deinitialized") }
}Re-link as before. Now:
alex = nil
// Prints "Alex Rivera is being deinitialized"The weak reference nils out, allowing deallocation. Then:
room101 = nil
// Prints "Dorm 101 is being deinitialized"Weak suits caching but not guaranteed-lifetime scenarios.
Unowned References
Use unowned for equal or longer lifetimes; it's non-optional and assumes validity. Mark with unowned. Accessing a deallocated unowned reference crashes.
Example: Client and LoyaltyCard. Cards always tie to clients:
class Client {
let name: String
var card: LoyaltyCard?
init(name: String) { self.name = name }
deinit { print("\(name) is being deinitialized") }
}
class LoyaltyCard {
let id: UInt64
unowned let client: Client
init(id: UInt64, client: Client) {
self.id = id
self.client = client
}
deinit { print("Card #\(id) is being deinitialized") }
}Assign:
var client: Client?
client = Client(name: "Alex Rivera")
client!.card = LoyaltyCard(id: 9876_5432_1098_7654, client: client!)Set to nil:
client = nil
// Prints "Alex Rivera is being deinitialized"
// Prints "Card #9876543210987654 is being deinitialized"Unowned Optional References
Unowned can wrap optionals for flexible lifetimes. Example: Department and Class with linked courses:
class Department {
var name: String
var classes: [Class]
init(name: String) {
self.name = name
self.classes = []
}
}
class Class {
var name: String
unowned var department: Department
unowned var nextClass: Class?
init(name: String, in department: Department) {
self.name = name
self.department = department
self.nextClass = nil
}
}Usage:
let dept = Department(name: "Biology")
let basics = Class(name: "Intro to Cells", in: dept)
let advanced = Class(name: "Genetics", in: dept)
basics.nextClass = advanced
dept.classes = [basics, advanced]Ensure manual cleanup to avoid invalid access.
Unowned with Implicitly Unwrapped Optionals
For mutual non-optional dependencies post-init, pair unowned with implicitly unwrapped optionals (!).
Example: Nation and Capital:
class Nation {
let name: String
var capital: Capital!
init(name: String, capitalName: String) {
self.name = name
self.capital = Capital(name: capitalName, nation: self)
}
}
class Capital {
let name: String
unowned let nation: Nation
init(name: String, nation: Nation) {
self.name = name
self.nation = nation
}
}Usage:
var nation = Nation(name: "Canada", capitalName: "Ottawa")
print("\(nation.name)'s capital is \(nation.capital.name)")
// Prints "Canada's capital is Ottawa"This satisfies two-phase initialization while avoiding cycles.
Strong Reference Cycles for Closures
Closures can cycle with instances if they capture self strongly (e.g., via self.property).
Example: WebElement with HTML renderer:
class WebElement {
let tag: String
let content: String?
lazy var toHTML: () -> String = {
if let content = self.content {
return "<\(self.tag)>\(content)</\(self.tag)>"
} else {
return "<\(self.tag) />"
}
}
init(tag: String, content: String? = nil) {
self.tag = tag
self.content = content
}
deinit {
print("\(tag) is being deinitialized")
}
}Custom closure:
let header = WebElement(tag: "h1")
let fallback = "Default Content"
header.toHTML = {
return "<\(header.tag)>\(header.content ?? fallback)</\(header.tag)>"
}
print(header.toHTML())
// Prints "<h1>Default Content</h1>"Assign and nil:
var section: WebElement? = WebElement(tag: "section", content: "Intro Text")
print(section!.toHTML())
// Prints "<section>Intro Text</section>"
section = nil
// No deinit—cycle!The closure captures self strongly.
Resolving Closure Cycles
Use a capture list to weaken captures. Prefix with [weak self] or [unowned self] before parameters or in.
For equal lifetimes, use unowned:
class WebElement {
let tag: String
let content: String?
lazy var toHTML: () -> String = {
[unowned self] in
if let content = self.content {
return "<\(self.tag)>\(content)</\(self.tag)>"
} else {
return "<\(self.tag) />"
}
}
init(tag: String, content: String? = nil) {
self.tag = tag
self.content = content
}
deinit {
print("\(tag) is being deinitialized")
}
}Now:
var section: WebElement? = WebElement(tag: "section", content: "Intro Text")
print(section!.toHTML())
// Prints "<section>Intro Text</section>"
section = nil
// Prints "section is being deinitialized"Always qualify self in closures to flag potential captures.