-1

I want to keep two properties in sync with Cocoa bindings.

In my code, you can see that I have two classes: A and B. I wish to keep the message values in A and B instances synchronized so that a change in one is reflected in the other. I'm trying to use the bind(_:to:withKeyPath:options:) method of the NSKeyValueBindingCreation informal protocol. I use Swift 4.2 on macOS.

import Cocoa

class A: NSObject {
  @objc dynamic var message = ""
}

class B: NSObject {
  @objc dynamic var message = ""

  init(_ a: A) {
    super.init()
    self.bind(#keyPath(message), to: a, withKeyPath: \.message, options: nil) // compile error
  }
}

I get a compile error in the line where I call bind: cannot convert value of type 'String' to expected argument type 'NSBindingName'. I get the suggestion to wrap the first parameter with NSBindingName(rawValue: ). After applying that, I get the error type of expression is ambiguous without more context for the third parameter.

What am I doing wrong?

mistercake
  • 692
  • 5
  • 14
  • Unclear what the problem is. Just call it. – matt Mar 03 '19 at 05:36
  • Example from my own code: `self.bind(#keyPath(additionalKeys), to: NSUserDefaultsController.shared(), withKeyPath: "values.\(Defaults.additionalKeys)", options: nil)` – matt Mar 03 '19 at 05:41
  • I am mostly interested in how the participating objects and properties are declared, what type they have and so on. – mistercake Mar 03 '19 at 06:04
  • Combine [Cocoa Bindings Programming Topics](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaBindings/CocoaBindings.html#//apple_ref/doc/uid/10000167i) and [Using Key-Value Observing in Swift](https://developer.apple.com/documentation/swift/cocoa_design_patterns/using_key-value_observing_in_swift). – Willeke Mar 03 '19 at 09:55
  • While the first document is helpful in understanding the basics of Cocoa binding, it did not help me to get it to work in Swift. The second page you linked to doesn't mention `bind` at all. – mistercake Mar 03 '19 at 13:10
  • But key value observing is how binding works. It is merely swizzling plus kvo. Is that what your question really is? – matt Mar 03 '19 at 13:34
  • I'm trying to get the `bind` call to work. I looked at your example above and I tried to come up with some simple example code myself, but I can't get it to work. I added what I have so far to the question. I keep thinking that I must be missing something obvious. – mistercake Mar 03 '19 at 13:41
  • My example does work; it comes from a working app. If instead of this very broad vague question you give a real life specific reproducible example of what you wish to do, that might be better. _You_ are the one who is expected to obey https://stackoverflow.com/help/mcve. – matt Mar 03 '19 at 14:28
  • What I wish to do is to keep the `message` values in the `A` and `B` instances synchronized so that a change in one is reflected in the other. I think a minimal example is better than a real life example because people are more likely to find my problem. – mistercake Mar 03 '19 at 14:39
  • @mistercake The shoe is on the other foot. I already showed you a working example (keeps a property coordinated with a value stored in user default). Now it is up to _you_ to show us _your_ code and what _you_ are trying to do. – matt Mar 05 '19 at 18:37
  • Although I already provided both, I have now copied my clarification into the question and rephrased it to make it clearer what I want. I know that the MCVE rules are for examples in the question. I was just saying that examples that meet these criteria would *also* help *me* understand the method better. Your example, while appreciated, is incomplete and did not help me resolve my issue. – mistercake Mar 05 '19 at 20:07
  • You can't bind anything to anything. Is `message` a Cocoa bindings–compatible property? – Willeke Mar 05 '19 at 23:12
  • 2
    Why is so many people down voting his question? – TimTwoToes Mar 06 '19 at 00:56
  • @Willeke I was under the impression that by declaring the properties with `@obj` and `dynamic` and by having the enclosing class inherit from `NSObject`, both of which I have, I would make that possible. Was I mistaken? – mistercake Mar 06 '19 at 04:14
  • Cocoa bindings use KVO. `@obj` and `dynamic` in a subclass of `NSObject` makes the property KVO compliant, not bindable. Use KVO instead of bindings or keep a reference to `a`, override the setter and getter of `B.message`and set/get `a.message`. – Willeke Mar 06 '19 at 09:17
  • @Willeke What makes a property bindable? – mistercake Mar 06 '19 at 17:21
  • Apparrently KVO compliant properties are read-only bindings. – Willeke Mar 06 '19 at 20:29

1 Answers1

0

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.

TimTwoToes
  • 695
  • 5
  • 10
  • `exposeBinding:` is useful only in an Interface Builder palette. Interface Builder palettes can't be used anymore. – Willeke Mar 06 '19 at 09:05
  • If you want to create your own bindings, which is what he want, you must expose them, before you can bind to a property. – TimTwoToes Mar 06 '19 at 09:37
  • No, you don't have to expose them. Does `secondCounter.number` change when `firstCounter.number` is changed? – Willeke Mar 06 '19 at 10:49
  • The documentation of the `bind(_:to:withKeyPath:options:)` function states for the binding parameter "The key path for a property of the receiver previously exposed using the exposeBinding(_:) method.". And yes `secondCounter.number` change when `firstCounter.number` is changed. – TimTwoToes Mar 06 '19 at 11:39
  • 1
    [How Do Bindings Work?](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaBindings/Concepts/HowDoBindingsWork.html#//apple_ref/doc/uid/20002373-194182) states "In most cases you need to use bind:toObject:withKeyPath:options:, and then only when you establish bindings programatically. Use of the unbind: is discussed in Unbinding. The other methods—the class method exposeBinding: and the instance methods exposedBindings and valueClassForBinding:—are useful only in an Interface Builder palette.". – Willeke Mar 06 '19 at 13:17
  • In my test app, `firstCounter.number` follows `secondCounter.number` but `secondCounter.number` doesn't follow `firstCounter.number`. I got the same result with and without `exposeBinding`. – Willeke Mar 06 '19 at 13:21
  • 1
    You are correct on both counts. exposeBinding is uneccessary and you would have to bind secondCounter.number to firstCounter.number to make it synchronise both ways. I'll change the example. This should answer his question now. – TimTwoToes Mar 06 '19 at 16:58
  • @TimTwoToes Thank you for the example. One of the things that I didn't realize before was that I have to call `bind` twice to get the bidirectionality. And my grasp of the key path syntax was a little shaky. Your example helped me a lot. Thanks. – mistercake Mar 09 '19 at 11:16