Understanding ARC: How Swift Manages Memory Automatically
In Monsters, Inc., the Scare Floor has a strict rule: every door that gets pulled out of the vault must be returned when the scare is done. If a door stays out forever, the vault runs out of space. Your app’s memory works the same way — every object you create takes up space, and if objects never get cleaned up, your app eventually runs out of memory and crashes.
Swift handles most of this cleanup automatically through a system called ARC (Automatic Reference Counting). You’ll
learn how ARC tracks objects, what happens when two objects hold onto each other in a retain cycle, and how weak
and unowned references break those cycles. We won’t cover Instruments profiling in detail — that gets
its own post.
Apple Docs: ARC Best Practices — Swift
This guide assumes you’re familiar with structs and classes and closures.
What You’ll Learn
- What Is ARC?
- How Reference Counting Works
- Retain Cycles: The Problem
- Breaking Cycles with weak and unowned
- Retain Cycles in Closures
- Common Mistakes
- What’s Next?
What Is ARC?
ARC stands for Automatic Reference Counting. It’s Swift’s memory management system for classes (reference types). Every time you create a new instance of a class, Swift allocates memory to store it. ARC’s job is to figure out when that memory is no longer needed and free it up.
Think of it like the door station in Monsters, Inc.: every time a Scarer checks out a door, the station adds a tally mark. When the Scarer returns it, the tally mark is removed. Once all tally marks are gone — nobody needs that door anymore — it goes back to the vault. ARC works the same way, but with objects instead of doors.
Note: ARC only applies to classes, not structs or enums. Structs are value types — they’re copied, not shared — so they don’t need reference counting. If this distinction is unfamiliar, review structs and classes.
How Reference Counting Works
Every class instance has a hidden reference count — a number that tracks how many variables, properties, or constants are pointing to it. When the count reaches zero, Swift deallocates the object and frees its memory.
class PixarCharacter {
let name: String
init(name: String) {
self.name = name
print("\(name) is created")
}
deinit {
print("\(name) is deallocated")
}
}
The deinit block runs automatically when an object is about to be removed from memory. It’s the opposite of init —
useful for cleanup and for understanding when ARC frees objects.
var ref1: PixarCharacter? = PixarCharacter(name: "Woody")
var ref2 = ref1 // Reference count: 2
var ref3 = ref1 // Reference count: 3
ref1 = nil // Reference count: 2
ref2 = nil // Reference count: 1
ref3 = nil // Reference count: 0 → deallocated
Woody is created
Woody is deallocated
The “Woody” object stays alive as long as at least one reference points to it. Only when all three references are set to
nil does ARC deallocate it.
Retain Cycles: The Problem
A retain cycle happens when two objects hold strong references to each other. Neither object’s reference count can ever reach zero, so neither gets deallocated — even when nothing else in your app needs them. This is a memory leak.
class Monster {
let name: String
var assistant: Monster?
init(name: String) {
self.name = name
print("\(name) hired")
}
deinit {
print("\(name) left the scare floor")
}
}
var sulley: Monster? = Monster(name: "Sulley")
var mike: Monster? = Monster(name: "Mike")
sulley?.assistant = mike // Sulley → Mike
mike?.assistant = sulley // Mike → Sulley (cycle!)
sulley = nil
mike = nil
// Neither deinit runs — memory leak!
Sulley hired
Mike hired
Notice that “left the scare floor” never prints. Even after setting both variables to nil, the two objects keep each
other alive because they each hold a strong reference to the other. This is the door that never makes it back to the
vault.
Breaking Cycles with weak and unowned
To break a retain cycle, change one of the references from strong (the default) to weak or unowned. A weak
reference doesn’t increase the reference count, so ARC can still deallocate the object.
weak References
A weak reference must be an optional var because the object it points to can be deallocated at any time, turning the
reference to nil.
class Monster {
let name: String
weak var assistant: Monster? // ← weak breaks the cycle
init(name: String) {
self.name = name
print("\(name) hired")
}
deinit {
print("\(name) left the scare floor")
}
}
var sulley: Monster? = Monster(name: "Sulley")
var mike: Monster? = Monster(name: "Mike")
sulley?.assistant = mike
mike?.assistant = sulley
sulley = nil
mike = nil
Sulley hired
Mike hired
Sulley left the scare floor
Mike left the scare floor
Both objects are properly deallocated now because the weak reference doesn’t prevent ARC from doing its job.
unowned References
An unowned reference also doesn’t increase the reference count, but unlike weak, it doesn’t become nil when the
object is deallocated. Use unowned only when you’re certain the referenced object will always outlive the current
object.
class Toy {
let name: String
var owner: Kid?
init(name: String) { self.name = name }
deinit { print("\(name) recycled") }
}
class Kid {
let name: String
unowned let favoriteToy: Toy // ← Kid can't outlive the toy
init(name: String, favoriteToy: Toy) {
self.name = name
self.favoriteToy = favoriteToy
}
deinit { print("\(name) grew up") }
}
Warning: If an
unownedreference is accessed after the object it points to has been deallocated, your app will crash. Only useunownedwhen the lifetime relationship is guaranteed.
Retain Cycles in Closures
Closures can also create retain cycles. When a closure captures self (the current
object), it creates a strong reference to it. If the object also holds a strong reference to the closure, you get a
cycle.
class MoviePlayer {
let title: String
var onComplete: (() -> Void)?
init(title: String) {
self.title = title
}
func setup() {
// ❌ This closure captures self strongly
onComplete = {
print("\(self.title) finished playing")
}
}
deinit {
print("\(title) player deallocated")
}
}
var player: MoviePlayer? = MoviePlayer(title: "Finding Nemo")
player?.setup()
player = nil // deinit never runs — retain cycle!
// Nothing prints — the player is leaked
To fix this, use a capture list with [weak self]:
func setup() {
onComplete = { [weak self] in
guard let self else { return }
print("\(self.title) finished playing")
}
}
The [weak self] capture list tells the closure to hold a weak reference instead of a strong one. The guard let self
safely unwraps it — if the object was already deallocated, the closure just returns without crashing.
Common Mistakes
Forgetting [weak self] in Closures
This is the most common source of memory leaks in iOS apps. Anytime a class stores a closure that references self, you
likely need [weak self].
// ❌ Don't do this — retain cycle
class Downloader {
var onFinish: (() -> Void)?
func start() {
onFinish = {
print(self)
}
}
}
// ✅ Do this instead — use [weak self]
class Downloader {
var onFinish: (() -> Void)?
func start() {
onFinish = { [weak self] in
guard let self else { return }
print(self)
}
}
}
Using unowned When weak Is Safer
unowned crashes if the referenced object is already gone. Unless you have a clear reason and a guaranteed lifetime
relationship, prefer weak.
// ❌ Don't do this — risky if the object is deallocated
unowned let delegate: SomeDelegate
// ✅ Do this instead — safe even if the object is gone
weak var delegate: SomeDelegate?
What’s Next?
- ARC automatically tracks how many references point to each class instance
- When the reference count hits zero, the object is deallocated
- Retain cycles happen when two objects hold strong references to each other — neither can be freed
- Use
weakto break cycles — the reference becomesnilwhen the object is deallocated - Use
unownedonly when you’re certain the referenced object will outlive the current one - Always use
[weak self]in closures that are stored as properties on the same object
Ready to see how to detect memory leaks in a running app? Head over to Instruments and Debugging to learn how to use Xcode’s memory graph debugger and the Leaks instrument.