Swift Memory Management: ARC, Retain Cycles and weak self

Advanced iOS Development: Concurrency, Architecture, Security and Release · Advanced Swift · 6 min read · Updated

A retain cycle

  1. Two objects each hold a strong reference to the other.
  2. Their counts never reach zero, so neither is freed. The memory is leaked.
  3. Marking one link weak means it no longer adds to the count.
  4. When the last strong reference goes, both objects are freed and deinit runs.

Swift manages memory for you, but not by magic. For classes, and for closures, it uses Automatic Reference Counting, or ARC. When your code does not follow its rules, the app leaks memory: objects that are never freed, so the app grows slowly until the system closes it. Memory questions are among the most common in iOS interviews, and leaks are among the most common real bugs, so the topic is worth an hour of care.

How ARC works

Each class instance has a counter of how many *strong* references point at it. When the counter reaches zero, Swift frees the object and calls its deinit. Structs and enums are values and are copied, so they do not have this problem. That is one more reason that SwiftUI uses structs for views.

final class Screen {
    let name: String
    init(name: String) { self.name = name; print("\(name) created") }
    deinit { print("\(name) freed") }
}

var a: Screen? = Screen(name: "Home")   // count = 1
var b = a                                // count = 2
a = nil                                  // count = 1, still alive
b = nil                                  // count = 0 -> "Home freed"

Printing in deinit is the simplest leak detector you have. If a screen should be gone and you never see its message, something is still holding it.

The retain cycle

A retain cycle is two objects that hold strong references to each other, so neither count can reach zero. They keep each other alive for ever.

final class Owner {
    var pet: Pet?
    deinit { print("Owner freed") }
}
final class Pet {
    var owner: Owner?          // strong: this closes the cycle
    deinit { print("Pet freed") }
}

var o: Owner? = Owner()
var p: Pet? = Pet()
o?.pet = p
p?.owner = o
o = nil
p = nil
// nothing is printed: both objects leaked

The fix is to make one side weak. A weak reference does not add to the count, and it becomes nil automatically when the object is freed, so it is always an optional var. Change the line to weak var owner: Owner?, and both messages print.

  • `strong` (the default) keeps the object alive.
  • `weak` does not keep it alive and becomes nil when the object goes. Use it when the other object can disappear first, such as a delegate or a parent.
  • `unowned` also does not keep it alive, but it is never nil. If the object is gone, using it crashes the app. Use it only when the two always live and die together, and when in doubt, use weak.

Closures and capture lists

A closure that uses self holds a strong reference to it. If self also holds the closure, you have a cycle. The cure is a capture list, written in square brackets before the closure's inputs.

final class Downloader {
    var onFinish: (() -> Void)?
    var count = 0

    func start() {
        // BAD: onFinish holds self, and self holds onFinish
        // onFinish = { self.count += 1 }

        onFinish = { [weak self] in
            guard let self else { return }
            self.count += 1
        }
    }
    deinit { print("Downloader freed") }
}

[weak self] makes self optional inside the closure, and guard let self else { return } turns it back into a normal value for the rest of the closure. Not every closure needs this. A cycle exists only if the closure is stored somewhere that self owns. A closure passed to map or sorted is used once and thrown away, so it is safe.

Tasks and Combine

  • A Task { ... } that uses self keeps self alive until the task ends. If the task never ends, for example a loop over an AsyncStream, the object never goes. Use [weak self], and make sure the task is cancelled when the object goes.
  • In Combine, a sink closure that uses self and is stored in self's own set of cancellables is a classic cycle. Use [weak self] there too.
  • The SwiftUI .task modifier cancels itself when the view goes, which is another reason to prefer it.

Finding leaks

  1. Add deinit { print(...) } to the classes you suspect, and navigate to the screen and back. No message means it did not free.
  2. Run the app, open the screen several times, and use Xcode's Debug Memory Graph button in the debug bar. Leaked objects show a purple warning mark, and the graph shows what still points at them.
  3. For a closer look, use Instruments (Product > Profile) with the Leaks and Allocations templates. Allocations shows how memory grows over time, and Leaks flags objects that cannot be reached.
  4. Fix the cycle, then repeat the same steps and check that the message appears.

Common mistakes

  • Using [weak self] in every closure out of habit, which makes code noisy. Use it where a cycle can really form.
  • Using unowned to avoid writing the optional, and getting crashes later.
  • A delegate property declared without weak. Delegates should almost always be weak.
  • Believing that structs cannot leak. A struct that holds a reference to a class can still be part of a cycle.

Your task

  1. Type the Owner and Pet classes, check that nothing is freed, then fix the cycle with weak.
  2. Write the Downloader class both ways and see that only the weak version prints Downloader freed.
  3. Open one of your own apps, add deinit prints to two view models, and check that they are freed when their screen closes.
  4. Run the Debug Memory Graph on your app after opening the same screen five times.

How to check it worked

  • Both Owner freed and Pet freed print after the fix.
  • You can explain in one sentence the difference between weak and unowned.
  • You can say when a closure needs a capture list and when it does not.

Frequently asked questions

What is a retain cycle in Swift?

Two class instances, or an object and a closure it stores, that hold strong references to each other. Their counts never reach zero, so they are never freed, and memory leaks.

What is the difference between weak and unowned?

Both avoid adding to the reference count. weak becomes nil when the object is freed, so it is an optional. unowned is never nil and crashes if the object is gone, so use it only when both live and die together.

Do I always need [weak self] in a closure?

No. You need it only when the closure is stored by something that self owns, so a cycle can form. A closure used once, such as in map, does not need it.