Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 2

Breaking Retain Cycles: Weak and Unowned References

Recommended resources

Hello! Welcome back to our course on professional iOS development.

In our last lesson, we established the foundation of how Automatic Reference Counting (ARC) works. We learned that ARC tracks strong references to class instances and deallocates them when their reference count drops to zero. This automated process is highly efficient but has one major Achilles' heel that requires our direct intervention: the strong reference cycle, or retain cycle.

Today's lesson directly addresses that challenge. Our learning outcome is to use weak and unowned references to identify and resolve retain cycles in closures and delegates. Mastering this skill is non-negotiable for writing robust, memory-efficient iOS applications. We'll explore what retain cycles are, why they cause memory leaks, and how to break them using Swift's reference-type keywords.

1. The Problem: Strong Reference Cycles

A strong reference cycle occurs when two or more class instances hold strong references to each other, creating a closed loop. Because each object in the loop is kept alive by another, their reference counts can never reach zero. As a result, ARC never deallocates them, and they remain in memory for the entire lifetime of the app. This is a classic memory leak.

To understand this conceptually, let's watch a short segment from the "Complete Guide to Memory Management" video, which uses a great analogy.

Complete Guide to Memory Management in Swift & Beyond

This clip explains the concept of a retain cycle using a clear analogy of a bank, a customer, and a credit card. It demonstrates how mutual strong references can prevent objects from being deallocated even when they are no longer in use.

Please watch the segment from 11:04 to 13:53. Focus on how the Customer and CreditCard objects keep each other alive, creating a leak. This perfectly illustrates the problem we need to solve.

Now, let's see this in code. A common scenario is a relationship between two objects, like a Person and their Apartment.

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

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

class Apartment {
    let unit: String
    var tenant: Person? // This will cause a cycle

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

// Scoping to demonstrate deallocation
func createAndBreakReferences() {
    print("--- Creating cycle ---")
    var john: Person? = Person(name: "John Appleseed")
    var unit4A: Apartment? = Apartment(unit: "4A")

    // 1. Create the cycle: two strong references
    john?.apartment = unit4A // Person has a strong ref to Apartment
    unit4A?.tenant = john    // Apartment has a strong ref to Person

    print("--- Setting references to nil ---")
    john = nil
    unit4A = nil
    print("--- Function finished ---")
}

createAndBreakReferences()

If you run this code in a playground, you will see init messages, but you will never see the deinit messages. Even though john and unit4A are set to nil at the end of the function, the Person and Apartment instances they referred to are still holding onto each other, keeping each other's reference count at 1. They have leaked.


2. The Solution: weak and unowned References

To break these cycles, Swift gives us two tools: weak and unowned references. Both create a reference to an object without increasing its reference count, thereby preventing them from participating in a strong reference cycle. The key difference lies in their behavior and the guarantees they provide.

To get a clear understanding of the distinction, we'll consult a detailed article on iOS memory management.

iOS Memory Management & Performance Optimization: Complete ...

This article, 'iOS Memory Management & Performance Optimization', provides a fantastic breakdown of the different reference types. We will focus on the sections defining weak and unowned.

Please read the sections titled '2. Weak References' and '3. Unowned References'. Then, read the first FAQ entry, 'Q: When should I use weak vs unowned references?'. Pay close attention to the code examples and the 'Golden Rule' mentioned.

Let's summarize the key points from the reading:

Weak References

  • A weak reference does not increase the reference count.
  • It is used when the referenced object can have a shorter lifetime. ARC can deallocate the object even if weak references still point to it.
  • When the object is deallocated, ARC automatically sets any weak references to it to nil.
  • Because of this, a weak reference must always be an optional var.

Let's fix our Apartment example by making the tenant reference weak:

class Apartment {
    let unit: String
    weak var tenant: Person? // This breaks the cycle

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

Now, if you run the same createAndBreakReferences function, you will see both deinit messages print as expected. The Apartment instance no longer keeps the Person instance alive.

Unowned References

  • An unowned reference also does not increase the reference count.
  • It is used when you can guarantee that the referenced object will have the same or a longer lifetime than the object holding the reference.
  • It is not optional. It's a promise to the compiler that the reference will never be nil during its lifetime.
  • Accessing an unowned reference after the object has been deallocated will crash your app.

A classic example is a Customer and a CreditCard. A credit card cannot exist without a customer.

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) { self.name = name }
    deinit { print("Customer \(name) deinitialized") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer // A card always has a customer

    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card #\(number) deinitialized") }
}

Rule of Thumb: When in doubt, use weak. It is safer. Use unowned only when the object relationship contractually guarantees the reference will not be nil, and for a potential micro-optimization (as it avoids the overhead of optionality and the side table mechanism needed for weak references).


3. Common Patterns and How to Fix Them

Now let's apply this knowledge to the two most common places you will find retain cycles in iOS development: Delegates and Closures.

Pattern 1: Delegate Retain Cycles

The delegate pattern is used extensively in UIKit. A classic example is a ViewController acting as the delegate for a custom data-fetching service.

  • The ViewController creates and holds a strong reference to the DataService.
  • The DataService needs a reference back to its delegate (ViewController) to send updates.
  • If this delegate property is strong, you have a retain cycle.
Strong and Weak References Between View Controllers
This diagram shows the correct relationship to avoid a cycle. The ParentViewController holds a strong reference to its child, but the child's reference back to the parent is `weak`.

The Fix: Always declare delegate properties as weak.

To do this, the protocol must be restricted to class types, because weak can only be used with reference types. You achieve this by inheriting from AnyObject (or class).

ARC in iOS: The Memory Management Revolution That ...

The article 'ARC in iOS' shows this pattern very clearly. We will focus on the section about delegates.

Read the section titled '4. Delegates Should Almost Always Be Weak'. Notice the protocol conformance to AnyObject and the weak var declaration.

Here's the corrected code structure:

// Protocol must be class-only
protocol DataServiceDelegate: AnyObject {
    func dataDidUpdate(data: String)
}

class DataService {
    // Delegate must be weak to prevent a retain cycle
    weak var delegate: DataServiceDelegate?

    func fetchData() {
        // ... after fetching data
        delegate?.dataDidUpdate(data: "New Data")
    }
}

class MyViewController: UIViewController, DataServiceDelegate {
    let dataService = DataService()

    override func viewDidLoad() {
        super.viewDidLoad()
        dataService.delegate = self // No cycle is created
        dataService.fetchData()
    }

    func dataDidUpdate(data: String) {
        print("Received data: \(data)")
    }
    
    deinit {
        print("MyViewController deinitialized")
    }
}

Pattern 2: Closure Retain Cycles

This is an extremely common source of leaks, especially when dealing with network requests or other asynchronous operations. A closure will capture any external variables it uses, and by default, it captures them strongly.

A retain cycle occurs when:

  1. A class instance holds a strong reference to a closure (e.g., the closure is a property of the class).
  2. That closure, in turn, captures a strong reference to the class instance (by using self).
Resolving a Retain Cycle with a Weak Reference
This diagram illustrates a retain cycle broken by a closure. The `TestClass` holds a strong reference to its `Closure`, but the closure's back-reference to the `TestClass` instance is `weak`.

The Fix: Use a capture list to define how the closure captures self.

The most common solution is [weak self]. This tells the closure to capture a weak reference to self. Inside the closure, self will now be an optional.

iOS Memory Management & Performance Optimization: Complete ...

The 'iOS Memory Management' article provides a great example of a closure-based retain cycle and the modern syntax to fix it.

Read the section titled 'Pattern #1: Retain Cycles in Closures'. Focus on 'The Problem' and 'The Fix'.

Here is the pattern in action:

class NetworkManager {
    // This closure property creates the potential for a cycle
    var onComplete: ((String) -> Void)?

    func makeRequest() {
        // Simulate an async network call
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            self.onComplete?("Request Complete")
        }
    }

    deinit { print("NetworkManager deinitialized") }
}

class AnotherViewController: UIViewController {
    let networkManager = NetworkManager()

    func setupAndFetch() {
        // Here's the cycle: self -> networkManager -> onComplete -> self
        networkManager.onComplete = { result in
            // By default, this is a strong capture of `self`
            self.view.backgroundColor = .green
            print(result)
        }
        
        // Let's fix it with a capture list
        networkManager.onComplete = { [weak self] result in
            // `self` is now `AnotherViewController?` (an optional)
            self?.view.backgroundColor = .green
            print(result)
        }
        
        networkManager.makeRequest()
    }
    
    deinit {
        print("AnotherViewController deinitialized")
    }
}
Test your understanding!

You're working on a timer feature. A TimerViewController holds a Timer instance. You use the block-based Timer.scheduledTimer API. The timer's closure needs to update a label on the view controller.

class TimerViewController: UIViewController {
    var timer: Timer?

    func startTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
            // How do you safely update a label here without causing a leak?
            // self.myLabel.text = "Updated"
        }
    }
    
    deinit {
        timer?.invalidate()
        print("TimerViewController deinitialized")
    }
}

How would you modify the closure to prevent a retain cycle?

Show answer

You would add [weak self] to the closure's capture list. The TimerViewController holds a strong reference to the Timer. The Timer holds a strong reference to its closure. If that closure captures self strongly, you have a cycle.

The correct implementation is:

timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
    // Safely update the label using optional chaining
    self?.myLabel.text = "Updated"
}

This ensures that the timer's closure doesn't keep the TimerViewController alive if the user navigates away from the screen.


Conclusion

Today we tackled the most critical aspect of manual intervention in ARC. By understanding and correctly applying weak and unowned references, you can prevent the most common sources of memory leaks in iOS apps.

Key Takeaways:

  • A strong reference cycle (retain cycle) occurs when two or more class instances hold strong references to each other, leading to memory leaks.
  • You can break cycles by declaring one of the references as weak or unowned.
  • weak: Use for references that can become nil. They must be optional var. This is the safe default.
  • unowned: Use only when you can guarantee the referenced object will outlive the owner. This is a non-optional promise that will crash if broken.
  • Delegate properties should almost always be weak.
  • Escaping closures that capture self should use a capture list, typically [weak self], to avoid cycles.

Preview of the Next Lesson:
We've learned the theory behind retain cycles and the syntax to fix them. But in a large, complex project, how do you find these leaks in the first place? In our next lesson, we will get practical and use the Instruments Memory Graph Debugger to visually detect memory leaks and retain cycles, turning abstract concepts into a powerful debugging skill.