I'm trying to implement a simple multi-delegate situation:
protocol Subscribable: class {
associatedtype Subscriber: AnyObject
var subscribers: NSHashTable<Subscriber> { get }
}
protocol ControllerSubscriber: class {
func controllerDidSomething()
}
class Controller: Subscribable {
typealias Subscriber = ControllerSubscriber
var subscribers = NSHashTable<Subscriber>.weakObjects() // Error
}
Error: Using 'ControllerSubscriber' as a concrete type conforming to protocol 'AnyObject' is not supported.
My question is:
And of course, how do I work around this? In the sense of an actual solution not a work around.
I have such a hard time understanding Swift's generics system. I seem to be running into seemingly simple situations like this constantly. I just want to put a thing conforming to a protocol into another thing :( . I would like to know where my thinking goes wrong so I can fix it and never have to see these errors again.
There is this related question but please note the answers give only workarounds, no explanations or solutions.
It is probably not fair to blame this problem on Swift. Reasoning about types seems to be some meta-art we will first have to get used to (unless you have been sitting on the C++ standards committee for the last 30 years that is :-).
Turns out your problem is related to your choice of NSHashTable
as the data structure to hold your subscribers
. The following would compile with minimal changes:
protocol Subscribable: class {
associatedtype Subscriber
var subscribers: [Subscriber?] { get }
}
protocol ControllerSubscriber: class {
func controllerDidSomething()
}
class Controller: Subscribable {
typealias Subscriber = ControllerSubscriber
var subscribers = [Subscriber?]()
}
however, it lacks the weak
semantics and is not really useful yet. The list of subscribers
is exhibited as a property and has to be manipulated directly by the clients. Furthermore each implementation of Subscribable
has to implement its own notification mechanism and there is hardly any logic that is centralised by this approach. Technically you can use it like this:
class Controller: Subscribable {
typealias Subscriber = ControllerSubscriber
var subscribers = [Subscriber?]()
func notify() {
for case let subscriber? in subscribers {
subscriber.controllerDidSomething()
}
}
}
var controller = Controller()
class IWillSubscribe : ControllerSubscriber {
func controllerDidSomething() {
print("I got something")
}
}
controller.subscribers.append(IWillSubscribe())
controller.notify()
but that is neither very practical nor very readable. This would have been an acceptable solution (since it was the only one) up to Java 7, but even in Java 8 (and much more so in Swift) we would like to encapsulate the notification logic into the Subscribable
protocol as a default implementation, but that would be another post.
Since you chose to implement subscribers
as an NSHashTable
(there is probably an ARC reason to desire weak references here) there seems to be some Objective-C trickery involved. After much experimentation (and finally finding the fourth answer to this question I got the following to work:
protocol Subscribable: class {
associatedtype Subscriber : AnyObject
var subscribers: NSHashTable<Subscriber> { get }
}
@objc protocol ControllerSubscriber: class {
func controllerDidSomething()
}
class Controller: Subscribable {
typealias Subscriber = ControllerSubscriber
var subscribers = NSHashTable<Subscriber>.weakObjects()
func notify() {
for subscriber in subscribers.allObjects {
subscriber.controllerDidSomething()
}
}
}
var controller = Controller()
class IWillSubscribe : ControllerSubscriber {
func controllerDidSomething() {
print("I got something")
}
}
let iDoSubscribe = IWillSubscribe()
controller.subscribers.add(iDoSubscribe)
controller.notify()
which is virtually identical to your original (with some proof around it). As it seems Objective-C @protocol
s are not quite the same as Swift protocol
s, but Swift can actually do either.
There is quite a lot of subtlety in this though, only allObjects
works without type erasure, your trusty objectEnumerator
only returns Any?
and that is a stupid animal to get anything from. Also note that
let iDoSubscribe = IWillSubscribe()
is instrumental. At first I tried
controller.subscribers.add(IWillSubscribe())
which actually added something to the count
of subscribers
, but went away with any attempt to iterate (as one should expect from a weak
reference that is not referred to anywhere else).
A very late answer that is already way too long, just to prove that this is still an issue, even with Swift 3. Maybe this will get better once this Jira ticket is resolved.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With