Swift events: almost generic, almost solution(s)

man wearing black and white stripe shirt looking at white printer papers on the wall

Fotografie de Startup Stock Photos pe Pexels.com

I have been thinking about a possible better/generic solution for adding multi-cast delegate/event dispatching support to Swift class hierarchies since January, i.e. since more than 6 months ago.

While in my opinion the solution below is still not better than the one I proposed back then, being less type safe unfortunately, I wanted to post it anyway, maybe it could help somebody who cares more than me about avoiding boilerplate code (or until first class event entities would eventually reach us in a following Swift update, who knows.)

Disclaimer: I know (and I already mentioned in that earlier post) that there is already a pretty good generic solution proposed by Greg Read, but I struggled to find something without AnyObject internal checks. Note that applying a type condition like T: AnyObject in his code does work but introduces new issues – try it yourself – as Swift would then not allow the delegates variable definition within the client class anymore even if you’d add a Delegate: class clause too – it really needs a class type, not a protocol type then! (Moreover, of course, his WeakWrapper class can also be changed to a struct and some parts of the code need to be adapted a bit to work with Swift 4, but these are side stories.)

So let’s move back a bit, and try something else:

class EventDispatcher {
    func addHandler(_ newHandler: EventHandler) {
        handlers.append(EventHandlerReference(object: newHandler))
    }
    func removeHandler(_ oldHandler: EventHandler) {
        guard let index = handlers.index(where: { handler in handler.object === oldHandler }) else { return }
        handlers.remove(at: index)
    }
    func raise(_ argument: Any) {
        for handler in handlers {
            handler.object?.handle(argument)
        }
    }
    private var handlers = [EventHandlerReference]()
    private struct EventHandlerReference {
        weak var object: EventHandler?
    }
    deinit {
        print("Dispatcher deinit")
    }
}
protocol EventHandler: class {
    func handle(_ argument: Any)
}

As you can see the idea is to extract adding, removing, and raising events (i.e. managing and calling handler delegates) into a Dispatcher class, to be instanced by each service, therefore without having to duplicate the internal code in the service classes themselves. Similar, somewhat, to Greg’s solution, but in our case the EventHandler protocol is not service-specific anymore.

Of course, the private wrapper class (EventHandlerReference) is needed to actually store weak delegate references so that all objects (clients and services) get properly deinitialized eventually – considering Swift‘s ARC context – similarly to the way we do it in the non-generic version (and to the way Greg does it as well).

Update: With Xcode 10+, you can now directly remove array items that respect a predicate, so the handler removal is now easier:

func removeHandler(_ oldHandler: EventHandler) { 
  handlers.removeAll(where: { handler in handler.object === oldHandler })
}

The main problem with this approach, however, is that EventHandler protocol must not be generic, i.e. it simply can’t have an Argument associated type or otherwise we couldn’t have a list of handlers stored at the dispatcher level (Swift is not C#!)

The only possible solution I could think of in order to get something running, though, was to define a common protocol for the arguments passed when raising and handling events, or, why not?, just use the most simple existing one: Any!

Services can then raise events through a dispatcher object that they would initialize themselves, and clients could handle them by simply implementing the EventHandler protocol:

class Service {
    init(_ name: String) {
        self.name = name
    }
    var name: String
    var dispatcher = EventDispatcher()
    func doSomething(_ info: String) {
        dispatcher.raise(EventType.somethingWasDone(info: info, source: self))
    }
    enum EventType {
        case somethingWasDone(info: String, source: Service)
    }
    deinit {
        print(name, "deinit")
    }
}
class Client: EventHandler {
    init(_ name: String, subscribingTo service: Service) {
        self.name = name
        self.service = service
        service.dispatcher.addHandler(self)
    }
    var name: String
    var service: Service
    func handle(_ argument: Any) {
        guard let type = argument as? Service.EventType else { return }
        switch (type) {
        case .somethingWasDone(let info, let source):
            print(self.name, "observes that something was done about", info, "on", source.name)
        }
    }
    deinit {
        print(name, "deinit")
    }
}

As you can see, the service class can define a custom enum (or any other type) for specifying the event types that it wants to be able to raise, and clients could handle them using switch statements (or otherwise checking the argument values they receive) in their handler method. However, since Swift enum values can also have supplemental arguments themselves, event related information and/or the source of event (the service instance that raises it) can be easily passed to the handler together with the event type.

But in my opinion, an ugly thing must, unfortunately, be done by the client developer anyway: narrow down the event argument value to the known type (e.g. Service1.EventType). As if this was a (too generic) message bus implementation!

let type = argument as? Service.EventType

This line is the one I don’t like at all, and it’s mostly why I think I’d keep using the January (non-generic) solution, myself; besides being more expressive, it’s way more type safe.

But if you want to use the generic idea in your application or framework, don’t forget to remove all deinitializers from the code first – I’ve added them just to be able to check that we don’t get any unexpected reference cycles formed.

For reference purposes – maybe to determine whether or not you can use it – here is some sample usage code and screen output below:

func run() {
    let service1 = Service("S1")
    let service2 = Service("S2")
    let client1 = Client("C1", subscribingTo: service1)
    let client2 = Client("C2", subscribingTo: service2)
    service1.doSomething("a") // C1 handles somethingWasDone("a") for S1
    service1.dispatcher.addHandler(client2)
    service2.dispatcher.addHandler(client1)
    service2.doSomething("b") // C1 and C2 handle somethingWasDone("b") for S2
    service2.dispatcher.removeHandler(client1)
    service2.doSomething("c") // C2 handles somethingWasDone("c") for S2
}
run()
C1 observes that something was done about a on S1
C2 observes that something was done about b on S2
C1 observes that something was done about b on S2
C2 observes that something was done about c on S2
C2 deinit
C1 deinit
S2 deinit
Dispatcher deinit
S1 deinit
Dispatcher deinit

Finally, I must say that there is yet another – more generic/type safe, but not surely better – solution (which I’ve reached to after reading some good StackOverflow debates on miscellaneous Swift amazing features and… amazing lack of features), requiring client developers to define closure typed variables (lazy if they need access to self from within the code) as event handlers. Personally I don’t like this solution too much either because it needs capturing either unowned self or field values of the current client object in order to avoid ARC traps, but again, maybe it is useful for somebody who does both service and client development within an app, rather than publishing a framework:

class Event<Argument> {
    func addHandler(_ newHandler: EventHandler<Argument>) {
        handlers.append(EventHandlerReference(object: newHandler))
    }
    func removeHandler(_ oldHandler: EventHandler<Argument>) {
        guard let index = handlers.index(where: { handler in handler.object === oldHandler }) else { return }
        handlers.remove(at: index)
    }
    func raise(_ argument: Argument) {
        for handler in handlers {
            handler.object?.function(argument)
        }
    }
    private var handlers = [EventHandlerReference]()
    private struct EventHandlerReference {
        weak var object: EventHandler<Argument>?
    }
    deinit {
        print("Event deinit")
    }
}
class EventHandler<Argument> {
    init(_ function: @escaping (Argument)->Void) {
        self.function = function
    }
    var function: (Argument)->Void
    deinit {
        print("Event handler deinit")
    }
}
class Service {
    init(_ name: String = "default") {
        self.name = name
    }
    var name: String
    func doSomething(_ info: Int) {
        somethingWasDone.raise(info)
    }
    var somethingWasDone = Event<Int>()
    deinit {
        print("Service deinit")
    }
}
class Client {
    init(_ name: String, subscribingTo service: Service) {
        self.name = name
        self.service = service
        service.somethingWasDone.addHandler(somethingWasDoneHandler)
    }
    var name: String
    var service: Service
    lazy var somethingWasDoneHandler = EventHandler<Int> { [name] argument in
         print(name, "observes that something was done about", argument)
    }
    deinit {
        print("Client deinit")
    }
}
func run() {
    let service1 = Service("S1")
    let service2 = Service("S2")
    let client1 = Client("C1", subscribingTo: service1)
    let client2 = Client("C2", subscribingTo: service2)
    service1.doSomething(1)
    service2.somethingWasDone.addHandler(client1.somethingWasDoneHandler)
    service2.doSomething(2)
    service2.somethingWasDone.removeHandler(client1.somethingWasDoneHandler)
    service2.doSomething(3)
}
run()
C1 observes that something was done about 1
C2 observes that something was done about 2
C1 observes that something was done about 2
C2 observes that something was done about 3
Client deinit
Event handler deinit
Client deinit
Event handler deinit
Service deinit
Event deinit
Service deinit
Event deinit
Advertisements

About Sorin Dolha

My passion is software development, but I also like physics.
This entry was posted in Swift and tagged , , , , . Bookmark the permalink.

Add a reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s