Right, let’s talk about how your iPhone doesn’t grind to a halt under the weight of a million abandoned cat pictures. The answer isn’t magic, it’s reference counting, and it’s the bedrock of memory management in Swift and Objective-C. It’s a simple, brutally effective idea: every object keeps a count of how many other things are interested in it. When something new points to it, the count goes up. When something stops pointing to it, the count goes down. When that count hits zero, the object is vaporized, its memory reclaimed immediately. No waiting for a “garbage collector” to saunter by. It’s deterministic, it’s fast, and it happens in line with your code. This is Automatic Reference Counting, or ARC. It’s not a garbage collector; it’s the compiler writing the boring memory management code for you, which is infinitely better.

How ARC Actually Works: It’s Just Math

Think of every class instance as having an invisible retainCount property. When you assign an object to a strong property, a variable, or pass it into a function (which creates a temporary strong hold), the runtime says, “Ah, a new reference!” and executes retainCount += 1. When that property is set to nil or the variable goes out of scope, it executes retainCount -= 1. The init starts the count at 1. deinit is called automatically by the runtime the moment the count hits zero.

Let’s see it in action. This code is a perfect, if tragic, illustration:

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit; print("Apartment \(unit) is initialized.") }
    deinit { print("Apartment \(unit) is being deallocated.") }
}

var tenant1: Apartment?
var tenant2: Apartment?

tenant1 = Apartment(unit: "3B") // Prints: Apartment 3B is initialized. (retainCount = 1)
tenant2 = tenant1 // The same Apartment instance now has another strong reference. (retainCount = 2)

tenant1 = nil // We clear one reference. (retainCount = 1)
print("The first tenant moved out.")
// The Apartment instance is still alive because tenant2 is holding onto it.

tenant2 = nil // The last reference is cleared. (retainCount = 0)
// Prints immediately: Apartment 3B is being deallocated.

See? No mystery. The object lives only as long as something needs it, and dies the instant it becomes irrelevant. It’s a meritocracy for memory.

The Classic Nightmare: Retain Cycles

Here’s where the simple math breaks down, and it’s the single biggest reason developers curse at their screens. A retain cycle is what happens when two objects love each other very, very much—or more accurately, when they hold strong references to each other. Their retain counts can never reach zero because they’re keeping each other on life support, permanently. It’s a memory leak; a ghost in your machine.

This is the textbook example. Don’t do this.

class Person {
    let name: String
    var apartment: Apartment? // A strong reference to an Apartment
    init(name: String) { self.name = name; print("\(name) is initialized.") }
    deinit { print("\(name) is being deallocated.") }
}

class Apartment {
    let unit: String
    var tenant: Person? // A strong reference back to a Person
    init(unit: String) { self.unit = unit; print("Apartment \(unit) is initialized.") }
    deinit { print("Apartment \(unit) is being deallocated.") }
}

var john: Person?
var unit3B: Apartment?

john = Person(name: "John Appleseed")
unit3B = Apartment(unit: "3B")

john!.apartment = unit3B // John strongly references the apartment...
unit3B!.tenant = john    // ...and the apartment strongly references John.

john = nil    // We try to get rid of John...
unit3B = nil  // ...and we try to get rid of the apartment.
// Nothing is printed. Neither deinitializer was called. The memory is lost.

We just created an island of useless, unreachable memory. The objects are now Schrödinger’s cat: both alive and dead, but mostly just a waste of RAM. The solution, thankfully, isn’t to abolish love, but to use weak or unowned references.

Breaking the Cycle: Weak and Unowned References

You break a retain cycle by making one of the references non-strong. Swift gives you two main tools for this, and choosing the right one is a mark of a seasoned developer.

A weak reference is a non-owning reference. It doesn’t contribute to the retain count. The killer feature is that it automatically becomes nil when the object it points to is deallocated. This makes it safe. You must always declare weak references as optional variables (var) because they can become nil.

An unowned reference is also a non-owning reference, but with the assumption that the reference will never be nil during its lifetime. It doesn’t keep the object alive and doesn’t become nil. If you’re wrong and the object is deallocated, accessing the unowned reference will trigger a catastrophic runtime crash. It’s like an implicitly unwrapped optional for memory management.

The correct fix for our cycle is to decide the relationship. Does the apartment require a tenant to exist? No. An empty apartment is fine. So tenant should be weak.

class Apartment {
    let unit: String
    weak var tenant: Person? // The key fix: a weak reference breaks the cycle.
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) is being deallocated.") }
}

// Now, when we set john and unit3B to nil, the objects can deallocate.
// Prints:
// John Appleseed is being deallocated.
// Apartment 3B is being deallocated.

Use weak when the other object has a shorter lifetime (like a view controller’s reference to a delegate). Use unowned only when you are absolutely certain the other object will outlive the one holding the reference, and even then, ask yourself if the safety of weak is worth the hassle of an optional. Usually, it is.