Binding 2 properties (observe) using keyPath

2019-04-02 04:23发布

问题:

I am trying to create a routine to simplify binding one property to another, a very common operation. I'm using the block based KVO's in Swift 4 and XCode 9.

I want to be able to write the following to bind two variables using their corresponding keyPath:

self.bind(to: \BindMe.myFirstName, from: \BindMe.person.firstName )

This is a simplified example that is generating various compile errors that I can't get around. It is likely the incorrect passing of the keyPath to func bind, but the setValue using a keyPath also fails to compile. Please see the comments in the code for the compile errors I'm getting.

class Person : NSObject
{
    init( firstName:String, lastName:String )
    {
        self.firstName  = firstName
        self.lastName   = lastName
    }

    @objc dynamic var firstName:String
    @objc dynamic var lastName:String
}

class BindMe : NSObject
{
    var observers   = [NSKeyValueObservation]()
    let person:Person

    var myFirstName:String = "<no first name>"
    var myLastName:String  = "<no last name>"

    init( person:Person )
    {
        self.person = person
        self.setupBindings()
    }

    func setupBindings()
    {
        self.bind(to: \BindMe.myFirstName, from: \BindMe.person.firstName )
        self.bind(to: \BindMe.myLastName,  from: \BindMe.person.lastName )
    }

    // this func declaration is likely incorrect
    func bind<T,Value,Value2>( to targetKeyPath:KeyPath<T,Value>, from sourceKeyPath:KeyPath<T,Value2>)
    {
        // Error: Generic parameter 'Value' could not be inferred
        self.observers.append( self.observe(sourceKeyPath, options: [.initial,.new], changeHandler: { (object, change) in

            // Error: Cannot convert value of type 'KeyPath<T, Value>' to expected argument type 'String'
            self.setValue(change.newValue, forKeyPath: targetKeyPath)
        }))
    }
}

EDIT

The answer below helps with the initial compile problems. However, for this to be useful at all I need to be able to push the plumbing into a superclass as shown here. This will make the class using it very simple, but I am still struggling with compile errors:

Cannot invoke 'bind' with an argument list of type '(to: WritableKeyPath<PersonWatcher, PersonWatcher>, from: WritableKeyPath<PersonWatcher, PersonWatcher>)'

If I pass a generic type T to the bind routine, I get this error instead:

Type 'BindBase' has no subscript members

class BindBase :NSObject
{
    var observers = [NSKeyValueObservation]()

    func bind<Value>(to targetKeyPath: ReferenceWritableKeyPath<BindBase, Value>, from sourceKeyPath: KeyPath<BindBase, Value>)
    {
        self.observers.append(self.observe(sourceKeyPath, options: [.initial, .new], changeHandler: { (object, change) in
            self[keyPath: targetKeyPath] = change.newValue!
        }))
    }
}

class PersonWatcher : BindBase
{
    @objc dynamic var person: Person

    @objc var myFirstName: String = "<no first name>"
    @objc var myLastName:  String = "<no last name>"

    init(person: Person) {
        self.person = person
        super.init()

        self.bind(to: \PersonWatcher.myFirstName, from: \PersonWatcher.person.firstName)
        self.bind(to: \PersonWatcher.myLastName,  from: \PersonWatcher.person.lastName)
    }
}

回答1:

According to the accepted proposal SE-0161 Smart KeyPaths: Better Key-Value Coding for Swift, you need to use ReferenceWritableKeyPath to write a value to the key path for an object with reference semantics, using subscript.

(You need to pass a classic String-based key path to setValue(_:forKeyPath:), not KeyPath.)

And some more:

  • Value and Value2 need to be the same for assignment
  • T needs to represent the type of self
  • KVC/KVO target properties need to be @objc
  • BindMe.init(person:) needs super.init()

So, your BindMe would be something like this:

class BindMe: NSObject {
    var observers = [NSKeyValueObservation]()
    @objc let person: Person

    @objc var myFirstName: String = "<no first name>"
    @objc var myLastName: String  = "<no last name>"

    init(person: Person) {
        self.person = person
        super.init()
        self.setupBindings()
    }

    func setupBindings() {
        self.bind(to: \BindMe.myFirstName, from: \BindMe.person.firstName)
        self.bind(to: \BindMe.myLastName,  from: \BindMe.person.lastName)
    }

    func bind<Value>(to targetKeyPath: ReferenceWritableKeyPath<BindMe, Value>, from sourceKeyPath: KeyPath<BindMe, Value>) {
        self.observers.append(self.observe(sourceKeyPath, options: [.initial, .new], changeHandler: { (object, change) in
            self[keyPath: targetKeyPath] = change.newValue!
        }))
    }
}

For EDIT:

The demand to make a BindBase like thing seems very reasonable, so I have made some tries.

To fulfill

  • T needs to represent the type of self

(where T == KeyPath.Root), using Self would be the most instinctive, but unfortunately, its usage is still very restricted in the current version of Swift.

You can move the definition of bind into a protocol extension using Self:

class BindBase: NSObject, Bindable {
    var observers = [NSKeyValueObservation]()
}

protocol Bindable: class {
    var observers: [NSKeyValueObservation] {get set}
}

extension Bindable {
    func bind<Value>(to targetKeyPath: ReferenceWritableKeyPath<Self, Value>, from sourceKeyPath: KeyPath<Self, Value>)
    where Self: NSObject
    {
        let observer = self.observe(sourceKeyPath, options: [.initial, .new]) {object, change in
            self[keyPath: targetKeyPath] = change.newValue!
        }
        self.observers.append(observer)
    }
}


标签: swift swift4