Swift Memory Management and ARC

Understand how Automatic Reference Counting frees class instances, spot strong reference cycles, and break them with weak, unowned and [weak self] in closures.

Lesson 19 of 43Advanced17 min read

Every class instance your app creates uses memory. When the instance is no longer needed, that memory must be given back, or the app slowly grows until the system shuts it down. Swift handles this with Automatic Reference Counting (ARC): it counts how many references point at each class instance and frees the instance the moment that count drops to zero.

Most of the time ARC works silently and you never think about it. But there's one classic problem it can't solve on its own: a strong reference cycle, where two objects hold on to each other (or an object holds a closure that holds the object). Neither count ever reaches zero, so the memory is never freed. This is called a memory leak.

Understanding ARC is what separates beginner iOS code from professional code. In this lesson you'll watch ARC release objects with deinit, create a reference cycle on purpose, and break it with weak and unowned references and closure capture lists like [weak self].

How ARC counts references

ARC only applies to classes (reference types). Structs and enums are copied, so they don't need counting.

Swift
class Session {
    let user: String
    init(user: String) { self.user = user; print("\(user) logged in") }
    deinit { print("\(user) logged out") }
}

var a: Session? = Session(user: "ada")   // count 1
var b = a                                // count 2
a = nil                                  // count 1
b = nil                                  // count 0: deinit runs
  • Every normal variable, constant or property that refers to an instance is a strong reference and adds 1 to the count.
  • deinit runs just before the instance is freed, which makes it handy for watching ARC at work.
  • Setting a reference to nil, or letting it go out of scope, removes it.

Strong reference cycles

A cycle forms when two instances point strongly at each other:

Swift
class Person { var apartment: Apartment? }
class Apartment { var tenant: Person? }

If a person refers to their apartment and the apartment refers back to the person, each keeps the other's count at 1 or more, even after every outside variable is gone. Neither deinit ever runs.

The fix is to make one side of the relationship not count:

  • weak var tenant: Person?: doesn't increase the count, and becomes nil automatically when the person is freed. It must be an optional var.
  • unowned let owner: Customer: doesn't increase the count and isn't optional. Use it only when the other object is guaranteed to live at least as long; accessing it after it's freed crashes.

Cycles with closures

Closures are reference types too, and they capture the variables they use, including self. If an object stores a closure that uses self, the object holds the closure and the closure holds the object:

Swift
class Timer {
    var onTick: (() -> Void)?
    var count = 0
    func start() {
        onTick = { [weak self] in
            self?.count += 1
        }
    }
}

The capture list [weak self] captures self weakly, breaking the cycle. Inside, self is optional; many developers write guard let self else { return } at the top of the closure. Closures that run once and aren't stored, such as map or filter, don't cause cycles and don't need [weak self].

Swift Memory Syntax

ARC syntax

class Child { weak var parent: Parent? }
class Card { unowned let owner: Customer; init(owner: Customer) { self.owner = owner } }
protocol ListDelegate: AnyObject { func didSelect(_ row: Int) }
weak var delegate: ListDelegate?
handler = { [weak self] in
    guard let self else { return }
    self.refresh()
}
deinit { print("freed") }

Swift Memory Summary

  • ARC counts strong references to class instances and frees them when the count reaches zero.
  • deinit runs just before an instance is freed.
  • Two objects that strongly reference each other form a cycle and leak.
  • weak references don't count and become nil; unowned references don't count and aren't optional.
  • Stored closures that use self need a capture list such as [weak self].

Swift Memory Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Watching ARC with deinit

Swift
class Download {
    let file: String
    init(file: String) {
        self.file = file
        print("Start \(file)")
    }
    deinit { print("Free \(file)") }
}

var first: Download? = Download(file: "a.zip")
var copy = first
first = nil
print("Still alive: \(copy?.file ?? "none")")
copy = nil

do {
    let temp = Download(file: "b.zip")
    print("Using \(temp.file)")
}
print("Done")
Output
Start a.zip
Still alive: a.zip
Free a.zip
Start b.zip
Using b.zip
Free b.zip
Done
Line-by-line explanation
  1. Line 7deinit runs when the last strong reference disappears.
  2. Lines 10 to 13Two references exist; removing one doesn't release the instance.
  3. Line 14Removing the last one frees it straight away.
  4. Lines 16 to 19temp goes out of scope at the end of the do block, so it's freed before "Done".
Example 2

Breaking a cycle with weak

Swift
class Person {
    let name: String
    var apartment: Apartment?
    init(name: String) { self.name = name }
    deinit { print("\(name) freed") }
}

class Apartment {
    let number: Int
    weak var tenant: Person?
    init(number: Int) { self.number = number }
    deinit { print("Apartment \(number) freed") }
}

var ada: Person? = Person(name: "Ada")
var flat: Apartment? = Apartment(number: 4)
ada?.apartment = flat
flat?.tenant = ada

ada = nil
print("Tenant now: \(flat?.tenant?.name ?? "nobody")")
flat = nil
Output
Ada freed
Tenant now: nobody
Apartment 4 freed
Line-by-line explanation
  1. Line 10weak means the apartment doesn't keep its tenant alive.
  2. Lines 17 to 18The two objects point at each other, but only one link is strong.
  3. Line 20Ada's only strong reference goes, so she's freed even though the apartment refers to her.
  4. Line 21The weak reference became nil automatically.
  5. Line 22Without weak, neither deinit would ever run.
Example 3

[weak self] in a stored closure

Swift
class Counter {
    var count = 0
    var onTick: (() -> Void)?

    func start() {
        onTick = { [weak self] in
            guard let self else { return }
            self.count += 1
            print("Count \(self.count)")
        }
    }
    deinit { print("Counter freed") }
}

var counter: Counter? = Counter()
counter?.start()
counter?.onTick?()
counter?.onTick?()
counter = nil
print("End")
Output
Count 1
Count 2
Counter freed
End
Line-by-line explanation
  1. Line 6The capture list stops the closure from keeping the counter alive.
  2. Line 7Inside, self is optional; guard unwraps it for the rest of the closure.
  3. Lines 17 to 18onTick?() calls the closure if it exists.
  4. Line 19With [weak self] the counter is freed; without it, "Counter freed" would never print.

Common Swift Memory Mistakes

A strong delegate

Don't
protocol PlayerDelegate { func didFinish() }
class Player { var delegate: PlayerDelegate? }     // the owner and player keep each other alive
Do
protocol PlayerDelegate: AnyObject { func didFinish() }
class Player { weak var delegate: PlayerDelegate? }

Capturing self strongly in a stored closure

Don't
viewModel.onUpdate = {
    self.tableView.reloadData()       // the controller can never be freed
}
Do
viewModel.onUpdate = { [weak self] in
    self?.tableView.reloadData()
}

unowned when the other object can disappear

Don't
class Request { unowned let screen: Screen }   // crashes if the screen closes first
Do
class Request { weak var screen: Screen? }

Swift Memory in Real Projects

Finding leaks in real apps

Leaks in iOS apps usually show up as screens whose deinit never runs after they're closed, or memory use that climbs as users move around the app. Xcode's Memory Graph Debugger shows every live object and highlights cycles, and Instruments' Leaks tool tracks them over time. A common habit is adding a temporary deinit { print("\(Self.self) freed") } to view controllers and view models to confirm they're released.

Swift Memory FAQ

What is ARC in Swift?

ARC, Automatic Reference Counting, is how Swift manages memory for class instances. It counts the strong references to each object and frees the object as soon as the count reaches zero. The compiler inserts the counting code for you, so there's no garbage collector and objects are freed at a predictable moment.

What is the difference between weak and unowned in Swift?

Both create a reference that doesn't keep the object alive. weak must be an optional var and becomes nil when the object is freed, so it's always safe. unowned isn't optional and assumes the object is still alive; using it after the object is gone crashes. Use weak unless you're certain of the lifetimes.

When should I use [weak self] in a closure?

Use it when the closure is stored by self, directly or indirectly, and uses self, such as a callback saved in a property or a long-lived timer or observer. Closures that run immediately and aren't kept, like map or filter, don't need it.

How do I find a memory leak in an iOS app?

Add a deinit that prints a message to the class you suspect and check whether it appears when the screen closes. Xcode's Memory Graph Debugger shows the objects still alive and the references holding them, and the Leaks instrument can find cycles while the app runs.

Do structs need weak references in Swift?

No. Structs and enums are value types and are copied, not reference-counted, so they can't form reference cycles on their own and can't be declared weak. Cycles only involve class instances and closures.

Swift Memory Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the property so it doesn't keep the delegate alive.

class TableView {
     var delegate: TableDelegate?
}
Predict the outputeasy

How many times does "Freed" print?

class Item { deinit { print("Freed") } }
var x: Item? = Item()
var y = x
x = nil
Multiple choicemedium

What happens to a weak reference when the object it points to is freed?

Multiple choicemedium

Which types does ARC manage?

Multiple choicehard

When do you need [weak self] in a closure?

Swift Memory Quiz

5 questions to check what you learned.

  1. What does ARC stand for?

    • Automatic Reference Counting
    • Automatic Resource Control
    • Array Reference Collection
    • Apple Runtime Cache
  2. When does deinit run?

    • When the instance is created
    • Just before the instance is freed, when its strong reference count reaches zero
    • When the app closes
    • Whenever a property changes
  3. What's the difference between weak and unowned?

    • There's none
    • unowned keeps objects alive
    • weak only works with structs
    • weak is optional and becomes nil; unowned isn't optional and crashes if used after the object is freed
  4. Why must a delegate protocol be marked AnyObject to use weak?

    • For speed
    • Only class instances can be referenced weakly, and AnyObject limits the protocol to classes
    • It's needed for Codable
    • It makes the protocol public
  5. What is a strong reference cycle?

    • Objects that strongly reference each other so neither count reaches zero and neither is freed
    • A loop that never ends
    • A recursive function
    • A closure called twice