Swift events

band blow blur brass

Photo by Mariusz Prusaczyk on Pexels.com

Swift does not have built-in .NET-like events. (I admit, I was used to have them in C#, although implementing the weak pattern in .NET was not the easiest job either.) Instead, in Swift there are two common patterns that developers (and even Apple) use for notifications: delegation and completion handlers.

Delegation involves defining a protocol that is separated from the service class, by convention having “Delegate” name suffix. That protocol would include function declarations for (usually) all notification types that the service object can send to a client. The service needs to hold a delegate reference to be able to call it whenever needed, and in Swift that can be easily set as weak to avoid memory leaks. But if we want to hold an array of delegate references instead of a single one, we just cannot set them as weak because the built-in array type of Swift is a value type (which – by the way – has many advantages in other development areas) and the actual item references are out of our control. In such case, we are forced to write a wrapper type over the delegate reference (to be able to mark it as weak).

At first thought, working with completion handlers (callbacks) instead of delegates seems a good replacement, overcoming some of the issues indicated above. While this would indeed work well for adding and notifying subscribers for each type of notification that we need to support, the first problem with this approach is that we cannot unsubscribe callbacks upon request. Simply because Swift just doesn’t let we compare function references (although functions are reference types) due to internal optimizations that the compiler relies on, so we cannot determine which reference need to be removed from the array of callback references that the service would host. Moreover, we cannot easily overcome memory management issues using weak references because function types are special and one cannot mark a function reference as weak, so to be able to resolve this I guess we would need a wrapper over a weak reference to an object that would have a function reference – and in my opinion that’s too much wrapping!

These being said, what can we then do if we just want to have N objects notifying M observers with memory leak protection (besides allowing manual unsubscribing), and – as a personal requirement – to avoid over-architecturing and increase customizability as much as possible, while (of course) trying to also limit the boilerplate code that would need repeated for each event type to a reasonable amount? (I’ve already seen some generic implementations, but for now I just feel that we could be OK without them.)

I think that we can do it this way (for each service that we develop):

  • Define one (or more) Delegate protocols, defining (and optionally further grouping) functions to represent the supported notification types;
  • To be able to listen to many service instances from a common client, one argument of each delegate function should be a reference to the sender service itself (I suggest always the first);
  • Define add- and remove*Delegate pairs of functions under the service definition (one pair for each Delegate protocol) for clients to be able to subscribe and unsubscribe themselves from the service’s delegate lists;
  • Define private wrapper type(s) over delegate object references marked as weak (one for each Delegate protocol); host and manage array(s) of such wrappers and call their references’ functions whenever notifications are to be sent to subscribers.

Easier said than done? Nah. Below there is a complete example that I play with. We have:

  • a service class (with an extracted protocol) that allows:
    • executing operations:
      • doing something;
      • doing something else;
    • adding and removing associated delegates of these types (separated protocols):
      • main service delegate that allows clients to be notified when:
        • something is about to be done, accepting cancelation orders;
        • something was done;
      • secondary service delegate that allows clients to be notified when:
        • something else was done;
  • a client class that:
    • declares itself both as a main delegate and as a secondary delegate for the service class;
    • subscribes to multiple service instances at initialization time;
    • gets notified whenever, at the level of any of its associated service instances:
      • something is about to be done, sending cancelation orders when needed;
      • something was done;
      • something else was done;
    • supports unsubscribing from any of its associated services upon request;
  • main code that:
    • initializes a few services and clients that subscribe to them;
    • calls doing something and something else on specific services;
    • unsubscribes clients from selected services;
  • and: all code includes print calls to indicate what actually happens when it’s run.

Feel free to try it yourself if you wish, either in xCode as a playground, or – why not – under an online Swift playground service, such as this one on iSwift.org:

protocol ServiceObject: class {
    var name: String { get }
    
    func doSomething(_ argument: String)
    func doSomethingElse(_ argument: String)
    
    func addDelegate(_: ServiceDelegate)
    func removeDelegate(_: ServiceDelegate)
    
    func addSecondaryDelegate(_: ServiceSecondaryDelegate)
    func removeSecondaryDelegate(_: ServiceSecondaryDelegate)
}

protocol ServiceDelegate: class {
    func somethingWillBeDone(_: ServiceObject, _ information: String) -> Bool
    func somethingWasDone(_: ServiceObject, _ information: String)
}

protocol ServiceSecondaryDelegate: class {
    func somethingElseWasDone(_: ServiceObject, _ information: String)
}

class Service: ServiceObject {
    let name: String
    
    init(_ name: String) {
        self.name = name
        
        print("Service", name, "initialized")
    }
    
    func doSomething(_ argument: String) {
        let information = argument
        if somethingWillBeDone(information) {
            print("Doing something about", argument, "on service", name)
            somethingWasDone(information)
        }
    }
    func doSomethingElse(_ argument: String) {
        let information = argument
        print("Doing something else about", argument, "on service", name)
        somethingElseWasDone(information)
    }
    
    func addDelegate(_ newDelegate: ServiceDelegate) {
        delegates.append(Delegate(object: newDelegate))
    }
    func removeDelegate(_ oldDelegate: ServiceDelegate) {
        guard let index = delegates.index(where: { delegate in delegate.object === oldDelegate })
            else { return }
        delegates.remove(at: index)
    }
    private var delegates = [Delegate]()
    private struct Delegate {
        weak var object: ServiceDelegate?
    }
    private func somethingWillBeDone(_ information: String) -> Bool {
        for delegate in delegates {
            if !(delegate.object?.somethingWillBeDone(self, information) ?? true) {
                return false
            }
        }
        return true
    }
    private func somethingWasDone(_ information: String) {
        for delegate in delegates {
            delegate.object?.somethingWasDone(self, information)
        }
    }
    
    func addSecondaryDelegate(_ newDelegate: ServiceSecondaryDelegate) {
        secondaryDelegates.append(SecondaryDelegate(object: newDelegate))
    }
    func removeSecondaryDelegate(_ oldDelegate: ServiceSecondaryDelegate) {
        guard let index = secondaryDelegates.index(where: { delegate in delegate.object === oldDelegate })
            else { return }
        secondaryDelegates.remove(at: index)
    }
    private var secondaryDelegates = [SecondaryDelegate]()
    private struct SecondaryDelegate {
        weak var object: ServiceSecondaryDelegate?
    }
    private func somethingElseWasDone(_ information: String) {
        for delegate in secondaryDelegates {
            delegate.object?.somethingElseWasDone(self, information)
        }
    }
    
    deinit {
        print("Deinitializing service", name)
    }
}

class Client: ServiceDelegate, ServiceSecondaryDelegate {
    let name: String
    let services: [ServiceObject]
    
    init(_ name: String, subscribeTo services: [ServiceObject]) {
        self.name = name
        self.services = services
        
        print("Client", name, "initialized")
        for service in services {
            print("Client", name, "subscribes to service", service.name)
            service.addDelegate(self)
            service.addSecondaryDelegate(self)
        }
    }
    
    func somethingWillBeDone(_ service: ServiceObject, _ information: String) -> Bool {
        print("Client", name, "observes that something about", information, "will be done on service", service.name)
        return information != "canceled"
    }
    func somethingWasDone(_ service: ServiceObject, _ information: String) {
        print("Client", name, "observes that something about", information, "was done on service", service.name)
    }
    func somethingElseWasDone(_ service: ServiceObject, _ information: String) {
        print("Client", name, "observes that something else about", information, "was done on service", service.name)
    }
    
    func unsubscribe(from service: ServiceObject) {
        print("Client", name, "unsubscribes from service", service.name)
        service.removeDelegate(self)
        service.removeSecondaryDelegate(self)
    }
    
    deinit {
        print("Deinitializing client", name)
    }
}

func run() {
    let serviceS = Service("S")
    let serviceT = Service("T")
    let clientC = Client("C", subscribeTo: [ serviceS, serviceT ])
    let clientD = Client("D", subscribeTo: [ serviceT ])
    
    serviceS.doSomething("s1")
    serviceT.doSomething("t1")
    serviceS.doSomething("canceled")
    serviceT.doSomethingElse("et1")
    clientC.unsubscribe(from: serviceT)
    
    serviceS.doSomething("s2")
    serviceT.doSomething("t2")
    serviceT.doSomethingElse("et2")
    clientD.unsubscribe(from: serviceT)
}

run()

Enjoy! And for reference purposes, the console output upon running the code should be:

Service S initialized
Service T initialized
Client C initialized
Client C subscribes to service S
Client C subscribes to service T
Client D initialized
Client D subscribes to service T
Client C observes that something about s1 will be done on service S
Doing something about s1 on service S
Client C observes that something about s1 was done on service S
Client C observes that something about t1 will be done on service T
Client D observes that something about t1 will be done on service T
Doing something about t1 on service T
Client C observes that something about t1 was done on service T
Client D observes that something about t1 was done on service T
Client C observes that something about canceled will be done on service S
Doing something else about et1 on service T
Client C observes that something else about et1 was done on service T
Client D observes that something else about et1 was done on service T
Client C unsubscribes from service T
Client C observes that something about s2 will be done on service S
Doing something about s2 on service S
Client C observes that something about s2 was done on service S
Client D observes that something about t2 will be done on service T
Doing something about t2 on service T
Client D observes that something about t2 was done on service T
Doing something else about et2 on service T
Client D observes that something else about et2 was done on service T
Client D unsubscribes from service T
Deinitializing client D
Deinitializing client C
Deinitializing service T
Deinitializing service S

Note that in this example the clients do not access the service except by adding and removing themselves as event subscribers, and instead it’s the main code that calls the service operations (just for the code to be easier to understand) but in practice clients can (and often will) call the services themselves; just that usually other clients will listen.

Another thing that you may have spotted is that I don’t clean up weak reference wrapper collections upon calling the delegates from within the service class, nor upon subscribing or unsubscribing delegates in the neighbourhood: I just let those nils in as I think the performance would (most of the time) be better this way and that the code is also more elegant – and shorter – without the repeated filtering that would be otherwise needed.

Finally, I just want to add that the (verbose) approach for multi-casting events that I proposed above for Swift can actually be implemented, following the same concept line, with virtually any other modern programming language and/or on multiple platforms, even when they do have built-in events, like C# and .NET do. Doing so would improve customizability on raising those events, and furthermore it would help avoiding memory leaks, assuming that the language is similar and that it offers a handy weak keyword or that the target platform or framework provides a public WeakReference class that does what we want – for C#, for example, see also this.

(Actually, this platform agnosticism itself is an important reason leading me towards selecting a simple, yet verbose solution vs. a more complex, generic event framework.)

And if you just have more simple situations – like 1-N, N-1, or 1-1 observable-observer pairs, you can just adapt some of the aspects of the code above (such as defining a single notification delegate per service, a single weak delegate reference instead of an array of weak reference wrappers, and/or removing the sender service argument within notification calls), accordingly.

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

func removeDelegate(_ oldDelegate: ServiceDelegate) { 
  delegates.removeAll(where: { delegate in delegate.object === oldDelegate })
}

Update: If you insist, we can extract the delegate collection as a generic type (may be struct!), which you can later use with more ease from your event dispatching services. But do note that (unless you accept adding generic delegate arguments to your service class, which would be “creepying” it up on its own) it would require using AnyObject for the inner weak reference since adding a where class clause for the type parameter would just not work. If that (see below) doesn’t bother you, then maybe you could instead go fully generic, like here (with some caveats, though – do read the entire blog post). In my opinion, however, both ways seem to require too much abstraction over too little gain (i.e. they won’t improve overall code readability/maintainability that much.)

struct DelegateCollection<Delegate> {
    mutating func add(_ newDelegate: Delegate) {
        delegateReferences.append(DelegateReference(newDelegate))
    }
    mutating func remove(_ oldDelegate: Delegate) {
        guard let index = delegateReferences.index(where: { reference in reference.pointsTo(oldDelegate) })
            else { return }
        delegateReferences.remove(at: index)
    }
    func call(_ function: (Delegate) -> Void) {
        for reference in delegateReferences {
            if let delegate = reference.delegate {
                function(delegate)
            }
        }
    }
    private var delegateReferences: [DelegateReference] = []
    private struct DelegateReference {
        init(_ delegate: Delegate) {
            object = delegate as AnyObject
        }
        private weak var object: AnyObject?
        var delegate: Delegate? {
            return object as? Delegate
        }
        func pointsTo(_ delegate: Delegate) -> Bool {
            return object === delegate as AnyObject
        }
    }
}

Here is a usage example (within the previously defined Service class) – indeed it’s a little cleaner, at the high cost of having a complex infrastructure DelegateCollection type, unfortunately:

func addDelegate(_ newDelegate: ServiceDelegate) {
    delegates.add(newDelegate)
}
func removeDelegate(_ oldDelegate: ServiceDelegate) {
    delegates.remove(oldDelegate)
}
private var delegates = DelegateCollection<ServiceDelegate>()
private func somethingWasDone(_ information: String) {
    delegates.call { delegate in delegate.somethingWasDone(self, information) }
}
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.

4 Responses to Swift events

  1. Pingback: Asynchronously awaiting for Swift await | Code {Sections}

  2. Pingback: Swift impressions | Code {Sections}

  3. Pingback: Testing Swift deinit | Code {Sections}

  4. Pingback: Swift events: almost generic, almost solution | Code {Sections}

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