In Swift, understanding memory management is essential for your app's performance and to avoid potential pitfalls, such as memory leaks. References are strong by default, but, to avoid retain cycles, you sometimes need to make use of weak references using the weak
keyword.
Weak references come with a few caveats or pitfalls, such as unexpected deallocations. An instance is immediately deallocated if no other object holds a reference to it. I'm sure you have seen this warning at some point "instance will be immediately deallocated because property delegate
is weak
".
Let's walk you through the problem by (1) giving you an example, (2) pinpointing the problem, and (3) looking at a few solutions.
An Example
Let's start with an example. Fire up Xcode and create a playground if you'd like to follow along with me. Add an import statement for the UIKit framework.
import UIKit
We start by declaring a protocol with name ViewControllerDelegate
. Note that we use AnyObject
to restrict the adoption of the protocol to classes. Structs and enums cannot conform to the protocol. That is important in a moment. The ViewControllerDelegate
protocol implements one method, viewControllerDidPerformAction(_:)
.
import UIKit
protocol ViewControllerDelegate: AnyObject {
// MARK: - Methods
func viewControllerDidPerformAction(_ viewController: UIViewController)
}
The next step is declaring a final class, MyDelegate
, that conforms to the ViewControllerDelegate
protocol. The MyDelegate
class is required to implement the viewControllerDidPerformAction(_:)
method.
import UIKit
protocol ViewControllerDelegate: AnyObject {
// MARK: - Methods
func viewControllerDidPerformAction(_ viewController: UIViewController)
}
final class MyDelegate: ViewControllerDelegate {
// MARK: - Methods
func viewControllerDidPerformAction(_ viewController: UIViewController) {
print("View Controller Did Perform Action")
}
}
Let's now implement the ViewController
class. It's a UIViewController
subclass as you may have guessed. It declares a property, delegate
, of type ViewControllerDelegate
. Note that we use the weak
keyword to weakly reference the delegate. It simply means the view controller doesn't hold on to its delegate. That is very common when adopting the delegate pattern. The delegating object should not strongly reference its delegate to avoid retrain cycles. Because the delegate
property is weak, it has an optional type.
The ViewController
class declares one method, performAction()
, in which it invokes the viewControllerDidPerformAction(_:)
method on its delegate. The view controller uses optional chaining because the delegate
property has an optional type.
import UIKit
protocol ViewControllerDelegate: AnyObject {
// MARK: - Methods
func viewControllerDidPerformAction(_ viewController: UIViewController)
}
final class MyDelegate: ViewControllerDelegate {
// MARK: - Methods
func viewControllerDidPerformAction(_ viewController: UIViewController) {
print("View Controller Did Perform Action")
}
}
final class ViewController: UIViewController {
// MARK: - Properties
weak var delegate: ViewControllerDelegate?
// MARK: - Methods
func performAction() {
delegate?.viewControllerDidPerformAction(self)
}
}
Let's put the pieces of the puzzle together. We create a ViewController
instance and create a MyDelegate
instance. We store a reference to the MyDelegate
instance in the view controller's delegate
property. Last, we invoke the view controller's performAction()
method.
let viewController = ViewController()
viewController.delegate = MyDelegate()
viewController.performAction()
Understanding the Warning
The compiler kindly warns us that the MyDelegate
instance will be immediately deallocated because the delegate
property of the view controller is declared weak
.
As a result, the delegate method isn't invoked when we executed the contents of the playground. The print statement of the viewControllerDidPerformAction(_:)
method isn't executed. That isn't surprising if we consider memory management.
An instance of a class is alive for as long as at least one object holds a reference to it, and that reference needs to be strong. The problem with this example is that the view controller holds a weak reference to the delegate. Because no other object holds a strong reference to the MyDelegate
instance, the MyDelegate
instance is immediately deallocated. This is textbook memory management.
Because the view controller uses the weak
keyword, it communicates to the compiler that it doesn't want to have a strong ownership relation with the delegate. As I wrote earlier, this is useful in situations where we want to avoid retain cycles, that is, two objects holding strong references to each other, resulting in a memory leaks.
Resolving the Warning
We have a few options to resolve the warning.
Bad Option 1: Strongly Referencing the Delegate
We can remove the weak
keyword of the delegate
property declaration. This immediately resolves the warning and fixes the problem.
final class ViewController: UIViewController {
// MARK: - Properties
var delegate: ViewControllerDelegate?
// MARK: - Methods
func performAction() {
delegate?.viewControllerDidPerformAction(self)
}
}
Why is this a bad option? When using the delegation pattern, the delegating object typically (read: almost always) holds a weak reference to its delegate to avoid a retain cycle. Take the UITableViewDelegate
protocol as an example. In a typical setup, the view controller owns the UITableView
instance and acts as the delegate of the table view. For that reason, a UITableView
instance holds a weak
reference to its delegate. Why? If the view controller strongly references its table view and the table view strongly references its delegate, the view controller, we end up with a retain cycle.
For that reason, declaring the delegate
property of the ViewController
class as strong
isn't a wise choice.
Bad Option 2: Using a Shared Singleton
We could use a shared singleton to ensure the MyDelegate
instance is never deallocated. For this example, that option is even worse than the previous option. I'm not a fan of shared singletons. Are Singletons Bad and What Is a Singleton and How To Create One In Swift explain this in more detail.
Option 3: Revising Your Architecture
The reason you are seeing this warning is most likely because your architecture is flawed in a subtle way. Let's revisit the UITableViewDelegate
protocol. The view controller owns the table view and the table view holds a weak reference to its delegate, the view controller. This setup works and doesn't introduce a retain cycle.
If the delegate object is immediately deallocated because no other object holds a strong reference to it, then investigate which object should be the owner of the delegate. I cannot answer that question for you. Take a look at References, Delegation, and Notifications for more details on the delegation pattern or email me if you feel stuck.