1

I'm defining a class structure that needs to notify its consumers when interesting things happen to it (gets new data, and so on). Similar to a delegate relationship, but there may be many consumers. And a more direct relationship than casting an NSNotification into the wind.

My inclination in most Objective-C programs would just be to define a protocol, and let those consumers implement it and register themselves as id<MyProtocol>; I'd stuff them into an NSMutableSet, which I'd iterate over at opportune moments. My time spent writing C# would incline me to try to modify that approach somewhat to use generics in Swift (a la private var myConsumers = Set<MyProtocol>()). That turns out to be a dark and painful rabbit hole, as far as I can tell. But rather than dive into it, let me back up and try to solve the real problem.

So: I have some class where instances will need to notify 0-N consumers of interesting things that happen. It'd be nice to allow those consumers to register and de-register themselves, since their lifespans may be shorter than my class.

What is the idiomatic Swift approach to implementing this pattern?

Sixten Otto
  • 14,816
  • 3
  • 48
  • 60
  • 1
    Why do you think the `Set()` is a dark and painful rabbit hole? Other than the obvious issue that Set isn't implemented as of 1.1 (it is in 1.2) but easy to solve that by using an Array instead. – David Berry Feb 13 '15 at 16:57
  • I may post another question about that, but it's really beside the point of this one. – Sixten Otto Feb 13 '15 at 17:04
  • Neither Objective-C nor Swift has ever addressed this problem "idiomatically". The Swift creators I talked to on the forum last year didn't even have the concept of why you'd want a multicast delegate; you'll be hard-pressed to get non-C# users to understand a decent solution, like you're describing. I use a wrapper class that loops through an array of closures, and runs each, until a proper language-level solution comes about. –  Feb 13 '15 at 17:05
  • @Jessy an array of closures seems simple enough to implement, but doesn't seem like it'd be possible to selectively de-register when a consumer is going away, yeah? – Sixten Otto Feb 13 '15 at 17:20
  • It should be possible; we should be able to remove closures from a collection. Because this hasn't been implemented (i.e. you can't test closures for equality), we need to wrap the closures in something can be removed from a collection. Check out [this shortsighted answer](http://stackoverflow.com/questions/24111984/how-do-you-test-functions-and-closures-for-equality) from Chris Lattner. –  Feb 13 '15 at 17:40
  • Yeah, that's what I meant: knowing which thing needs to be removed. Even wrapping the closure in some object that can be tested for equality, how would you know which wrapper obj? Wrap the consumer itself in there, too? – Sixten Otto Feb 13 '15 at 17:46

3 Answers3

2

The idiomatic solution to multi-delegations in Cocoa is notifications, specifically using addObserverForName(object:queue:usingBlock:). Pass your closure. Save the return value, and use that return value to unregister yourself in the future.

There are ObjC multicast delegates (Robbie Hanson's is probably my favorite outside of the ones I've implemented), but most of the time its more trouble than its worth. Notifications are how Cocoa manages non-delegate observers.

"Non-delegate" here means "things that are notified, but never asked anything." The problem with "multi-delegate" is that it is really a nonsense term. A "delegate" is something you ask for information; something you "delegate" responsibility for a decision to. Observers aren't really "delegates" (though delegates are sometimes observers). You don't ask them anything. They have no control over decision making. So a multi-delegate is nonsense. How can multiple things all be responsible for answering a question? To make this more concrete, delegates have non void methods. Consider what it would mean to have a "multi-delegate" for UITableViewDelegate. How would you decide what the row height was?

If you are building your own, I would probably recommend Dictionary<Key, ()->Void> where "key" is the handle you return to the caller for later removal. That way you don't have to worry about the craziness of closure equality (which I agree with Chris Lattner is a very open-ended, and possibly unsolvable, question).

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Your points about this not being delegation are well-taken, and you're absolutely right that the case I'm describing is more like "notification" than "delegation", because there's no decision-making involved. – Sixten Otto Feb 13 '15 at 20:29
  • 1
    I'm not sure that I can articulate why `NSNotificationCenter` feels like a weird solution to this for me. It's more aesthetic than technical. There aren't going to be any non-Cocoa Swift programs in the near future, so it's not a "pure Swift" objection. Part of it is that, I think, is that it feels so disconnected (though that has its virtues!). In any event, having to box the consumers (whether protocol objects or naked closures) feels kind of icky. – Sixten Otto Feb 13 '15 at 20:35
  • As I'm primarily into Swift for Metal, I cannot agree with embracing Cocoa hacks just because you're using Swift. NSNotificationCenter is a hack. Swift is wonderful, aside from having no event system. This rationale of embracing the garbage in Cocoa because you're working with OS X/iOS is not going to get us to a point where Swift enables functionality that Objective-C can't. –  Feb 13 '15 at 22:02
0

Here's the most minimal thing I've been able to come up with so far, to emulate MultiCastDelegate.

struct MultiClosure<T, U> {
    private typealias Key = MultiClosureKey
    private let closures: [Key: T -> U]

    init() {closures = Dictionary<Key, T -> U>()}
    init(_ closure: T -> U) {closures = [Key(): closure]}

    private init(_ closures: [Key: T -> U]) {self.closures = closures}

    func run(t: T) {
        for closure in closures.values {closure(t)}
    }
}


private class MultiClosureKey: Hashable {
    // Is this ignorant? I don't know yet.
    // It seems to work so I haven't looked for a better solution.
    var hashValue : Int { return ObjectIdentifier(self).hashValue }
}

private func == (lhs: MultiClosureKey, rhs: MultiClosureKey) -> Bool {
    return lhs === rhs
}



func + <T, U>(left: MultiClosure<T, U>, right: MultiClosure<T, U>) -> MultiClosure<T, U> {
    return MultiClosure(left.closures + right.closures)
}

func += <T, U>(inout left: MultiClosure<T, U>, right: MultiClosure<T, U>) {
    left = left + right
}

func - <T, U>(left: MultiClosure<T, U>, right: MultiClosure<T, U>) -> MultiClosure<T, U> {
    return MultiClosure(left.closures - right.closures)
}

func -= <T, U>(inout left: MultiClosure<T, U>, right: MultiClosure<T, U>) {
    left = left - right
}

Dictionary extensions:

func + <T, U>(var left: Dictionary<T, U>, right: Dictionary<T, U>) -> Dictionary<T, U> {
    for (key, value) in right {
        left[key] = value
    }
    return left
}

func += <T, U>(inout left: Dictionary<T, U>, right: Dictionary<T, U>) {
    left = left + right
}

func - <T, U>(var left: Dictionary<T, U>, right: Dictionary<T, U>) -> Dictionary<T, U> {
    for key in right.keys {
        left[key] = nil
    }
    return left
}

func -= <T, U>(inout left: Dictionary<T, U>, right: Dictionary<T, U>) {
    left = left - right
}
0

I wrote a native Swift notification broadcasting system which enables developers to match a specific notification type by using switch-case syntax and works the same to NSNotificationCenter.

I'm now handling notifications like:

func handleNotification(notification: PrimitiveNotificationType) {  
    switch notification {
    case let aNotification as CustomNotification:
        let initialFrame = aNotification.initialFrame
        let finalFrame = aNotification.finalFrame
        print("Initial frame: \(initialFrame)")
        print("Final frame: \(finalFrame)")
    default: return
    }
}

Here is the post: https://wezzard.com/2015/08/08/notification-handling-best-practice-in-swift/

Here is the code: https://github.com/WeZZard/Nest

Plus, these stuffs works only on Swift 2.

WeZZard
  • 3,536
  • 1
  • 23
  • 26