Binding 2 Properties (Observe) Using Keypath

Binding 2 properties (observe) using keyPath

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

How to keep two properties in sync using bind(_:to:withKeyPath:options:)?

I made the following example in a playground. Instead of class A and B, I used a Counter class since it is more descriptive and easier to understand.

import Cocoa

class Counter: NSObject {
// Number we want to bind
@objc dynamic var number: Int

override init() {
number = 0
super.init()
}
}

// Create two counters and bind the number of the second counter to the number of the first counter
let firstCounter = Counter()
let secondCounter = Counter()

// You can do this in the constructor. This is for illustration purposes.
firstCounter.bind(NSBindingName(rawValue: #keyPath(Counter.number)), to: secondCounter, withKeyPath: #keyPath(Counter.number), options: nil)
secondCounter.bind(NSBindingName(rawValue: #keyPath(Counter.number)), to: firstCounter, withKeyPath: #keyPath(Counter.number), options: nil)

secondCounter.number = 10
firstCounter.number // Outputs 10
secondCounter.number // Outputs 10
firstCounter.number = 60
firstCounter.number // Outputs 60
secondCounter.number // Outputs 60

Normally bindings are used to bind values between your interface and a controller, or between controller objects, or between controller object and your model objects. They are designed to remove glue code between your interface and your data model.

If you only want to keep values between your own objects in sync, I suggest you use Key-Value Observing instead. It has more benefits and it is easier. While NSView and NSViewController manages bindings for you, you must unbind your own objects, before they are deallocated, because the binding object keeps a weak reference to the other object. This is handled more gracefully with KVO.

Take a look at WWDC2017 Session 212 - What's New in Foundation. It shows how to use key paths and KVO in a modern application.

How can I update this class to support binding a keypath value to a function?

This Bindable approach expects there to be some observing object, but you don't have one. That said, it doesn't actually care what that object is. It's just something passed back to the handler. So you could handle this in an extension this way, by using self as a placeholder object:

func bind<T>(_ sourceKeyPath: KeyPath<Value, T>, onNext: @escaping (T) -> Void) {
addObservation(for: self) { object, observed in
let value = observed[keyPath: sourceKeyPath]
onNext(value)
}
}

That said, this feels a little messy, so I might redesign Bindable to support this natively, and build object binding on top of it. Make the private addObservation do a bit less, by just calling a handler:

private extension Bindable {
func addObservation(handler: @escaping (Value) -> Bool) { // <== Require a bool here
lastValue.map { handler($0) }

observations.append { handler($0) } // <== Just call the handler
}
}

And make all the public methods do a bit more checking about object, so the private extension doesn't have to know about it.:

extension Bindable {
// Non Optionals
func bind<O: AnyObject, T>(_ sourceKeyPath: KeyPath<Value, T>, to object: O, _ objectKeyPath: ReferenceWritableKeyPath<O, T>) {
addObservation { [weak object] observed in
guard let object = object else { return false } // <== Have to check object here instead
let value = observed[keyPath: sourceKeyPath]
object[keyPath: objectKeyPath] = value
return true
}
}

// Optionals
func bind<O: AnyObject, T>(_ sourceKeyPath: KeyPath<Value, T>, to object: O, _ objectKeyPath: ReferenceWritableKeyPath<O, T?>) {
addObservation { [weak object] observed in
guard let object = object else { return false }
let value = observed[keyPath: sourceKeyPath]
object[keyPath: objectKeyPath] = value
return true
}
}

// Function
func bind<T>(_ sourceKeyPath: KeyPath<Value, T>, onNext: @escaping (T) -> Void) {
addObservation { observed in
let value = observed[keyPath: sourceKeyPath]
onNext(value)
return true
}
}
}

There's probably some more refactoring you could do here to reduce some of the code duplication, but the key point would be to make the primitive handler do less.

Note that in iOS 13+, this should be done with Combine instead. It does this all in a more powerful way.

Swift compiler segmentation fault using `ReferenceWritableKeyPath `

The problem is that you attempt to use != with the generic type Value, which does not necessarily have == and != implementations. Replacing <Value> by <Value: Equatable> solves it.

Having said that, the compiler crashing with a segmentation fault is always a bug, regardless of whether your code is correct or not. You should consider filing a bug report at https://bugs.swift.org if you have time.

How can I use Key-Value Observing with Smart KeyPaths in Swift 4?

As for your initial code, here's what it should look like:

class myArrayController: NSArrayController {
private var mySub: Any? = nil

required init?(coder: NSCoder) {
super.init(coder: coder)

self.mySub = self.observe(\.content, options: [.new]) { object, change in
debugPrint("Observed a change to", object.content)
}
}
}

The observe(...) function returns a transient observer whose lifetime indicates how long you'll receive notifications for. If the returned observer is deinit'd, you will no longer receive notifications. In your case, you never retained the object so it died right after the method scope.

In addition, to manually stop observing, just set mySub to nil, which implicitly deinits the old observer object.



Related Topics



Leave a reply



Submit