Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

KVObserving a protocol in Swift 4

I am struggling to use the new strongly-typed KVO syntax in Swift 4 to observe properties that are only visible through a protocol:

import Cocoa

@objc protocol Observable: class {
    var bar: Int { get }
}

@objc class Foo: NSObject, Observable {
    @objc dynamic var bar = 42
}

let implementation = Foo()

let observable: Observable = implementation

let observation = observable.observe(\.bar, options: .new) { _, change in
    guard let newValue = change.newValue else { return }

    print(newValue)
}

implementation.bar = 50

error: value of type 'Observable' has no member 'observe'
let observation = observable.observe(\.bar, options: .new) { _, change in

Clearly, Observable is not an NSObject. But I cannot simply cast it to NSObject, because the type of the keypath will not match the type of the object.

I tried being more explicit about the type:

let observable: NSObject & Observable = implementation

But:

error: member 'observe' cannot be used on value of protocol type 'NSObject & Observable'; use a generic constraint instead
let observation = observable.observe(\.bar, options: .new) { _, change in

Is what I am trying to do not possible? This seems a common use case. It is easily done with old #keypath syntax. Can you offer any alternatives? Thanks.

like image 222
proxi Avatar asked Nov 09 '17 10:11

proxi


1 Answers

This code compiles and runs in Swift 4.1.2 (Xcode 9.4):

import Foundation

@objc protocol Observable: AnyObject {
    var bar: Int { get }
}

@objc class Foo: NSObject, Observable {
    @objc dynamic var bar = 42
}

let implementation = Foo()

let observable: NSObject & Observable = implementation

func observeWrapper<T: NSObject & Observable>(_ object: T) -> NSKeyValueObservation {
    return object.observe(\.bar, options: .new) { _, change in
        guard let newValue = change.newValue else { return }
        print(newValue)
    }
}

let observation = observeWrapper(observable)
implementation.bar = 50
withExtendedLifetime(observation, {})

All I did is wrap the call to observe in a generic function that's constrained to NSObject & Observable.

Despite the introduced generic, it’s still possible to pass the protocol-typed observable to this new function. To be honest, I can't really explain why this works.

Edit: This might explain it: protocols in Swift generally don't conform to themselves, so it wouldn't be allowed to call a generic function with an existential (a protocol type) even if the constraints match. But there's an exception for @objc protocols (without static requirements), which do conform to themselves. I suppose this works because Observable is marked @objc.

like image 144
Ole Begemann Avatar answered Sep 28 '22 01:09

Ole Begemann