Do you need help on a specific subject? Use the contact form (Request a blog entry) on the right hand side.

2015-11-26

Swift Example: Using Cocoa Bindings to connect a NSTextField to a String.

Bindings are a neat way to wire up a GUI. They connect a GUI element to a variable in such a way that updates to the interface are reflected in the variable, and updates to the variable are reflected in the interface. It is a powerful concept, but there are a few snags along the way.
In the MVC parlour, a binding can (up to a point) replace a Controller. You will probably not want to go that far, but at least for simple GUI's it is possible (as we shall see in this example).

To introduce the concept, lets look at a very simple example: Connecting a textfield to a String variable.

Since this is going to be a very simple example, I want to put the variable in the AppDelegate like this:

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var parameterId: String = "Any Name"

Here we hit the first 'snag'. This won't work since later on we need to specify the property of a container that can be bound to the textfield. So we need to introduce a wrapper class like this:

class StringWrapper {
    var str: String
    init(str: String) {
        self.str = str
    }
}

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var parameterId: StringWrapper = StringWrapper(str: "One")

Er.. no, actually that won't work either. The StringWrapper Swift class is not KVO/KVC compliant. While it is probably possible to add the necessary protocols, it is way easier to simply inherit them from NSObject. Like this:

class StringWrapper: NSObject {
    var str: String
    init(str: String) {
        self.str = str
    }
}

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var parameterId: StringWrapper = StringWrapper(str: "One")

That is all for the AppDelegate, the rest is done within Interface Builder.

Open up the MainMenu.xib file and make sure you display the object/property hierarchy.
Next pick a ObjectController and drop in in the "Objects", it should show up like this (the Object Controller is placed under the "Font Manager"):


Select the Object Controller and bring up the "Bindings Inspector". Here we have to set the "Content Object". Select the "Bind to" checkbox, and select the "Delegate" from the popup box after it. In the "Model Key Path" enter "self.parameterId". Once you entered "self." a popup should appear from which you can choose "parameterId". If that does not happen, check the spelling and the code in AppDelegate.swift before continuing.


Now the controller knows where to find the variable, but it does not know about the textfield the variable should be connected to. To connect the controller with the textfield, we start from the textfield. I.e. we connect the textfield to the controller instead of the other way around. This way it is possible to connect multiple textfields with a single controller, should you ever want to...

Select the "Window" and drop the "NSTextField" in it. You can also add a label should you want to.
Now select the "Text Field":



and bring up the "Binding Inspector" again. This time we edit the "Value" binding as follows:


Click the "Bind to" check box, select the "Object Controller" and enter "self.str" in the Model Key Path. The "Controller Key" remains empty (will be filled in automatically later), and ignore the warning symbol. There is no popup selection box this time (or rather, it is empty). Maybe this will be fixed in a future version of xcode.

Now compile and run the app. You won't see the updates you'll make in the textfield, to see that the variable is actually updated, add an observer:

class StringWrapper: NSObject {
    var str: String {
        didSet {
            print("New value = \(str)")
        }
    }
    init(str: String) {
        self.str = str
    }
}

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    var parameterId: StringWrapper = StringWrapper(str: "One") {
        didSet {
            print("Will never be called")
        }
    }


Notice that only the wrapper observer is called, the "parameterId" observer is never called.

One more thing: When you want to update the value of parameterId.str do not simply assign the value, but use a KVC compliant method. Thus do not:

        parameterId.str = "Will not update the view"

But do this:

        parameterId.setValue("Will update the view", forKey: "str")

Happy coding...

Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH

We don't get the world we wish for... we get the world we pay for.

3 comments:

  1. Hi. Is the wrapping and using ObjectController really necessary? I usually bind to File's owner. Can you also please give me a hint why you used wrapper? I can bind it directly. I might be missing something.

    ReplyDelete
  2. If you mark your property/var as dynamic you don't have to call setValue forKey, assign is fine.

    ReplyDelete
  3. The object controller was necessary for my real life app which involved an array. And I did not know that it is possible to bind directly to the file's owner.
    The wrapper is probably only needed if a controller is used.
    I still prefer to use controllers as I also usually group properties that belong together. That makes for a nicer fit (but your milage may vary...)
    Thanks for the tip about the 'dynamic' marking.

    ReplyDelete