One-to-many delegates

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. The 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]?
    
  func broadcast() {
    for delegate in delegates 
      delegate?.didYetAnotherImportantThing()
    }
  }
}

But 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!