8

I am trying to implement the multicast delegate functionality in Swift. In Objective C, we have this excellent implementation

https://github.com/robbiehanson/XMPPFramework/blob/master/Utilities/GCDMulticastDelegate.m

And I have just created this basic functionality:

protocol MyProtocol : class{
    func testString()-> String;
}

class MulticastDelegateNode <T:AnyObject> {
    weak var delegate : T?

    init(object : T){
        self.delegate = object;
    }
}

class MulticastDelegate <T:AnyObject> {
    var delegates = Array<MulticastDelegateNode<T>>()


    func addDelegate(delegate : T){
        var newNode = MulticastDelegateNode(object : delegate);
        delegates.append(newNode);
    }

    func removeDelegate(delegate : AnyObject){
        self.delegates = self.delegates.filter({ (node : MulticastDelegateNode) -> Bool in
            return node.delegate !== delegate;
        });
    }
}

class OP {
    var delegate  = MulticastDelegate<MyProtocol>();

    func process(){
        //...
        //make actions

        //notify the objects!
    }

}

My problem is that it seems I cannot figure out a way to do this:

delegate.testString()

In order to give the command 'testString()' to all delegates that are in the nodes. Can anyone help me with this?

Nate Cook
  • 92,417
  • 32
  • 217
  • 178
csotiriou
  • 5,653
  • 5
  • 36
  • 45
  • If I see it correctly, GCDMulticastDelegate makes heavy use of NSInvocation and forwardInvocation: which are (as far as I know) not available in Swift. GCDMulticastDelegate also uses the fact that in Objective-C you can send *any* method to `id` as long as the method selector is known to the compiler. – Martin R Nov 16 '14 at 16:30
  • Thank, I already knew that. I am not asking for an exact equivalent. My end goal is to construct a reusable multicast delegate pattern that works in Swift. Is there any way to do that? – csotiriou Nov 16 '14 at 18:56
  • Hi. could you solve the problem? – szuniverse Mar 03 '15 at 15:31
  • Try this one: https://github.com/jonasman/MulticastDelegate – João Nunes Dec 30 '15 at 14:21

6 Answers6

9

Swift 3 implementation:

class MulticastDelegate<T> {
    private var delegates = [Weak]()

    func add(_ delegate: T) {
        if Mirror(reflecting: delegate).subjectType is AnyClass {
            delegates.append(Weak(value: delegate as AnyObject))
        } else {
            fatalError("MulticastDelegate does not support value types")
        }
    }

    func remove(_ delegate: T) {
        if type(of: delegate).self is AnyClass {
            delegates.remove(Weak(value: delegate as AnyObject))
        }
    }

    func invoke(_ invocation: (T) -> ()) {
        for (index, delegate) in delegates.enumerated() {
            if let delegate = delegate.value {
                invocation(delegate as! T)
            } else {
                delegates.remove(at: index)
            }
        }
    }
}

private class Weak: Equatable {
    weak var value: AnyObject?

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

private func ==(lhs: Weak, rhs: Weak) -> Bool {
    return lhs.value === rhs.value
}

extension RangeReplaceableCollection where Iterator.Element : Equatable {
    @discardableResult
    mutating func remove(_ element : Iterator.Element) -> Iterator.Element? {
        if let index = self.index(of: element) {
            return self.remove(at: index)
        }
        return nil
    }
}

You can test it with:

protocol SomeDelegate: class {
    func onSomeEvent()
}

class SomeDelegateImpl: SomeDelegate {
    let value: Int

    init(value: Int) {
        self.value = value
    }

    func onSomeEvent() {
        print("Invoking delegate \(value)")
    }
}

let multicastDelegate = MulticastDelegate<SomeDelegate>()

func testInvoke() {
    multicastDelegate.invoke {
        $0.onSomeEvent()
    }
}

print("Adding first delegate.")

let delegate1 = SomeDelegateImpl(value: 1)

multicastDelegate.add(delegate1)

testInvoke()

let delegate2 = SomeDelegateImpl(value: 2)

print("Adding second delegate.")

multicastDelegate.add(delegate2)

testInvoke()

print("Removing first delegate.")
multicastDelegate.remove(delegate1)

testInvoke()

print("Adding third delegate.")

({
    let delegate3 = SomeDelegateImpl(value: 3)
    multicastDelegate.add(delegate3)
    testInvoke()
})()

print("Third delegate is deallocated by ARC.")

testInvoke()

It prints:

Adding first delegate.
Invoking delegate 1.
Adding second delegate.
Invoking delegate 1.
Invoking delegate 2.
Removing first delegate.
Invoking delegate 2.
Adding third delegate.
Invoking delegate 2.
Invoking delegate 3.
Third delegate is deallocated by ARC.
Invoking delegate 2.

Based on this blog post.

mixel
  • 25,177
  • 13
  • 126
  • 165
  • If I call `.remove(self)` in a deinit my app will crash saying can't assign weak. This is due creating a Weak wrapper in order to search for it. I fixed this by doing a `firstIndex(where:` like this: `return delegates.firstIndex(where: { weakRef in if let value = weakRef.value { return value === (delegate as AnyObject) } return false })` – Michael Ozeryansky Oct 04 '19 at 14:27
4

Ok. In some of the solutions I see mistakes (strong retain cycles, race conditions, ...)

Here is what I combine based on 1 day research. For the stack of delegates I used NSHashTable, so all the delegates are having weak reference.

Swift 3.1

class MulticastDelegate <T> {
  private let delegates: NSHashTable<AnyObject> = NSHashTable.weakObjects()

  func add(delegate: T) {
    delegates.add(delegate as AnyObject)
  }

  func remove(delegate: T) {
    for oneDelegate in delegates.allObjects.reversed() {
      if oneDelegate === delegate as AnyObject {
        delegates.remove(oneDelegate)
      }
    }
  }

  func invoke(invocation: (T) -> ()) {
    for delegate in delegates.allObjects.reversed() {
      invocation(delegate as! T)
    }
  }
}

func += <T: AnyObject> (left: MulticastDelegate<T>, right: T) {
  left.add(delegate: right)
}

func -= <T: AnyObject> (left: MulticastDelegate<T>, right: T) {
  left.remove(delegate: right)
}



How to set delegate:

object.delegates.add(delegate: self)



How to execute function on the delegates: instead of

delegate?.delegateFunction

you use

delegates.invoke(invocation: { $0.delegateFunction })
Klemen
  • 2,144
  • 2
  • 23
  • 31
  • 1
    you can go half step further to your desired behaviour by implementing extension on MulticastDelegate where T == SomeDelegateProtocol and implementing all delegate methods by calling self.invoke with desired method. – Tankista Jul 11 '17 at 09:53
3

A Simple demo about MulticastDelegate.

class DelegateMulticast <T> {

  private var delegates = [T]()

  func addDelegate(delegate: T) {
    delegates.append(delegate)
  }

  func invokeDelegates(invocation: (T) -> ()) {
    for delegate in delegates {
        invocation(delegate)
    }
  }
}

protocol MyProtocol {
   func testString() -> String
}


class OP {
var delegates = DelegateMulticast<MyProtocol>()

  func process(){
    delegates.invokeDelegates{
        $0.testString()
    }
  }
}
Zhengjie
  • 451
  • 4
  • 9
  • I'm using this pattern and it works very well! In swift 2.0 i hope we can use this with protocol extensions and simplify the whole thing. – João Nunes Sep 01 '15 at 09:56
  • 10
    Watch out, the `delegates` array in `OP` keeps a strong reference to all its objects, so they stay alive. – Eric Feb 01 '16 at 14:32
  • @Eric so it's the responsibility of the one who uses this class to also remove the observer? Or is there another solution possible? – user1007522 Feb 01 '17 at 14:31
  • Yes. And then it's the responsibility of OP to release the individual delegates. Here's a better solution: https://arielelkin.github.io/articles/objective-c-multicast-delegate – Eric Feb 01 '17 at 18:23
  • @user1007522 there's a problem with this implementation suggested by Eric, the object needs to remove himself as delegate. Check this solution: https://github.com/jonasman/MulticastDelegate – RodolfoAntonici Aug 25 '17 at 13:08
  • You could also change it to a hashtable there you can store weak references. – user1007522 Aug 29 '17 at 09:15
2

Here is my implementation of multicast delegate using Swift 2.0 protocol extensions. Also i've added ability to remove delegates. To do so I've made my delegate type conform to NSObjectProtocol, didn't get how to declare that it should be reference type to use === operator for remove.

protocol MulticastDelegateContainer {

    typealias DelegateType : NSObjectProtocol
    var multicastDelegate  : [DelegateType] {set get}
}

extension MulticastDelegateContainer {

    mutating func addDelegate(delegate : DelegateType) {
        multicastDelegate.append(delegate)
    }

    mutating func removeDelegate(delegate : DelegateType) {
        guard let indexToRemove = self.multicastDelegate.indexOf({(item : DelegateType) -> Bool in
            return item === delegate
        }) else {return}

        multicastDelegate.removeAtIndex(indexToRemove)
    }

    func invokeDelegate(invocation: (DelegateType) -> ()) {
        for delegate in multicastDelegate {
            invocation(delegate)
        }
    }
}

and here is example of usage

@objc protocol MyProtocol : NSObjectProtocol {
    func method()
}


class MyClass : MulticastDelegateContainer {
    typealias DelegateType = MyProtocol
    var multicastDelegate = [MyProtocol]()

    func testDelegates() {
        invokeDelegate { $0.method() }
    }
}
Peter K
  • 438
  • 3
  • 10
  • A problem with this approach is that you responsabilize the delegate object to remove itself as a listener, what can be harmful on the long run with other developers working on the project, please checkout this solution: https://github.com/jonasman/MulticastDelegate – RodolfoAntonici Aug 25 '17 at 13:05
1

I've added my implementation of a Swift multicast delegate on GitHub: https://github.com/tumtumtum/SwiftMulticastDelegate

Basically you use the overloaded operator "=>" with a block to perform the invocation. Internally the MulticastDelegate will call that block for every listener.

class Button
{
  var delegate: MulticastDelegate<ButtonDelegate>?

  func onClick()
  {
    self.delegate => { $0.clicked(self) }
  }  
}
tumtumtum
  • 1,052
  • 9
  • 11
0

You might be able to add

@objc

To your protocol & classes, of course then you are no longer doing pure swift ... but that might solve your issue as it will re-enable dynamic dispatch powers.

Jeef
  • 26,861
  • 21
  • 78
  • 156