I find myself having trouble making code generic when using class-constrained protocols in contexts requiring AnyObject. Since a protocol used as a type does not conform to the protocols from which it inherits, the code snippets below do not compile.
A couple quick examples:
If we want to box an arbitrary value to hold a reference weakly, the boxed type must conform to AnyObject, but this prevents us from boxing instances class-constrained protocols:
struct WeakBox<T: AnyObject> {
weak var value: T?
}
protocol ClassConstrained: AnyObject { }
let x: WeakBox<ClassConstrained> // nope!
We encounter a similar issue with a simple wrapper around a protocol-based observation subscription model:
Details
protocol Observable {
associatedtype Observer: AnyObject
var observers: [ObjectIdentifier: Observer] { get set }
}
extension Observable {
mutating func addObserver(_ observer: Observer) {
let id = ObjectIdentifier(observer)
observers[id] = observer // (we'd toss some weak-boxing in here too)
}
}
protocol MyClassObserver: AnyObject {
func myClassDidSomething(_ instance: MyClass)
}
class MyClass: Observable {
typealias Observer = MyClassObserver // nope!
var observers: [ObjectIdentifier: MyClassObserver]
}
Is there a straightforward way to be able to use a class-constrained protocol as a type in contexts where a class is expected? We know any instance conforming to such a protocol will be a class, so it doesn't seem unreasonable—but I can't come with up anything nice that both works in a generic context and retains type safety.
It's possible to hide dynamic casting by limiting the API surface, but this still doesn't seem ideal:
struct WeakBox<T> {
private weak var _value: AnyObject?
var value: T? {
return _value as? T
}
init(_ value: T) {
self._value = value as AnyObject
}
}
AnyObject is a protocol to which all classes implicitly conform.
Not sure if this is what you were looking for, if MyObserver is your class you could simply have it as
class MyObserver {} //It would implicitly conform to AnyObject
class ObservedClass: Observable {
typealias Observer = MyObserver
var observers = [ObjectIdentifier: Observer]()
}
Sorry, perhaps I didn't provide enough detail. The idea is that MyObserver is some protocol which functions something like a delegate would, but the observed object holds a dictionary of observers to be able to notify more than just one. Something like this:
This works, but at the cost of some type safety. If I want to call observer.myClassDidSomething for each observer in MyClass, I must dynamically cast the values in the dictionary to MyClassObserver instances.
Then we lose flexibility since the model is no longer generic. The approach doesn't scale; Observable wouldn't work for MyOtherClass with MyOtherClassObserver.
Your two suggestions summarize the challenge here: how do we retain both type safety and extensibility?
To take the focus off the observation API, here's another example of the underlying challenge.
If we want to box an arbitrary value to hold a reference weakly, the boxed type must conform to AnyObject, but this prevents us from boxing instances class-constrained protocols.
struct WeakBox<T: AnyObject> {
weak var value: T?
}
protocol ClassConstrained: AnyObject { }
let x: WeakBox<ClassConstrained> // nope!
The problem is, Swift doesn't allow you to create an instance of a generic with a protocol instead of a concrete type, if this generic's type-parameter is also constrained with a protocol. For example, you can not do this:
protocol Base {
}
protocol Derived: Base {
}
struct MyStruct<T: Base> {
}
var myStruct = MyStruct<Derived>()//ERROR: Using 'Derived' as a concrete type conforming to protocol 'Base' is not supported
This is because it opens up the doors for you to declare a static func on Base and then try to call it from inside of MyStruct type:
Now, the code above is okay when you just use it with a concrete type:
struct Concrete: Base {
}
var myStruct = MyStruct<Concrete>
myStruct.myFunc()
But when you use it with a protocol it is not okay:
var myStruct = MyStruct<Derived>
myStruct.myFunc()//not okay
, because Swift doesn't know how to call function .baseFunc on protocol Derived - because it does not have any particular Metatype to do so.
PS The workaround for this I'm aware of is to declare your protocols as @objc. It will unblock you to run this, but only unless you use static members in the protocols - this won't work in any case