Swift Memory Management

Every object you create takes up memory. When an object is no longer needed, its memory must be freed so other parts of the app can use it. Swift handles this automatically using Automatic Reference Counting (ARC), but understanding how it works helps you avoid subtle bugs.

The Library Book Analogy


┌──────────────────────────────────────────────────┐
│  ARC = A library borrowing system                │
│                                                  │
│  Book (object) is borrowed by readers (owners)   │
│                                                  │
│  Reader 1 borrows → reference count: 1           │
│  Reader 2 borrows → reference count: 2           │
│  Reader 1 returns → reference count: 1           │
│  Reader 2 returns → reference count: 0           │
│                 → Book goes back to shelf        │
│                   (memory freed)                 │
└──────────────────────────────────────────────────┘

How ARC Works

class Laptop {
    var brand: String
    init(brand: String) {
        self.brand = brand
        print("\(brand) created")
    }
    deinit {
        print("\(brand) deallocated")
    }
}

var a: Laptop? = Laptop(brand: "Apple")   // count: 1
var b = a                                  // count: 2
a = nil                                    // count: 1
b = nil                                    // count: 0 → deallocated
// Apple deallocated

ARC increments a reference count when a new variable points to an object, and decrements it when a variable is set to nil or goes out of scope. When the count hits zero, the object is destroyed and memory is freed.

Strong References – The Default

class Person {
    var name: String
    init(name: String) { self.name = name }
}

var p1: Person? = Person(name: "Rahul")
var p2 = p1   // both strongly reference the same object

By default, every reference is a strong reference. Both p1 and p2 keep the object alive.

The Problem – Retain Cycles

class Owner {
    var name: String
    var pet: Pet?
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

class Pet {
    var name: String
    var owner: Owner?
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

var owner: Owner? = Owner(name: "Priya")
var pet: Pet?    = Pet(name: "Max")

owner?.pet   = pet
pet?.owner   = owner

owner = nil
pet   = nil
// Nothing prints — memory is LEAKED

┌──────────────────────────────────────────────────┐
│  Retain Cycle                                    │
│                                                  │
│  Owner ──(strong)──▶ Pet                        │
│    ▲                   │                         │
│    └────(strong)───────┘                         │
│                                                  │
│  Both point to each other.                       │
│  Neither count ever reaches 0.                   │
│  Memory is never freed. This is a memory leak.   │
└──────────────────────────────────────────────────┘

Weak References – Break the Cycle

class Pet {
    var name: String
    weak var owner: Owner?   // weak breaks the cycle
    init(name: String) { self.name = name }
    deinit { print("\(name) deallocated") }
}

var owner: Owner? = Owner(name: "Priya")
var pet: Pet?    = Pet(name: "Max")

owner?.pet = pet
pet?.owner = owner

owner = nil
pet   = nil
// Priya deallocated
// Max deallocated

A weak reference does not increase the reference count. When owner is set to nil, its count drops to zero, it deallocates, and the cycle is broken.

Unowned References

class CreditCard {
    let number: String
    unowned let customer: Customer
    init(number: String, customer: Customer) {
        self.number = number
        self.customer = customer
    }
}

class Customer {
    var name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
}

Use unowned when the referenced object will always outlive the one referencing it. Unlike weak, an unowned reference is not optional — accessing it after the object is gone crashes the app.

weak vs unowned – When to Use Which


┌──────────────────────────────────────────────────────┐
│  weak    → use when the reference CAN be nil         │
│            (returns Optional, safe to check)         │
│                                                      │
│  unowned → use when the reference will NEVER be nil  │
│            (non-optional, crashes if object is gone) │
│                                                      │
│  Default: prefer weak when unsure                    │
└──────────────────────────────────────────────────────┘

Capture Lists in Closures

class Timer {
    var label = "My Timer"

    lazy var display: () -> Void = { [weak self] in
        guard let self = self else { return }
        print(self.label)
    }
}

Closures capture variables strongly by default. A capture list like [weak self] prevents a retain cycle when a closure references the object that owns it. This is one of the most common memory issues in iOS apps.

Leave a Comment

Your email address will not be published. Required fields are marked *