16.
Using Cocoa Bindings
Written by Sarah Reichelt
In the last chapter, you designed the movie edit window. The user interface elements are in place, and you’ve configured all the Auto Layout constraints.
Now, you have to display the data and allow the users to edit it. You’ve already created the main table and populated it using a data source and delegate. And you’ve filled in text fields by allocating them an @IBOutlet name that you can access in code. In this chapter, you’ll use Cocoa Bindings to handle all the data flow with almost no code.
Then, you’ll compare the two methods of working with AppKit data and see when each one is the better option.
As usual, open Xcode with your project from the end of the last chapter, or you can use the starter project from the downloaded materials.
Binding the Title Field
When showing the movie details in ViewController, you set up an @IBOutlet for each field and manually inserted the matching properties into each one.
This time, you’ll do it a different way; you’ll use Cocoa Bindings. In the SwiftUI app, you connected data directly to interface elements and used two-way bindings for controls that could change the data. Cocoa Bindings is an older technology, but it has a similar effect. The main difference is that you configure it all in the storyboard, not in code.
Start with the title. Open Main.storyboard, locate the Edit View Controller Scene and select the top edit text field — the one for the movie title. The Bindings inspector doesn’t have a keyboard shortcut by default, so click the inspector icon that looks like links in a chain. Expand the Value section at the top.
Check Bind to and select Edit View Controller from the popup.
Enter self.movie.title in the Model Key Path field:
The first of these settings tells the field to gets its value from Edit View Controller. The second tells it what property to use. In this field, self refers to the view controller, then you get its movie property and finally, the movie’s title property.
Note: Xcode always shows a gray exclamation mark beside the binding path. You only need to worry if it shows a red error marker.
You’ve bound one field, so that’s enough for a test. But you need to make one change to the data first.
Open EditViewController.swift and replace var movie: Movie? with
@objc dynamic var movie: Movie?
Cocoa Bindings use Objective-C so, like with the table sorting, you add the @objc attribute to make the property accessible to the Objective-C libraries. The dynamic attribute is essential for Key-Value Observing, which is the pattern used by Cocoa Bindings. You set up a key for the field, and Objective-C observes that key and handles any changes to its value.
Now run the app, select a movie, right-click and select Edit Movie…. The movie title appears in the edit field:
Now for the cool part. Change the title and click Save Changes. The Edit window closes and your new title appears in the table and in the movie details:
How’s that for a no-code solution? And when you quit and restart the app, your edit is still there.
Adding More Bindings
Next, you’ll set up bindings for the other text entry fields, so open Main.storyboard again.
Select the field beside the Run time label and open the Bindings inspector. Expand the Value section at the top if it isn’t already expanded.
Check Bind to and select Edit View Controller from the popup. Enter self.movie.runTime in the Model Key Path field.
Repeat this for the Year, Ratings and Genres fields. For each one, check Bind to and select Edit View Controller. Make sure you set the properties on the text entry fields, not their matching title labels.
Enter the respective Model Key Path:
- Year: self.movie.year
- Rating: self.movie.rating
- Genres: self.movie.genres
These match the properties in the Movie class. Cocoa Bindings does the work of converting the properties into strings and back again if needed. Make sure you press Return or Tab to finish editing the last one, or Xcode won’t process it and the app will crash.
Run the app, double-click a movie to open the edit window and test all these fields:
Right now, these text fields accept any text input. But in the Movie class, runTime is an Int and rating is a Double. What happens if you type in the wrong sort of data?
Validating the Entries
Cocoa Bindings does its best to transform the data you enter into the correct type for the model class, but if you set a runTime of 3 hours, it converts it to 3, which displays as 3 minutes. Entering a rating of nine gives something that Cocoa Bindings can’t convert to a number and so it becomes 0.
Neither of these options is ideal, so you’ll add a NumberFormatter to both the numeric fields to validate the entries.
Open Main.storyboard and arrange the views so you can see the Run time and Rating entry fields. Open the Library and search for number. Drag a Number Formatter into each of these fields.
The two formatters appear in the outline view, attached to each field. Select the first one, and the Run time field highlights in the storyboard. Open the Attributes inspector and change Style to Decimal:
Now, select the Rating formatter in the outline view. Make sure it highlights the correct entry field:
Over in the Attributes inspector, set Style to Decimal again. But this time, you’ll set limits. The rating must be between 1 and 10, so set the Minimum to 1 and the Maximum to 10.
Time to test this. Run the app, edit a movie and try typing invalid data into the Run time and Rating fields, pressing Tab to trigger the formatter:
That finishes the editing and validation for the data fields. Now, it’s time to populate the principals table.
Configuring the Table Columns
Open Data & Models ▸ Principal.swift to check the properties. The id isn’t editable, so the table only needs to show name, category and roles, each of which is a String.
Back in Main.storyboard, click the table in the Edit View Controller, and use the Shift-right-click menu to drill down to Table View.
Use Command-Option-5 to open the Attributes inspector and change the number of columns to 3. The outline view shows the three columns, two with automatic identifiers and the new one:
Select the first column in the outline view and use the Attributes inspector to set its Title to Name. Switch to the Size inspector and set its Width to 200.
Use the same technique to set the second column’s Title to Category and its Width to 150.
Finally set the third column to Roles. This needs a width that completely fills the table at the table’s current default width. Use the stepper arrows beside the width number to step up and down until you see it lined up exactly with the edge of the table. It’ll end up at 162.
Finally, go back to Table View and, in the Attributes inspector, check Alternating Rows to improve the look of the entire table.
Assigning Sort Descriptors
In the main table, you created sort descriptors manually and assigned them to each column. In the Edit window, you’re setting everything up in the storyboard.
Select the Name column in the outline and return to the Attributes inspector. Set the Sort Key to name and press Return. This fills in the default sort selector or method — compare: — but you want a case-insensitive sort. Replace compare: with localizedCaseInsensitiveCompare:, which is the sort method you applied to the movie titles in the main table. Now, this column sorts on the name property using a case-insensitive sort method:
Repeat this for the other two columns, setting the Sort Key to category and roles respectively. They can both use the default compare: selector.
Linking the Data
In the text input fields, you bound them to properties of the view controller’s movie. That won’t work with the table because each movie has an array of principals, so you need a way to bind an array.
The tool for this is an NSArrayController, which gives you a controller that manages the binding for a collection of objects.
Still in Main.storyboard, open the Library and find Array Controller. Drag it into the bar above the Edit View Controller:
Now you can bind this array controller to the principals data.
Make sure you have the array controller selected in the top bar, then open the Bindings inspector. Expand Controller Content ▸ Content Array, check Bind to and select Edit View Controller.
Set Model Key Path to self.movie.principals. This is the same pattern you used for the text input fields, but you’re binding the property to an array controller instead of a field.
With the array controller set up, you can link it to the table. Select the Table View and make sure you’re still in the Bindings inspector.
Expand Table Content ▸ Content. Check Bind to and select Array Controller if Xcode doesn’t have it as the default. The Controller Key is preset to arrangedObjects, which refers to the sorted objects in the array controller’s binding, and this is correct.
You haven’t finished yet, but run the app and double-click to edit a movie:
The table has the correct number of rows for the principals. Next, you’ll tell the table cells what to show for each row.
Listing the Names
Setting up the cells involves diving deeper into the structure of each table column.
Option-click the disclosure arrow beside the Name column in the outline view to expand it fully. (If it was already partially expanded, collapse it and then Option-click.) Ignoring the Constraints for now, you have this structure:
Press Command-Option-4 to open the Identity inspector so you can see the class of each element as you step through them.
Name is an NSTableColumn.
It contains Table Cell View — an NSTableCellView — and Text Cell — an NSTextFieldCell.
Table Cell View contains a Table View Cell — notice how the words have swapped around — but this one has an L icon and is an NSTextField. It holds another Table View Cell and this one is an NSTextFieldCell. So many parts with very confusing names.
The important one is the outermost Table View Cell with the L icon. Select it and use the Identity inspector to confirm you’ve selected an NSTextField.
Switch to the Binding inspector and expand the Value section if necessary.
Check Bind to. The default selection is Table Cell View and that’s correct. In Model Key Path, enter objectValue.name:
You’ve already bound the entire table to the Array Controller and this provides an objectValue property for each row and cell. You’re telling this cell to take its value from the name of this object.
Run the app and edit a movie:
And there are the names for the principals in the Name column.
Next, apply the same logic to the remaining columns. Back in Main.storyboard, click the disclosure arrow beside Name to collapse it and Option-click the disclosure arrow for Category.
Find the first Table View Cell and select it. In the Bindings inspector, check Bind to and choose Table Cell View. Set Model Key Path to objectValue.category.
Repeat this process for the Roles column, setting Model Key Path to objectValue.roles.
Run the app again, edit a movie and admire the results of your work:
Auto Layout Again
Before going any further, it’s time to fix the yellow layout warning in the outline view. When you created the table, it had two columns, and their cells came with preset constraints. The cell in the third column has no constraints.
In Main.storyboard, expand the Name column in the outline and select the Table View Cell — the one with the L icon. Open the Size inspector and check the constraints:
These align Center X And Center Y to the superview and give a leading spacing of 2.
Select the matching Table View Cell in the Roles column and click Align in the bottom toolbar. It’s to the left of Add New Constraints.
Check Horizontally in Container and Vertically in Container and then click Add 2 Constraints:
This sets two of the three constraints but moves the cell into the center of the column. Click Add New Constraints and set the left spacing to 2. Click Add 1 Constraint to apply this and set the cell to be the same as the other two. And this gets rid of the yellow warning. Hurray!
Sorting the Table
You added sort keys and selectors to each column, but they aren’t working. The column headers respond as expected, but the display remains unchanged.
It’s time for another binding!
Select Table View and swap to the Binding inspector. Expand Table Content ▸ Sort Descriptors. Check Bind to and make sure it’s set to Array Controller. This time, set Controller Key to sortDescriptors. If you start typing it, autocomplete suggests it.
Run the app and edit a movie. Sort the principals table by different columns and sort again to change from ascending sort to descending:
You already set up the sort descriptors for each column of the table. This configuration binds them to the array controller’s sort descriptors, so now it all just works!
The next step is to make the table editable.
Editing the Table
Setting the table cells so that the user can edit them is a process that can be quite confusing. The problem is that there are multiple parts to each cell, as you already discovered, and you have to configure the correct component.
In Main.storyboard, Shift-right-click in the first cell of the first column of the principals table and select the first Table View Cell. That’s the one with the Label icon because right now, it’s a Label:
Open the Attributes inspector and change Behavior to Editable:
Next, select the first Table View Cell in the Category column and set its Behavior to Editable.
Repeat this process for the Roles column.
It won’t happen immediately, but eventually Xcode works out that these are now edit fields, instead of labels, and changes their icons in the outline view:
Time to test this. Run the app and double-click to edit a movie. Select a row in the table and then click the text, or click anywhere in an empty cell. This lets you change the data in that cell:
Click Save Changes, and the new data appears in the main window.
Now you can edit existing data, but what about adding or deleting rows?
Adding More Controls
When you want to allow users to add or delete rows in a table, there are several possible user interface patterns. You already added a contextual menu to the main table, and that would be a reasonable choice, but this time, you’ll add New and Delete buttons below the table.
Use the Library to drag two Push Buttons into the view, between the Cancel and Save Changes buttons. Use the Attributes inspector to set their Titles to New and Delete.
These new buttons need Auto Layout constraints. Since they’re functionally linked, you’ll keep them together on either side of the center.
Select the New button and click Align. You used Align earlier to center the cell in the third column, but this time, you’ll add an alignment with an offset value.
Check Horizontally in Container and set the value to -40 instead of the default 0. A negative value pushes the constrained object to the left:
You’re familiar now with the standard Add New Constraints popup. Use it to set the bottom spacing below the button to Standard.
Repeat this for the Delete button, but this time, use an alignment offset of 40. A positive offset pushes to the right.
Now that the new buttons are properly positioned, it’s time to make them work.
Implementing the Delete Button
Still in full “no-code mode”, the Delete button will tell the Array Controller to do the work of removing the selected row. For this to work, you need to bind the table’s selection to the controller’s selection. That way the Array Controller knows which row to delete.
Click in the table area and use Shift-right-click to select the Table View. Open the Bindings inspector and expand Table Content ▸ Selection Indexes.
Check Bind to and confirm it’s set to Array Controller. In the Controller Key field, start typing select and choose selectionIndexes:
Since the table only allows a single selection at a time, you might think to choose selectionIndex, but don’t do it. Your app will crash unless you set this to selectionIndexes.
You only want to enable the Delete button if there’s a selected row to delete. This requires another binding to another array controller property.
Select the Delete button and, in the Bindings inspector, expand Availability ▸ Enabled.
Check Bind to — it’s set to Array Controller by default. Enter canRemove in the Controller Key field. This is a Boolean property on Array Controller that’s true whenever it’s possible to remove a row. Connecting this to the enabled state of the button is perfect.
There’s one more step to complete the implementation. Control-drag from the Delete button up to Array Controller in the top bar. Select remove: from the menu that appears when you release the mouse button:
And that’s it. Time for a trial run. Run the app, edit a movie, select a principal and click the Delete button:
Note: For this screenshot, I edited Jane Eyre (1914) and deleted Alberta Roy, so her name appears in the main window but not in the principals table.
Command-click the selected row to deselect everything and disable the Delete button.
You may be wondering why there’s no dialog here to confirm a destructive operation. It isn’t necessary because the user can always click Cancel to exit without saving any changes. But if you considered a confirmation dialog, that’s a good instinct.
Now, it’s time to complete your app by implementing the New button.
Inserting a New Principal
Again, Array Controller does the work here, but it needs some help. Before it can add a new object, it has to know what type of object to add.
Still in Main.storyboard, select Edit View Controller Scene ▸ Array Controller in the outline view and open the Attributes inspector.
In the Object Controller section, Mode is already set to Class, which is correct. In the Class menu, choose Principal:
Xcode sorts the list alphabetically, which puts Principal way down the list, but with the menu open, start typing prin to scroll directly to it.
This makes sure that Array Controller adds a new Principal when adding a new record.
When Array Controller creates a new object, it calls that object’s class init. Right now, Principal doesn’t have such a method.
Open Data & Models ▸ Principal.swift and add this:
// 1
override init() {
// 2
id = UUID().uuidString
// 3
name = "New Person"
category = ""
roles = ""
}
With this method you:
- Override the standard
initto provide some default property values. - Create a unique
id. UUID is its own object type, but itsuuidStringproperty is aString, which suits this class. - Provide default values for the other properties. You need to have text in at least one of these properties or the new row appears empty.
And finally, you’re ready to connect the button.
Jump back to Main.storyboard, Control-drag from the New button to the Array Controller and select insert:. There are two possible methods you could use: add appends the new record to the end of the table but since it doesn’t scroll the table to the new record, it can seem like it hasn’t worked. insert puts the new record before the current selection and then selects it, which provides a better user experience.
Run the app, edit a movie and click New:
There’s your new row, showing the default data you provided. You can edit its details and save the changes.
You’ve done it! The app is complete. You can view the list of movies, mark your favorites, sort and search. You can set different view modes, navigate to the movies web page and delete movies. And now, you can edit every detail about any movie. There’s a lot packed into this app, and you should feel very proud of yourself for having got through it all.
What Method to Use?
You’ve now used two different techniques for managing data. But which is the best?
The main window uses a data source and delegate to manage its table data, sorting and selections. When the user selects a table row, you have a series of labels set up as @IBOutlets that you populate with the movie data.
In the editing window, you use Cocoa Bindings exclusively. The EditViewController has very little code, where ViewController and its extension in TableData.swift have a lot of methods.
By now, you’ve probably decided which one you prefer, but which should you use?
The answer is it depends. :]
Let’s consider some arguments in favor of using the data source - delegate - IBOutlet method:
Maintainability: When you open a project that’s new to you, or maybe one you haven’t touched in a year, the data source - delegate - IBOutlet method is more obvious. It’s easier to work out what parts of the code provide and manipulate the data.
By contrast, when you open a project that uses Cocoa Bindings, the data seems to appear by magic, and it can take a lot of detective work to track down exactly what’s happening.
Source control: Swift files are plain text and, when you use a source control system like git, you can track all the edits and revert any changes if you break things.
In a Cocoa Bindings app, all the action is in the storyboard file. Under the hood, this is XML text, but it uses a custom format and while you can track changes in the XML, using that as a way of reverting changes would be tricky.
Also Xcode changes the storyboard file periodically, editing time stamps and internal identifiers. This adds confusion and noise to your attempts to track edits.
Now for some advantages to using Cocoa Bindings.
Table Editing: When you set up a data source and delegate table for editing, you use a delegate method to detect text field changes. Once you have a change, there’s a lot of code involved in working out exactly what cell in what row the user edited. And then you apply that manually to the model object. With Cocoa Bindings, it all just works.
Less Code: You write a lot less code when using bindings and this leads to fewer bugs. If you set up the bindings incorrectly, your app either displays no data or it crashes. This means you detect errors quickly. When you write a lot of lines of code, there’s always the possibility of some subtle logic error that may look right, but isn’t correct.
My general recommendation is to use the data source - delegate - IBOutlet method, unless you need in-table editing. If you come to a project that’s already set up, it may not be your decision, but now you have the tools to work with either technology.
As a side note, you may be wondering how SwiftUI fits into this. It uses bindings but sets them up in code. This is the best of both worlds as it gives you maintainable code but still offers the magic of data binding.
Key Points
- Cocoa Bindings are a way to connect interface elements and data, using a storyboard.
- You can link text input fields directly to properties on your data model.
- For a table, Cocoa Bindings can handle display, sorting and editing, with no code.
Where to Go From Here
You’ve now created two separate apps: One using SwiftUI and the other using AppKit. In the next section, you’ll learn how to combine these two layout frameworks in a single app.