Swift Memory Management
Swift manages memory automatically using Automatic Reference Counting (ARC). ARC tracks how many parts of your code hold a reference to each class instance and frees the memory when nothing holds it anymore. Understanding ARC helps you avoid memory leaks and crashes — two of the most common bugs in iOS apps.
How ARC Works
Every class instance carries a reference count. Each time you assign the instance to a variable or constant, the count goes up by one. Each time that variable goes out of scope or is set to nil, the count drops by one. When the count reaches zero, ARC destroys the instance and frees the memory.
Diagram: Reference Counting
let a = Person("Alice") → Reference count: 1
let b = a → Reference count: 2
let c = a → Reference count: 3
c = nil → Reference count: 2
b = nil → Reference count: 1
a = nil → Reference count: 0 → Destroyed ✓
Strong References (Default)
Every reference in Swift is strong by default. A strong reference keeps the instance alive as long as it exists.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) created")
}
deinit {
print("\(name) destroyed")
}
}
var alice: Person? = Person(name: "Alice")
// Output: Alice created
var reference = alice // Two strong references now
alice = nil // Count drops to 1 — NOT destroyed yet
reference = nil // Count drops to 0 — destroyed now
// Output: Alice destroyedThe Retain Cycle Problem
A retain cycle happens when two objects hold strong references to each other. Neither reaches zero, so ARC never frees them. This is a memory leak.
class Person {
let name: String
var apartment: Apartment?
init(name: String) { self.name = name }
deinit { print("\(name) destroyed") }
}
class Apartment {
let unit: String
var tenant: Person? // ← Strong reference back to Person
init(unit: String) { self.unit = unit }
deinit { print("Apt \(unit) destroyed") }
}
var bob: Person? = Person(name: "Bob")
var apt: Apartment? = Apartment(unit: "4B")
bob!.apartment = apt // Person → Apartment (strong)
apt!.tenant = bob // Apartment → Person (strong)
bob = nil // Count still 1 (Apartment holds it) — NOT destroyed
apt = nil // Count still 1 (Person holds it) — NOT destroyed
// No "destroyed" messages — memory is leaked!Diagram: Retain Cycle
[Person: Bob] ←────── strong ──────┐
| |
└──── strong ────→ [Apartment: 4B]
Both variables set to nil, but they keep each other alive.
Neither reaches count = 0. Memory is never freed. ← LEAK
Fixing With weak References
A weak reference does not increase the reference count. It always has an optional type because the object it points to can be deallocated at any time, setting the reference to nil automatically.
class Apartment {
let unit: String
weak var tenant: Person? // ← weak: does not hold Person alive
init(unit: String) { self.unit = unit }
deinit { print("Apt \(unit) destroyed") }
}
var bob: Person? = Person(name: "Bob")
var apt: Apartment? = Apartment(unit: "4B")
bob!.apartment = apt
apt!.tenant = bob // weak — does not increase Bob's count
bob = nil // Bob's count → 0 → destroyed immediately
// Output: Bob destroyed
apt = nil // Apt's count → 0 → destroyed
// Output: Apt 4B destroyedFixing With unowned References
An unowned reference also does not increase the count, but it is non-optional. Use it only when you know the referenced object will always outlive the reference holder. Accessing an unowned reference after the object is gone crashes the app.
class Customer {
let name: String
var card: CreditCard?
init(name: String) { self.name = name }
deinit { print("\(name) destroyed") }
}
class CreditCard {
let number: String
unowned let owner: Customer // Card can't exist without a Customer
init(number: String, owner: Customer) {
self.number = number
self.owner = owner
}
deinit { print("Card \(number) destroyed") }
}
var customer: Customer? = Customer(name: "Carol")
customer!.card = CreditCard(number: "1234-5678", owner: customer!)
customer = nil
// Output:
// Carol destroyed
// Card 1234-5678 destroyedDiagram: weak vs unowned
weak var reference: SomeClass? - Optional type - Becomes nil automatically when object is deallocated - Safe — always check before use - Use when either object can be nil unowned let reference: SomeClass - Non-optional type - Does NOT become nil — crashes if accessed after dealloc - Use only when the referenced object always outlives this one
Capture Lists in Closures
Closures capture variables from their surrounding scope. A closure that captures self strongly inside a class creates a retain cycle between the closure and the class instance.
class Timer {
var count = 0
var tick: (() -> Void)?
func start() {
// [weak self] breaks the retain cycle
tick = { [weak self] in
guard let self = self else { return }
self.count += 1
print("Tick: \(self.count)")
}
}
deinit { print("Timer destroyed") }
}
var t: Timer? = Timer()
t!.start()
t!.tick?() // Output: Tick: 1
t!.tick?() // Output: Tick: 2
t = nil
// Output: Timer destroyed ← no leakWhen to Use Each Reference Type
| Reference | Count Effect | Type | Use When |
|---|---|---|---|
| strong (default) | Increases | Non-optional | You need the object to stay alive |
| weak | No change | Optional | The referenced object might become nil |
| unowned | No change | Non-optional | The referenced object always outlives this one |
Instruments: Finding Memory Leaks
Xcode's Instruments tool includes a Leaks profiler. Run your app through it and Instruments highlights objects that were allocated but never released — a clear sign of a retain cycle. Always run Leaks before shipping an app.
Summary
ARC automatically tracks and frees class instances by counting strong references. Retain cycles form when two objects hold strong references to each other, preventing deallocation. Break cycles with weak (for optional back-references) or unowned (when the referenced object is guaranteed to outlive the holder). Use [weak self] in closures inside classes to avoid capturing self strongly.
