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
nilwhen 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, useweak.
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 usesselfkeepsselfalive until the task ends. If the task never ends, for example a loop over anAsyncStream, the object never goes. Use[weak self], and make sure the task is cancelled when the object goes. - In Combine, a
sinkclosure that usesselfand is stored inself's own set of cancellables is a classic cycle. Use[weak self]there too. - The SwiftUI
.taskmodifier cancels itself when the view goes, which is another reason to prefer it.
Finding leaks
- Add
deinit { print(...) }to the classes you suspect, and navigate to the screen and back. No message means it did not free. - 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.
- 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.
- 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
unownedto 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
- Type the
OwnerandPetclasses, check that nothing is freed, then fix the cycle withweak. - Write the
Downloaderclass both ways and see that only the weak version printsDownloader freed. - Open one of your own apps, add
deinitprints to two view models, and check that they are freed when their screen closes. - Run the Debug Memory Graph on your app after opening the same screen five times.
How to check it worked
- Both
Owner freedandPet freedprint after the fix. - You can explain in one sentence the difference between
weakandunowned. - You can say when a closure needs a capture list and when it does not.