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?

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 unowned reference is accessed after the object it points to has been deallocated, your app will crash. Only use unowned when 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 weak to break cycles — the reference becomes nil when the object is deallocated
  • Use unowned only 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.