Search code examples
swiftcocoa-bindingsappkit

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


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?


Solution

  • 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.