Using class-constrained protocol as AnyObject

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
    }
}

Any suggestions?

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:

protocol MyClassObserver: AnyObject {
    func myClassDidSomething(_ instance: MyClass)
}

class MyClass: Observable {
   typealias Observer = MyClassObserver
   var observers: [ObjectIdentifier: MyClassObserver]
}

I've updated the OP with this example for clarity.

I could be wrong, but I think you don't need an associated type for this scenario.

protocol MyClassObserver: AnyObject {
    func myClassDidSomething(_ instance: MyClass)
}

protocol Observable {
    var observers: [ObjectIdentifier: AnyObject] { get set }
}

extension Observable {
    mutating func addObserver(_ observer: AnyObject) {
        let id = ObjectIdentifier(observer)
        observers[id] = observer // (we'd toss some weak-boxing in here too)
    }
}

class MyClass : Observable {
    var observers: [ObjectIdentifier : AnyObject] = [ObjectIdentifier : MyClassObserver]()
}

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 you could just make that as a requirement in Observable.

The change is Observable protocol requires observers to be of the type [ObjectIdentifier: MyClassObserver]

protocol MyClassObserver: AnyObject {
    func myClassDidSomething(_ instance: MyClass)
}

protocol Observable {
    var observers: [ObjectIdentifier: MyClassObserver] { get set }
}

extension Observable {
    mutating func addObserver(_ observer: MyClassObserver) {
        let id = ObjectIdentifier(observer)
        observers[id] = observer // (we'd toss some weak-boxing in here too)
    }
}

class MyClass : Observable {
    var observers = [ObjectIdentifier : MyClassObserver]()
}

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!
1 Like

This is a fair point, not sure why the compiler couldn't infer anything conforming to ClassConstrained would also conform to AnyObject

This is because class constrained protocols are not yet fully implemented.

1 Like

Thanks a lot @Nobody1707 for the clarification was a bit confused

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:

protocol Base {
    static func baseFunc()
}

struct MyStruct<T: Base> {
    func myFunc() {
        T.baseFunc()
    }
}

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

I'm not sure what you mean. They certainly are implemented. Protocols do not conform to other protocols though, but that is a separate issue.

Sorry, I don't know what I was thinking there.

Terms of Service

Privacy Policy

Cookie Policy