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.
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.
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?
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.
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.
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.
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.
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.