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
andValue2
need to be the same for assignmentT
needs to represent the type ofself
- KVC/KVO target properties need to be
@objc
BindMe.init(person:)
needssuper.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 ofself
(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 deinit
s the old observer object.
Related Topics
How to Copy Skspritenode with Skphysicsbody
Swift: Reusable Uiview in Storyboard and Sizing Constraints
Subscript of a Struct Doesn't Set Values When Created as an Implicitly Unwrapped Optional
How to Grant Discoveruserinfowithuserrecordid Permission
Swift Packages and Conflicting Dependencies
How to Turn Off Color Literals in Xcode
How to Override Private Method and Call Super in Swift
Realm: Map JSON to Realm-Objects with Alamofire
How to Convert Data of Int16 Audio Samples to Array of Float Audio Samples
Swift: Visual Glitches When Presenting a Main and Alternative (Login/Onboarding) Flow
Changing Nav Bar Item Programmatically in Swift
Instagram Explorer Searchbar and Tableview
Xcode Error - Undefined Symbols for Architecture X86_64
Shared Cookies with Wkprocesspool for Wkwebview in Swift
How to Add Multiple Values for One Key in a Dictionary Using Swift