Why should a delegate property be weak, and why must the protocol be class-bound?

3.3.2 · L1 · Interview

Predict first

Does this compile?

protocol Delegate {}
final class Worker {
    weak var delegate: Delegate?
}
  • Yes
  • No

Predict first

What does this print with a strong delegate property?

protocol Delegate: AnyObject {}
final class Worker { var delegate: Delegate? }
final class Screen: Delegate {
    let worker = Worker()
    init() { worker.delegate = self }
    deinit { print("screen gone") }
}

var s: Screen? = Screen()
s = nil
print("after")
  • screen gone, after
  • after

Mental model. A news channel reports to its audience but does not own them. If it did, the audience could never leave.

A delegate is usually the owner of the object that calls it: a screen owns its data service, and the service reports back to the screen. If the service held the screen strongly too, the two would form a cycle. So the convention is weak var delegate: SomeDelegate?: the delegate is notified, but not owned.

A weak reference needs a class type, so the protocol must be class-bound: protocol SomeDelegate: AnyObject. Without that the compiler rejects weak and tells you to add a class bound. The exception is a delegate you intend to own (an URLSession holds its delegate strongly until invalidated, by design), which should be documented because it is a deliberate cycle that you break with invalidateAndCancel.

A protocol that is not class-bound could be adopted by a struct, which cannot be weak. Declare protocol Delegate: AnyObject.

The screen owns the worker and the worker owns the screen: a cycle. Making delegate weak lets screen gone print.

Common traps

  • Forgetting AnyObject and then removing weak to make it compile. You silently create the cycle.
  • Making the delegate property let. Delegates change, so keep a settable weak variable.

They might follow up with

  • Which framework delegates are strong? URLSession retains its delegate until the session is invalidated; read the docs for each.
  • How does this change with closures? A closure callback has the same shape; the same rule applies through a capture list.

Related questions

  • 3.2.2 Why is a weak reference optional, what can be weak, and what is the trap when reading one?
  • 3.3.3 When does a closure create a cycle, and why doesn’t a lazy var closure always?

← What is a retain cycle, and how do you find and fix one?   When does a closure create a cycle, and why doesn’t a lazy var… →