Skip to main content

Swift ARC Basics

Introduction

Memory management is a crucial aspect of programming that ensures your application uses resources efficiently. In Swift, memory management is handled through a system called Automatic Reference Counting (ARC). Unlike other programming languages that use garbage collectors, Swift's ARC determines when class instances are no longer needed and automatically frees up the memory they occupy.

In this lesson, we'll explore how ARC works in Swift, why it's important, and how to avoid common memory management issues like memory leaks.

What is Automatic Reference Counting?

ARC is a memory management feature in Swift that tracks and manages your app's memory usage. When you create an instance of a class, ARC allocates a chunk of memory to store information about that instance. When the instance is no longer needed, ARC frees up the memory used by that instance so that the memory can be used for other purposes.

Key Concept: References

ARC works by keeping track of how many references exist to each class instance. As long as at least one active reference to an instance exists, ARC keeps that instance in memory. When no active references remain, ARC deallocates the instance.

Let's see how this works with a simple example:

swift
class Person {
let name: String

init(name: String) {
self.name = name
print("\(name) is being initialized")
}

deinit {
print("\(name) is being deinitialized")
}
}

// Create a new scope
do {
let person = Person(name: "John")
print("Person object is available inside this scope")
}
// The scope ends here

print("The scope has ended")

Output:

John is being initialized
Person object is available inside this scope
John is being deinitialized
The scope has ended

In this example:

  1. We create a Person class with an initializer and a deinitializer (marked by the deinit keyword).
  2. Inside the do block, we create an instance of Person named "John".
  3. When the do block ends, the person variable goes out of scope, meaning there are no more references to our Person instance.
  4. ARC automatically deallocates the instance, triggering the deinitializer.

Strong References

By default, any time you assign a class instance to a property, constant, or variable, a strong reference is created. A strong reference says, "Keep this object in memory as long as I exist."

Strong Reference Cycles (Memory Leaks)

One challenge with ARC is the possibility of creating strong reference cycles (also known as retain cycles or memory leaks). This happens when two class instances hold strong references to each other, creating a cycle that prevents ARC from deallocating either instance.

Let's see an example of a strong reference cycle:

swift
class Person {
let name: String
var apartment: Apartment?

init(name: String) {
self.name = name
print("\(name) is being initialized")
}

deinit {
print("\(name) is being deinitialized")
}
}

class Apartment {
let unit: String
var tenant: Person?

init(unit: String) {
self.unit = unit
print("Apartment \(unit) is being initialized")
}

deinit {
print("Apartment \(unit) is being deinitialized")
}
}

// Create a strong reference cycle
do {
let john = Person(name: "John")
let unit4A = Apartment(unit: "4A")

john.apartment = unit4A // Person now has a strong reference to Apartment
unit4A.tenant = john // Apartment now has a strong reference to Person
}
print("The scope has ended")

Output:

John is being initialized
Apartment 4A is being initialized
The scope has ended

Notice that the deinitializers are never called! Even though john and unit4A go out of scope, each instance maintains a strong reference to the other, preventing ARC from deallocating either object. This is a memory leak.

Breaking Strong Reference Cycles

Swift provides two ways to resolve strong reference cycles:

  1. Weak references
  2. Unowned references

Weak References

A weak reference does not keep an object alive. If the object it points to is deallocated, the weak reference is automatically set to nil. Weak references must be optional variables because they can become nil at any time.

Let's fix our previous example using a weak reference:

swift
class Person {
let name: String
var apartment: Apartment?

init(name: String) {
self.name = name
print("\(name) is being initialized")
}

deinit {
print("\(name) is being deinitialized")
}
}

class Apartment {
let unit: String
weak var tenant: Person? // Using weak reference

init(unit: String) {
self.unit = unit
print("Apartment \(unit) is being initialized")
}

deinit {
print("Apartment \(unit) is being deinitialized")
}
}

do {
let john = Person(name: "John")
let unit4A = Apartment(unit: "4A")

john.apartment = unit4A
unit4A.tenant = john
}
print("The scope has ended")

Output:

John is being initialized
Apartment 4A is being initialized
John is being deinitialized
Apartment 4A is being deinitialized
The scope has ended

Now both instances are properly deallocated when they go out of scope.

Unowned References

An unowned reference is similar to a weak reference but must always refer to an instance that has not been deallocated. Unlike weak references, unowned references are not optional and cannot be nil. Use unowned when the referenced instance will always have the same or longer lifetime than the instance that references it.

swift
class Customer {
let name: String
var card: CreditCard?

init(name: String) {
self.name = name
print("\(name) is being initialized")
}

deinit {
print("\(name) is being deinitialized")
}
}

class CreditCard {
let number: String
unowned let customer: Customer // Using unowned reference

init(number: String, customer: Customer) {
self.number = number
self.customer = customer
print("Card \(number) is being initialized")
}

deinit {
print("Card \(number) is being deinitialized")
}
}

do {
let john = Customer(name: "John")
john.card = CreditCard(number: "1234-5678-9012-3456", customer: john)
}
print("The scope has ended")

Output:

John is being initialized
Card 1234-5678-9012-3456 is being initialized
Card 1234-5678-9012-3456 is being deinitialized
John is being deinitialized
The scope has ended

In this case, we use unowned because a credit card will always have a customer, and we can assume the customer won't be deallocated before their credit card.

When to Use Weak vs. Unowned References

Use weak when:

  • The reference might become nil at some point
  • The referenced object might be deallocated while your object still exists
  • Common cases: delegate patterns, parent-child relationships where the parent doesn't own the child

Use unowned when:

  • The reference will never be nil once set
  • The referenced object will always have a longer or equal lifetime
  • Common cases: part-whole relationships where one object is a logical part of another

Practical Example: Delegate Pattern

The delegate pattern is one of the most common places where weak references are needed in iOS development:

swift
protocol DataUpdaterDelegate: AnyObject {
func didUpdateData(_ data: String)
}

class DataUpdater {
weak var delegate: DataUpdaterDelegate?

func fetchData() {
// Simulate network fetch
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.delegate?.didUpdateData("Fresh data from server")
}
}
}

class DataPresenter: DataUpdaterDelegate {
let dataUpdater = DataUpdater()

init() {
dataUpdater.delegate = self
}

func startFetching() {
dataUpdater.fetchData()
}

func didUpdateData(_ data: String) {
print("Received data: \(data)")
}
}

// Usage
let presenter = DataPresenter()
presenter.startFetching()
// After 1 second: "Received data: Fresh data from server"

In this example, we use a weak reference for the delegate to avoid a strong reference cycle between DataUpdater and DataPresenter.

Summary

Automatic Reference Counting (ARC) is Swift's memory management system that works behind the scenes to allocate and deallocate memory for your app. Key points to remember:

  • ARC tracks strong references to class instances
  • When the count of strong references to an instance drops to zero, ARC deallocates the instance
  • Strong reference cycles occur when two objects reference each other strongly, preventing deallocation
  • Weak and unowned references help break reference cycles
    • Use weak for references that can become nil
    • Use unowned for references that will never become nil during their lifetime

Understanding ARC is essential for writing efficient Swift applications that don't leak memory. By being mindful of your reference relationships, you can ensure your app uses memory efficiently.

Exercises

  1. Create a parent-child relationship between two classes where the parent has an array of children and the children have a reference back to the parent. Use appropriate reference types to avoid memory leaks.

  2. Implement a notification system where objects can subscribe to receive notifications. Make sure that when subscriber objects are deallocated, they are automatically removed from the notification list.

  3. Identify and fix the memory leak in the following code:

    swift
    class NetworkManager {
    var completionHandler: (() -> Void)?

    func performRequest(completion: @escaping () -> Void) {
    self.completionHandler = {
    print("Request completed")
    completion()
    }
    }
    }

    class ViewController {
    let networkManager = NetworkManager()

    func fetchData() {
    networkManager.performRequest {
    self.updateUI()
    }
    }

    func updateUI() {
    print("UI Updated")
    }
    }

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)