Event class for Swift

Say we want to implement the event/message pattern in Swift to decouple our objects. The event should be generic so that we can specify a type argument.

let start = Event<Int>()

start is an event that can be fired passing an integer; listeners must having a function that takes in an Int value and returns Void can subscribe to this event

Iteration 1

Let's target an API like the following.

var object_1 = SomeClass()
var object_2 = AnotherClass()

// NOTE: handleEvent is a function defined as
// func handleEvent(arg:Int) { ... }
// in the respective classes
start.add(object_1.handleEvent)
start.add(object_2.handleEvent)

To get this in place, we would need to specify our Event class as follows.

class Event<T> {
    // this will be the signature of handler functions
    // that can respond to this event, it should take in
    // a single parameter of type T and return Void
    typealias EventHandler = (T)->()

    // the list of handlers this event knows about
    var handlers = [EventHandler]()

    // given a handler, add it to the list of handlers
    func add(handler:EventHandler) {
        handlers.append(handler)
    }

    // to raise the event, we need to pass in the args parameter
    // of type T which will then be forwarded to all the handlers
    func fire(arg:T) {
        for handler in handlers {
            handler(arg)
        }
    }
}

So once start has been configured, we can fire it using start.fire(100) and all the listeners associated with start will get called; in our example, that means the handleEvent functions of both object_1 and object_2 will be called, being passed an argument value of 100.

This means that once all listeners are added to start, later firing start will in turn notify the listeners. It looks all fine except thar if the following situation happens, we may have a problem:

// initially
var object_1 = SomeClass() // the initial instance
start.add(object_1.handleEvent)
start.fire(100) // the initial object will get notified

// later on
object_1 = SomeClass() // re-initialize new instance
start.fire(200)

The problem is that the second call to start.fire will invoke the handleEvent function of the initial instance, which means that even though the variable object_1 now references a new instance, the Event class has held a strong reference internally to the old instance. This is as it is - since we are passing in a Function to the add method, the handlers array inside Event will hold a strong reference to that, and in-turn, a strong reference to the initial instance of object_1.

This could lead to problems - for example, what if we really intended to remove the old instance object_1 referenced and replace it with a new instance, and did not expect any unwanted side effects just because we added it to the Event class. We are in a situation where we cannot event identify which handler in the handlers array to remove, because Function types are not equatable.

What we need is a mechanism for Event to hold weak references to the handler objects. A weak reference to an object will not cause a retain in ARC when the referenced object can be otherwise deallocated.

Iteration 2

To allow for weak references, we need to allow for weak Function references. This is not possible in Swift (as of up to 2.0).

This means we need to start thinking outside (T)->() type handlers. First off, we need to pass on the object so that we can create a weak reference to it, and also the handler function. Swift instance methods are curried functions. Which means calling object_1.handleEvent(100) is the same as calling SomeClass.handleEvent(object_1)(100). In the second version we specify the type of object_1, which is SomeClass in our example. This will have a signature like (SomeClass)->(Int)->(), which means a function that takes in an argument of SomeClass and returns another function of type (Int)->().

Now we are getting somewhere; if we could generalize the above, we could say that all Event handlers are of AnyObject type and the function to handle the event should be of type (AnyObject)->(T)->().

// perhaps something like this in the event class?

// handlers have a new type now
var handlers = [(AnyObject)->(T)->()] ()

func add(listener:AnyObject, handler:(AnyObject)->(T)->()) {
    // now what to do?
}

What do we do inside the add method?

First of all, we need a mechanism to capture the listener argument in a weak reference. We need this in order to invoke handler in fire() (it would be something like handler(listener)(arg) ).

We need to approach this in a slightly roundabout fashion; the modified add method is as follows.

public typealias EventCallback = (T)->(Bool)

    var callbacks = [EventCallback]()

    public func add(
        target:H, 
        _ handler:(H)->(T)->()
    ) {
        callbacks.append(createWeakCallback(target, handler))
    }

We modify the handlers variable and call it callbacks, and it has a different type (T)->Bool - more on that later. In the add method, we create a callback from the arguments passed in. The createWeakCallback is the crux of our new solution.

func createWeakCallback(
        target:H, 
        _ handler:(H)->(T)->()
    ) -> EventCallback {
        return {
            [weak target]
            (args:T) in
            if let t = target {
                let f = handler(t)
                f(args)
                return true
            }
            return false
        }
    }


This function does a couple of things. First of all, this returns another function of type (T)->Bool which gets stored in the callbacks array. The returned function works as follows:

  • It captures a weak reference to the target object that needs to be notified on the event. The line [weak target] does this.
  • The function then checks if the captured variable is not nil. If it is it returns false; else it will invoke the handler function with the args variable which is of type T.

So essentially the function returned by createWeakCallback can be used to (1) notify the listener, and (2) understand if the callback itself is referencing a handler that is not deallocated.

We can use this information to modify the fire method.

// loops through all the callbacks
    // invokes them, and if they returned
    // false, it means that the callback
    // is referencing a weak variable that
    // is now nil
    public func fire(args:T) {
        var i = 0
        while i != callbacks.count {

            let callback = callbacks[i]

            if callback(args) {
                // since this callback succeeded, move
                // on to the next callback in the list
                i++
            }
            else {
                // housekeeping - since the callback
                // did not actually notify anything
                // no point in having it around, so
                // let's remove it from our list
                // to avoid future processing

                // we need to save it to a temp
                // variable because removeAtIndex returns
                // the item that was removed, and since
                // the item in our case is a function
                // we cannot have a statement in Swift
                // that is a unused function reference

                let removed = callbacks.removeAtIndex(i)
            }
        }
    }

Of course, if we do not wish to perform housekeeping to remove unwanted callbacks, we can simply use the following version instead.

public func fire(arg:T) {
        for callback in callbacks {
            callback(arg)
        }       
    }

Its simply a tradeoff between space and time; and solely depends on the use case. In fact, we could event create another function that returns whether a weak reference is nil or not similar to the createWeakCallback method, and use that in a separate housekeeping function that can be called separately outside the event processing cycle. This is left as an exercise to the reader.

The final Event class in its entirety.

public class Event<T> {

    public init() {}

    typealias EventCallback = (T)->(Bool)
    var callbacks = [EventCallback]()

    func createWeakCallback(target:H, _ handler:(H)->(T)->()) -> EventCallback {
        return {
            [weak target]
            (args:T) in
            if let t = target {
                let f = handler(t)
                f(args)
                return true
            }
            return false
        }
    }

    public func add(target:H, _ handler:(H)->(T)->()) {
        callbacks.append(createWeakCallback(target, handler))
    }

    public func fire(args:T) {
        var i = 0
        while i != callbacks.count {
            let callback = callbacks[i]
            if callback(args) {
                i++
            }
            else {
                let removed = callbacks.removeAtIndex(i)
            }
        }
    }

}