One-to-many delegates

By Will Braynen

There is this belief in the iOS community, perhaps reinforced by the popularity of this as an interview question, that for one-to-one callbacks we should use a (weak) delegate and for one-to-many we should use NotificationCenter. This is false.

What is true is that this is a common pattern. It is also true that it is easy to write a NotificationCenter broadcast for the one-to-many case. One problem with notification broadcasts, however, is that you get no stack trace. This means that debugging notification broadcasts can be mystifying.

Ideally, if you were to place a breakpoint inside the method that executed as a delegate callback, you then want to see an easy-to-read stack trace where all the relevant calls are on one thread and where the method that executed as a delegate callback is above the method that broadcast that callback. Similarly, if you were to place a breakpoint at the broadcast site, you would want to be able to “step into” each of the receiving sites. With NotificationCenter broadcasts, however, this is not what you get.

But, you do not have to live this way. You could in fact also use a delegate pattern for the one-to-many case. This would give you the benefit of having a stack trace and being able to step through the code.

The idea is simple enough: you would need an array of delegates and to broadcast you would loop through this array and make your calls.

The wrong way:

/// Our "observer" / "listener"
protocol ImportantThingsDoerDelegate: class {
  /// Our "callback" / notifier
  func didYetAnotherImportantThing()
}

class ImportantThingsDoer {
  // Wrong because it's a weak array of strong members, not a strong array of weak members.
  weak delegates: [ImportantThingsDoerDelegate]?

  // Assume, for the sake of discussion, that we do not 
  // have a (concurrent) dispatch queue and instead are 
  // literally extending the delegation pattern we might
  // typically use with a view and a view controller to
  // the one-to-many case.
    
  func broadcast() {
    for delegate in delegates 
      // Note that, as mentioned above, are not actually
      // async-dispatch the notification here because
      // we are not using a dispatch queue. (Although
      // in production code, we probably should.)
      delegate?.didYetAnotherImportantThing()
    }
  }
}

The dispatch-queue mechanism is not what I want to focus on in this article, so I am explicitly leaving that out. Instead, let’s focus on the weak delegates.

One problem with the code sketch above is that we don’t need a weak array of strong members. Instead, we need a strong array of weak members. (As you know, the reason for “weak” is to avoid a retain cycle.) What we really need is:

// Swift-like pseudocode
delegates: [weak ImportantThingDelegate?]

Sadly, that’s not valid Swift syntax. Instead—an idea I once got from someone else’s blog and which can also be found on stackoverflow—we could wrap our delegate like so:

class DelegateWrapper {
  // "weak var ... optional" is the familiar pattern.
  private(set) weak var delegate: ImportantThingsDoerDelegate?
    
  init(delegate: ImportantThingsDoerDelegate) {
    self.delegate = delegate
  }
}
  
// Some class that does important things and then notifies others
// whenever it did an important thing.
class ImportantThingsDoer {
  var delegateWrappers = [DelegateWrapper]()
  
  func broadcast() {
    for aDelegateWrapper in delegateWrappers {
      // notify whoever is listening
      aDelegateWrapper.delegate?.didYetAnotherImportantThing()
    }
  }
  
  // MARK: - Some convenience methods
  
  func subscribe(_ delegate: ImportantThingsDoerDelegate) {
    guard !delegatesWrappers.contains(where: { $0.delegate === delegate }) else   
  
    let delegateWrapper = DelegateWrapper(delegate: delegate)
    delegatesWrappers.append(delegateWrapper)
  }

  func unsubscribe(_ delegate: ImportantThingsDoerDelegate) {
    guard let index = delegateWrappers.firstIndex(where: { $0.delegate === delegate }) else   
    
    delegateWrappers.remove(at: index)
  }

  func unsubscribeAll() 
    delegateWrappers.removeAll()
  }
}

Now we would be able to (a) use the debugger to “step into” to see what happens after we notify someone, and (b) have a stack trace if we end up getting notified and wish to retrace our steps to see who broadcast the notification and why.

Next time someone tells you that one-to-many is for notification center, you could say, “well, actually…” One-to-many delegates!