Swift ARC (Automatic Reference Counting): Memory Management Explained
Swift uses Automatic Reference Counting, or ARC, to manage the lifetime of class instances for you. Understanding ARC helps you write code that creates objects when needed, releases them when they are no longer used, and avoids memory leaks caused by reference cycles.
Quick answer: ARC tracks how many strong references point to a class instance. When the count reaches zero, Swift deinitializes the instance automatically. You control ARC by choosing between strong, weak, and unowned references.
Difficulty: Beginner
You'll understand this better if you know: basic Swift variables, classes, and the difference between value types and reference types.
1. What Is ARC?
Automatic Reference Counting is Swift's system for tracking how many active strong references exist for each class instance. A reference is a variable, constant, property, or collection element that points to an object.
- ARC applies to class instances, not to structs or enums.
- Each strong reference increases the instance's reference count by one.
- When a strong reference goes away, the count decreases.
- When the count reaches zero, Swift deallocates the instance.
ARC is automatic, but it is not magic. You still decide which references should be strong, weak, or unowned so your object graph can be released correctly.
2. Why ARC Matters
ARC gives Swift memory management with predictable performance and without requiring you to manually free objects. That is especially important in apps with many view models, controllers, models, and long-lived services.
It matters because memory bugs often come from objects keeping each other alive too long. ARC helps prevent use-after-free problems by tying an object's lifetime to its references, but you must still avoid reference cycles.
3. Basic Syntax or Core Idea
Strong references keep objects alive
By default, assignments create strong references. As long as at least one strong reference exists, the instance stays in memory.
class Person {
let name: String
init(name: String) {
self.name = name
}
deinit {
print("\(name) was deinitialized")
}
}
var person: Person? = Person(name: "Ava")
person = nilThis example creates a Person, keeps it alive with a strong reference, and then releases it by setting the variable to nil.
ARC uses reference counts behind the scenes
You do not call retain or release directly in Swift. ARC increments and decrements counts automatically as references are created, reassigned, or go out of scope.
A useful mental model is: more strong references means the object stays alive longer; fewer strong references means the object can be destroyed sooner.
4. Step-by-Step Examples
Example 1: One strong reference
This is the simplest ARC flow. The instance exists while the variable holds it and disappears when that variable is cleared.
class Car {
let model: String
init(model: String) {
self.model = model
}
deinit {
print("Car released")
}
}
var car: Car? = Car(model: "Sedan")
car = nilOnce car becomes nil, ARC can release the instance because no strong references remain.
Example 2: Multiple strong references
Two variables can point to the same instance. The instance stays alive until both references are removed.
class Book {
let title: String
init(title: String) {
self.title = title
}
deinit {
print("Book released")
}
}
var first: Book? = Book(title: "Swift Basics")
var second = first
first = nil
second = nilSetting first to nil is not enough because second still holds the object. Only after the last strong reference disappears does ARC release it.
Example 3: A retain cycle
Two objects that strongly reference each other can keep each other alive forever. This is one of the most important ARC problems to understand.
class Customer {
var card: CreditCard?
let name: String
init(name: String) {
self.name = name
}
deinit {
print("Customer released")
}
}
class CreditCard {
let number: String
let customer: Customer
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Credit card released")
}
}This version leaks because Customer owns CreditCard and CreditCard owns Customer. Neither reference count can reach zero.
Example 4: Fixing a cycle with weak references
Use weak when a reference should not keep the object alive. This is common for back-references such as delegates and parent pointers.
class Customer {
var card: CreditCard?
let name: String
init(name: String) {
self.name = name
}
deinit {
print("Customer released")
}
}
class CreditCard {
let number: String
weak var customer: Customer?
init(number: String, customer: Customer) {
self.number = number
self.customer = customer
}
deinit {
print("Credit card released")
}
}Now the card does not keep the customer alive. When the last strong reference to the customer disappears, both objects can be released normally.
5. Practical Use Cases
- Model objects that own other model objects through strong references.
- Delegation patterns where the delegate should be weak to avoid cycles.
- Parent-child relationships in trees, where children often hold a weak reference to the parent.
- Closures that capture self and need an explicit capture list to avoid retaining the owner.
- View models, coordinators, and services that should release resources when no longer used.
6. Common Mistakes
Mistake 1: Creating a strong reference cycle between two objects
Two classes that point to each other with strong references will never deallocate, even if your app no longer needs them.
Problem: Each object keeps the other alive, so ARC cannot drop either reference count to zero.
class Owner {
var pet: Pet?
}
class Pet {
var owner: Owner?
}Fix: Make the back-reference weak when that reference should not own the object.
class Owner {
var pet: Pet?
}
class Pet {
weak var owner: Owner?
}The corrected version works because owner no longer increases the owner's reference count.
Mistake 2: Using unowned when the reference can disappear first
unowned is only safe when the referenced object is guaranteed to outlive the reference. If that assumption is wrong, your app can crash at runtime.
Problem: Accessing an unowned reference after the target has been deallocated causes a runtime crash.
class BankAccount {
var balance: Double = 0
}
class CustomerProfile {
unowned let account: BankAccount
init(account: BankAccount) {
self.account = account
}
}Fix: Use weak if the reference may become absent, or redesign ownership so the referenced object truly outlives it.
class BankAccount {
var balance: Double = 0
}
class CustomerProfile {
weak var account: BankAccount?
init(account: BankAccount) {
self.account = account
}
}The corrected version avoids crashes because the reference becomes nil instead of pointing to freed memory.
Mistake 3: Capturing self strongly inside a closure
Closures can store strong references to objects they use. If an object stores the closure and the closure stores the object, ARC creates a cycle.
Problem: The closure and the object keep each other alive, so deinit never runs.
class Downloader {
var onComplete: (() -> Void)?
func start() {
onComplete = {
print(self."Done")
}
}
}Fix: Capture self weakly when the closure is stored by the object itself or may outlive the call.
class Downloader {
var onComplete: (() -> Void)?
func start() {
onComplete = { [weak self] in
guard let self = self else { return }
print(self."Done")
}
}
}The corrected version breaks the cycle because the closure no longer owns the downloader strongly.
7. Best Practices
Practice 1: Use weak for back-references and delegates
Back-references usually should not control lifetime. Making them weak prevents accidental ownership loops in common object graphs.
protocol TaskDelegate: AnyObject {
func didFinish()
}
class Task {
weak var delegate: TaskDelegate?
}This keeps the delegate pattern safe and predictable.
Practice 2: Use unowned only with guaranteed ownership
If one object must always outlive another, unowned can express that relationship clearly and avoid optional unwrapping. Only use it when the lifetime relationship is guaranteed by design.
class Country {
let name: String
var capitalCity: City?
init(name: String) {
self.name = name
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}This is safe only because the country owns the city structure and outlives it.
Practice 3: Check closure captures when a property stores the closure
Whenever a class stores a closure property, assume there may be a cycle until you verify the capture list. Capturing self weakly is often the safest default.
class ViewModel {
var completion: (() -> Void)?
func load() {
completion = { [weak self] in
guard let self = self else { return }
print(self."Loaded")
}
}
}This avoids surprise leaks and makes the ownership relationship explicit.
8. Limitations and Edge Cases
- ARC only manages class instances. Structs and enums are value types and do not use reference counting.
- Strong cycles can also happen through collections, such as arrays or dictionaries that hold class instances in both directions.
- weak references must be optional because the referenced object can become nil at any time.
- unowned references are non-optional, but they crash if accessed after the referenced object is gone.
- Closures often create the most surprising cycles, especially when stored as properties.
- ARC does not solve all memory problems; large cached data, unmanaged resources, and long-lived singletons can still cause high memory usage.
- You cannot rely on deinit running if a cycle exists, so cleanup code placed there may never execute until the cycle is broken.
9. Practical Mini Project
In this mini project, we will build a tiny library loan system that demonstrates strong ownership, a weak back-reference, and deinitialization when objects go out of scope.
final class Library {
let name: String
init(name: String) {
self.name = name
}
deinit {
print("Library released")
}
}
final class Book {
let title: String
weak var library: Library?
init(title: String, library: Library? = nil) {
self.title = title
self.library = library
}
deinit {
print("Book released: \(title)")
}
}
do {
let library = Library(name: "City Library")
let book = Book(title: "ARC in Swift", library: library)
print(book.title, "belongs to", book.library?.name ?? "no library")
}When the do block ends, both objects are released because the book's reference to the library is weak, so there is no cycle.
10. Key Points
- ARC automatically releases class instances when their strong reference count reaches zero.
- Strong references keep objects alive, so use them for true ownership.
- Use weak for non-owning links that can become nil.
- Use unowned only when the referenced object is guaranteed to outlive the reference.
- Closures can create retain cycles just like stored properties can.
- Checking deinit is a useful way to confirm objects are being released.
11. Practice Exercise
- Create two classes, Teacher and Student.
- Make Teacher strongly own a student.
- Make the student's reference to the teacher weak so both objects can be released.
- Add deinit messages to both classes.
- Create the objects inside a local scope and verify that both are deinitialized when the scope ends.
Expected output: You should see both deinitialization messages after the local scope finishes.
Hint: Remember that the back-reference should be optional when it is weak.
Solution:
final class Teacher {
let name: String
var student: Student?
init(name: String) {
self.name = name
}
deinit {
print("Teacher released")
}
}
final class Student {
let name: String
weak var teacher: Teacher?
init(name: String, teacher: Teacher? = nil) {
self.name = name
self.teacher = teacher
}
deinit {
print("Student released")
}
}
do {
let teacher = Teacher(name: "Morgan")
let student = Student(name: "Sam")
teacher.student = student
student.teacher = teacher
}12. Final Summary
ARC is Swift's built-in memory management system for class instances. It works by tracking strong references and automatically releasing objects when nothing owns them anymore. This gives you predictable, efficient memory management without manual retain and release calls.
The key to using ARC well is understanding ownership. Strong references represent real ownership, weak references break cycles and allow values to disappear safely, and unowned references are for relationships where one object always outlives the other. If you understand those three ideas, most ARC problems become much easier to spot and fix.
As you build more Swift code, watch for retain cycles in delegates, parent-child models, and stored closures. A good next step is to learn more about strong, weak, and unowned references in detail, then practice identifying ownership in your own app's object graphs.