Understanding Swift Memory Management

How to Use a Capture List to Break a Retain Cycle

Understanding Swift Memory Management

If you have been paying attention, then you may have noticed that we haven't resolved the strong reference cycle we introduced between the root view controller and the settings view controller earlier in this series. This is what the implementation of the RootViewController class looks like.

import UIKit

class RootViewController: UIViewController {

    // MARK: - Properties

    private lazy var settingsViewController: SettingsViewController = {
        // Initialize Settings View Controller
        guard let settingsViewController = UIStoryboard.main.instantiateViewController(withIdentifier: SettingsViewController.storyboardIdentifier) as? SettingsViewController else {
            fatalError("Unable to Instantiate Settings View Controller")
        }

        // Install Handler
        settingsViewController.didModifySettings = {
            self.updateView()
        }

        return settingsViewController
    }()

    // MARK: - Navigation

    @IBAction func showSettings(_ sender: Any) {
        // Present Settings View Controller
        present(settingsViewController, animated: true)
    }

    // MARK: - View Methods

    private func updateView() {}

}

The RootViewController class defines an action, showSettings(_:), to present a SettingsViewController instance to the user. It lazily instantiates an instance of the SettingsViewController class and keeps a reference to the instance in its settingsViewController property. It configures the settings view controller by installing the didModifySettings handler.

Remember that we created a reference cycle or retain cycle. The root view controller keeps a strong reference to the settings view controller through its settingsViewController property. The settings view controller indirectly keeps a strong reference to the root view controller through its didModifySettings handler. We discussed this earlier in this series.

Because closures are reference types, this creates a strong reference cycle.

Capturing Values

Closures are powerful and versatile, which is in part due to their ability to capture values from the scope in which they are defined. If you would like to learn more about closures and capturing values, I recommend reading the chapter on closures in The Swift Programming Language.

When a closure captures a value from its surrounding scope, Swift is responsible for managing the memory for that value. As I mentioned earlier, you sometimes need to give ARC a hint to avoid retain cycles. Let's revisit the settingsViewController property of the RootViewController class.

private lazy var settingsViewController: SettingsViewController = {
    // Initialize Settings View Controller
    guard let settingsViewController = UIStoryboard.main.instantiateViewController(withIdentifier: SettingsViewController.storyboardIdentifier) as? SettingsViewController else {
        fatalError("Unable to Instantiate Settings View Controller")
    }

    // Install Handler
    settingsViewController.didModifySettings = {
        self.updateView()
    }

    return settingsViewController
}()

The closure we assign to the didModifySettings property of the settings view controller captures self, the RootViewController instance. The closure keeps a strong reference to the RootViewController instance. Because the root view controller also keeps a strong reference to the SettingsViewController instance, we end up with a reference cycle or retain cycle.

If you read or watched the previous episode of this series, then you probably know how we can break this strong reference cycle. We need to prevent the closure from strongly referencing self, the RootViewController instance. How do we do that?

Defining a Capture List

The references a closure holds to reference types are strong by default. We can change this behavior by defining a capture list. The capture list of a closure defines how the reference types captured by the closure are managed in memory. This simply means that we can use a capture list to communicate to ARC how it should treat the references the closure holds to reference types.

A capture list is nothing more than a collection of pairs wrapped in square braces. The pairs are separated by commas. A capture list is defined after the opening curly brace and before the parameter list and return type of the closure. An element of a capture list is the weak or unowned keyword followed by the reference to a class instance. In this example, we use a capture list to weakly capture self in the closure.

private lazy var settingsViewController: SettingsViewController = {
    // Initialize Settings View Controller
    guard let settingsViewController = UIStoryboard.main.instantiateViewController(withIdentifier: SettingsViewController.storyboardIdentifier) as? SettingsViewController else {
        fatalError("Unable to Instantiate Settings View Controller")
    }

    // Install Handler
    settingsViewController.didModifySettings = { [weak self] in
        self?.updateView()
    }

    return settingsViewController
}()

Notice that we use optional chaining to access self, the RootViewController instance. Remember that a weak reference is always of an optional type. We covered this earlier in this series.

Because the closure assigned to the didModifySettings property doesn't define parameters and doesn't return a value, the capture list is positioned between the opening curly brace of the closure and the in keyword.

The settings view controller is owned by the root view controller and that implies that we can be sure that the root view controller is deallocated after the settings view controller. This means that we could use an unowned instead of a weak reference to the root view controller.

private lazy var settingsViewController: SettingsViewController = {
    // Initialize Settings View Controller
    guard let settingsViewController = UIStoryboard.main.instantiateViewController(withIdentifier: SettingsViewController.storyboardIdentifier) as? SettingsViewController else {
        fatalError("Unable to Instantiate Settings View Controller")
    }

    // Install Handler
    settingsViewController.didModifySettings = { [unowned self] in
        self.updateView()
    }

    return settingsViewController
}()

By using an unowned reference, we don't need to use optional chaining to access the root view controller in the closure. I personally don't use unowned references. I explained why that is in the previous episode.

The strong reference cycle is broken.

Debugging Memory Issues

I would like to end this episode with a common and subtle retain cycle. The application shows a view with a button that reads Next. Tapping the button pushes a second view controller onto the navigation stack of a navigation controller. Tapping the back button in the top left brings the user back to the first view controller.

Let's take a peek under the hood. The root view controller of the navigation controller is an instance of the FirstViewController class. The implementation is as simple as it gets.

import UIKit

class FirstViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set Title
        title = "First"
    }

}

When the user taps the Next button, an instance of the SecondViewController class is pushed onto the navigation stack of the navigation controller. The implementation of the SecondViewController class isn't complex either.

import UIKit

class SecondViewController: UIViewController {

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()

        // Set Title
        title = "Second"

        // Observe Notification: Application Did Become Active Notification
        NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
            self?.updateView()
        }
    }

    // MARK: - View Methods

    private func updateView() {}

}

The view controller sets its title property and adds itself as an observer for UIApplication.didBecomeActiveNotification notifications. Notice that the SecondViewController class uses the more modern API for observing notifications. The closure that is passed to the addObserver(forName:object:queue:using:) method invokes the updateView() method of the SecondViewController class.

// Observe Notification: Application Did Become Active Notification
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
    self?.updateView()
}

Let's run the application in the simulator. Tapping the Next button takes us to the second view and tapping the back button takes us back to the first view. At this point, you would expect the SecondViewController instance to be deallocated. From the moment a view controller is popped from the navigation stack of a navigation controller, the view controller should be deallocated. Right?

Click the Debug Memory Graph button at the bottom to find out if that is true. The memory graph debugger doesn't agree with this hypothesis. The instance of the SecondViewController class is still alive. The application is leaking memory. Do you know why that is?

Memory Graph Debugger

Let's ask Xcode for some help. Click the scheme at the top and choose Edit Scheme.... Select the Diagnostics tab at the top and check Malloc Stack. This is a powerful option to debug memory issues that I use frequently. By checking this option, the memory graph debugger not only shows the relationships between objects. It also shows the allocation stack traces. I cover this in more detail in Debugging Applications With Xcode. Let me show you what this means.

Enabling Malloc Stack

We build and run the application one more time, tap the Next button, and return by tapping the back button. Click the Debug Memory Graph button at the bottom. Select the SecondViewController instance on the left. The memory graph debugger shows us which objects keep a reference to the view controller.

Memory Graph Debugger

Select the Swift closure context object and open the Memory Inspector on the right. The Backtrace section shows us the allocation stack trace. If you hover over the fourth frame, an arrow appears on the right. Click it to navigate to the piece of code that corresponds with the frame.

Memory Graph Debugger

The closure that is passed to the addObserver(forName:object:queue:using:) method strongly references the SecondViewController instance and that prevents it from being deallocated when it is popped from the navigation stack. You should know how we can resolve this memory leak. We define a capture list to weakly capture self, the SecondViewController instance, and use optional chaining to access the SecondViewController instance in the closure. That's it.

// Observe Notification: Application Did Become Active Notification
NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: .main) { [weak self] _ in
    self?.updateView()
}

Let's see if that worked. Run the application in the simulator, tap the Next button, and return by tapping the back button. Click the Debug Memory Graph button at the bottom. The SecondViewController instance is properly deallocated when it is popped from the navigation stack.

Memory Graph Debugger

Don't Skip Memory Management

Even though memory management may seem like a more advanced topic, it absolutely isn't. It is essential that you understand how Automatic Reference Counting works, what the differences are between value types and reference types, and when to use a weak or unowned reference instead of a strong reference. Capture lists are essential to avoid strong reference cycles. They may seem a bit exotic at first, but you get used to the syntax in no time.