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 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 = nil

This 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 = nil

Once 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 = nil

Setting 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

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

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

11. Practice Exercise

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.