Skip to content →

Multicast Delegates in Swift

Introduction

At some point, eventually, the need arises where parts of your code need to be notified of something that happened in other code. This is commonly implemented by way of the Observer Pattern.

As you may know already, Swift does indeed support the idea of Protocols. With Protocols, much like in Objective-C, it is typical to have a class member named “delegate” which can be assigned a single instance of an object that implements the protocol. Something like the code segment below is a common pattern…

This works well for simple scenarios, such as responding to a button or refreshing a single-view application when a data model has changed. But what if our needs are more complex? What if we have several subviews that need to be notified when something changes in our model?

Cocoa itself provides several mechanisms to do this (NSNotification, target-action and KVO to name a few) but none of them really fit the bill, in my opinion, because they all rely on the Cocoa framework. In addition, they can be confusing and cumbersome to implement for your own objects.

Coming from a C#.NET background myself, I really like and appreciate the way events work. They are simple to use and it is a great way to add multiple listeners to a single event.

After doing some reading of how others were solving this problem, one of the better solutions I discovered was Colin Eberhardt’s article on Implementing Events in Swift. It addresses a lot of the issues when implementing multicast delegates in Swift, but there are a few reasons that I do not prefer this approach…

  • He makes no use of protocols. Personally, I like the protocol approach because it allows a single object to handle several types of events. Also, using protocols matches nicely with existing language convention.
  • His final implementation does not support the += and -= syntactic sugar.

The Scenario

Below is some sample code demonstrating how I wanted multicast delegates to function. Essentially I wanted them to behave very similarly to the single-delegate pattern we already have with Swift but with the ability to add more than one listener.

The scenario below also reveals if we have issues with strong cycle references (or circular references). When working with delegates, usually you want to preface them with the “weak” keyword so they do not add to the reference count of a particular object. ARC (Automatic Reference Counting) is beyond the scope of this post, but it is definitely a concern when working with multicast delegates, which is addressed later on in this article.

Option #1 – This Works, But…

One of the most common implementations that Swift developers try to implement looks something like this…

In the example above, we keep an array of delegates and when the invoke() method is called, the appropriate delegates are passed along to the caller so the appropriate protocol member(s) may be called for each delegate. Our dispatcher class’s code that “raises the event” looks like this…

This implementation does indeed work, but with a few significant caveats…

  • Delegate array strongly references each delegate. We want delegates to be weak references, generally. In our example scenario, this will result in a strong reference cycle (memory leak) because the delegates refer to the object that is dispatching (which is common in practice).
  • No removeDelegate() implemented. Because T is not guaranteed to be AnyObject or Equatable, we can not search for the element in the array.

Option #2 – A Better Way

In order to implement a removeDelegate() method, we need to enforce T to be only for classes (by using T: AnyClass in the MulticastDelegate declaration). In addition, we need to have a way to have weak references as array elements (which Swift does not seem to support). To work around this array limitation, we can create wrapper class (called “WeakWrapper”) to store the weak reference to the actual delegate.

There are a also few compromises I needed to make that makes this code not quite as “clean” as I would like…

  • A runtime check was required to make sure T is a class (is AnyObject). In a perfect world, we would like to enforce this at compile time, but Swift’s limitations for using protocols as concrete types prevents us from doing this.
  • Using a Set would be more appropriate than an array, and would avoid having to do an array search. But this would force delegates to have to be Hashable.
  • It is not practical to support value types (struct or enum) that conform to the protocol. The reason for this is because it is too easy to run into strong reference cycles if the struct has a strong reference to the dispatching class (because a copy of the struct with a strong reference would have to be stored).

The final implementation of MulticastDelegate looks like this…

Conclusion

It would be nice if there was more support in the Swift language for multicast delegates, but for the time being, the above option provides a reasonable enough solution to be useful in most situations.

Published in Software Development

11 Comments

  1. David Rees

    David Rees

    You’ve done a great job on this. Extremely useful.

  2. Hannes Sverrisson

    Hannes Sverrisson

    Thanks, for the idea of the weak delegates.

    I think the wrapper should be a struct:
    private struct WeakDelegate {
    weak var value: AnyObject?

    init(_ value: AnyObject) {
    self.value = value
    }
    }

  3. Antonio

    Antonio

    Great post. Thanks 🙂

    You can use:
    private let delegates: NSHashTable = NSHashTable.weakObjects()

    instead of WeakWrapper

  4. Another Programmer

    Another Programmer

    This is absolute gold! Such respect.

  5. Lampros

    Lampros

    consider to use NSMapTable wich can hold keys and values with weak references, in such a way that entries are removed when either the key or value is deallocated rather the NSHashTable?

  6. Mike

    Mike

    Thanks for post! Could you share an example of race condition on invoke method?

  7. JM

    JM

    This works great (had to change a bit of the syntax for Swift 3).

    I ran some performance tests vs NSHashTable and this is much faster. On 100_000 items this took 0.027s vs NSHashTable 0.087! I believe it’s faster primarily because to get the contents of NSHashTable you have to use the table’s allObjects property, and I believe there is some overhead involved.

    What I don’t understand is what is going on in invoke(). I noticed that in addDelegate() there is nothing stopping you from adding the same object twice; yet on invoke() duplicate objects get removed and so are never called twice. I can’t follow the logic of how this works. All I see is a check to see if the value is nil, which means it was killed by arc. Nothing to do with checking duplicates?

    So I was digging further– in addDelegate() I added a loop to check if the object was already there and then purposely tried to add the same object twice. The check always fails and always ends up adding the duplicate object.

    func addDelegate(_ delegate: T) {
    for delegateInArray in self. weakDelegates {
    if let obj = delegateInArray.value {
    if obj === delegate as AnyObject {
    return
    }
    }
    }
    delegates.append(WeakWrapper(delegate as AnyObject))
    }

    What am I missing?

    Much appreciated and great work.

  8. […] if this below doesn’t bother you, then you could eventually go fully generic (with some caveats, though – read the entire blog post), but again, in my opinion it would […]

Leave a Reply

Your email address will not be published. Required fields are marked *