CocoaDevCentral.com . Articles . Introduction to Cocoa Bindings Email Feedback
Introduction to Cocoa Bindings


Cocoa Bindings in a nutshell: write less code

Historically, Cocoa developers have had to keep data objects and view objects in sync manually. If you want to use a table view, you implement datasource methods. Each method interprets the table view's request, and makes sure data ends up in the right place.

This is actually a pretty good deal, though. By just writing a few methods, you give life to very rich, full-featured tables. Cocoa does a lot of the hard work.

But if you have multiple controls that depend on each other, there's more work involved. If a dropdown changes, you have to make sure the contents of a table gets updated.

Bindings gives you a way to define relationships, and Cocoa figures out what to do at runtime. You have to write less code, which means faster development and more functionality for free.

You can mix and match Cocoa Bindings with datasource and delegate methods, so if your target users have Panther installed, there are very few reasons to not use bindings.


Note that Cocoa Bindings are synonymous with the controller layer, though bindings is the preferred term.


A Simple Approach to Bindings

Cocoa Bindings is a far-reaching topic, but I'm going to focus on a simple, practical example.

You're going to take a fictional email client called MailDemo that uses only the classic datasource methods, and convert it into one that uses Cocoa Bindings.

To keep things simple, the app only has three classes: Mailbox, Email and MyController.


Download the project so you can follow along and make the changes as they're described. Note that this is not the finished project, just the starting point:

> Download the MailDemo Xcode project 109k


Requirements

Learning about bindings requires that you have a firm grasp on the basics. To get value from this tutorial, you need to be comfortable with Xcode, Interface Builder, and have a solid understanding of Objective-C syntax in general.

A basic understanding of key-value coding is also helpful, as the entire bindings system is built upon it and related protocols.

Define the Email Class

The Email class is very basic. It has one member:

Email.h
@interface Email : NSObject { NSMutableDictionary * properties; } - (NSMutableDictionary *) properties; - (void) setProperties: (NSDictionary *)newProperties; @end

There aren't members for the address, subject or date. All of the email's information is stored in the properties dictionary. This design makes it easy to change the behavior of the application later and requires less code.

I want to give each new Email object some reasonable values right from the start. This is taken care of in the init method:

Email.m (excerpt)
- (id) init { if (self = [super init]) { NSArray * keys = [NSArray arrayWithObjects: @"address", @"subject", @"date", @"body", nil]; NSArray * values = [NSArray arrayWithObjects: @"test@test.com", @"Subject", [NSDate date], [NSString string], nil]; properties = [[NSMutableDictionary alloc] initWithObjects: values forKeys: keys]; } return self; }

The only other methods implemented are the basic accessors for the actual properties dictionary object. All the setting/getting of individual values inside the dictionary is handled automatically by key-value coding.


The class is called Email and not Message because the term "message" has a special meaning in Objective-C. Using it as a name for a data class could lead to confusion.

Define the "Mailbox" Class

The Mailbox class is also very simple by design. It has two members: a properties dictionary and a mutable array to hold Email objects.

Mailbox.h
@interface Mailbox : NSObject { NSMutableDictionary * properties; NSMutableArray * emails; } - (NSMutableDictionary *) properties; - (void) setProperties: (NSDictionary *)newProperties; - (NSMutableArray *) emails; - (void) setEmails: (NSArray *) newEmails; @end

I set a default title for the mailbox inside the init method:

Mailbox.m (excerpt)
- (id) init { if (self = [super init]) { NSArray * keys = [NSArray arrayWithObjects: @"title", nil]; NSArray * values = [NSArray arrayWithObjects: @"New Mailbox", nil]; properties = [[NSMutableDictionary alloc] initWithObjects: values forKeys: keys]; emails = [[NSMutableArray alloc] init]; } return self; }

And that's it for the data classes. On to the controller.

Define the "MyController" Class

As usual, the controller class defines the core structure of the application. I've declared outlets to each UI object, and also a mutable array of mailboxes;

MyController.h (excerpt)
@interface MyController : NSObject { IBOutlet id mailboxTable; IBOutlet id emailTable; IBOutlet id previewPane; IBOutlet id mailboxStatusLine; IBOutlet id emailStatusLine; NSMutableArray *_mailboxes; }

Here are the declared methods for the MyController class:

// simple accessors - (NSMutableArray *) mailboxes; - (void) setMailboxes: (NSArray *)newMailboxes; // UI action methods - (IBAction) addEmail: (id)sender; - (IBAction) addMailbox: (id)sender; - (IBAction) removeEmail: (id)sender; - (IBAction) removeMailbox: (id)sender;

The action methods are pretty self-explanatory. They simply create or remove Email or Mailbox objects. MyController also implements the standard NSTableView datasource methods and receives updates from the preview pane via NSTextView's delegate messages.


MyController Implementation

I'm of course not going to list the entire contents of MyController.m, but I'm going to highlight one of the datasource methods:

MyController.m (excerpt)
... setObjectValue:(id)object forTableColumn:(id)column row:(int)row { NSString * key = [column identifier]; if (table == mailboxTable) { Mailbox * mailbox = [[self mailboxes] objectAtIndex: row]; [[mailbox properties] setObject: object forKey: key]; [mailboxTable reloadData]; } if (table == emailTable) { // get current mailbox int mailboxRow = [mailboxTable selectedRow]; if (mailboxRow < 0) return; Mailbox * mailbox = [[self mailboxes] objectAtIndex: mailboxRow]; NSArray * emails = [mailbox emails]; Email * theEmail = [emails objectAtIndex: row]; [[theEmail properties] setObject: object forKey: key]; [emailTable reloadData]; } }

So the point here is that I check which table is making the request, and then do manual updates between data objects and table views. And after the data has been updated, I need to make sure that I refresh the tables and views that are affected by the changes.

This is the infamous "glue code."

Except for location of the source data, most table datasource method are basically the same. This is a sure sign that the process can be generalized. That's exactly what the bindings system does.


First Step into Bindings

From within the MailDemo Xcode project, double-click MainMenu.nib to open it in Interface Builder.

Cocoa Bindings is a very broad topic, but the main classes we're interested in for this application are NSObjectController and NSArrayController. Select the "Controllers" pane from the object palette.

The magic green boxes of the NSController family:
NSUserDefaultsController, NSObjectController, and NSArrayController


1. Drag the NSObjectController icon (the middle one) to your MainMenu palette. Double click the title of the icon and rename it to ControllerAlias. Then, control-drag a connection from the green ControllerAlias to the blue MyController object, and approve the connection to the content outlet.

This little green box is your bridge between the code in MyController.m and the bindings system.


2. Now drag a NSArrayController onto the MainMenu document window and rename it to Mailboxes. This will represent the mailboxes array that's declared in MyController.h.

3. Drag out another NSArrayController and name it Emails. This will track the currently selected Mailbox, and represent its emails array.

Your MainMenu document window should now look like this:


Configure the NSArrayControllers

Now that you know which NSControllers you're going to use to represent the data, it's time to hook them up. The process of assigning bindings is not the typical control-drag affair.

You define bindings for an object by choosing the Bindings dropdown from the inspector window.

The ControllerAlias doesn't need any bindings configured. It has a connection to MyController via the content outlet.

Select the Mailboxes object and bring up its bindings inspector. Click the Bind checkbox and provide the settings show in the table here to the left (also reflected in the screenshot to the right).


Mailboxes - NSArrayController
Bind to: ControllerAlias
Controller Key: selection
Model Key Path: mailboxes

In essence, the Mailboxes array controller is saying: Take the thing that ControllerAlias points at, and let me manage the contents of its mailboxes array.

You're delegating the responsibility of managing the array of Mailbox objects to an instance of NSArrayController. You give it some general guidelines of what you want to accomplish and it handles most of the details.


What Do These Fields Mean?

Without getting into too much detail just yet, the Controller Key is the name of an NSController method. The Model Key Path is the keypath used on the destination object to retrieve a value.

Here's another way to look at it:

ControllerAlias > Controller Key: selection == MyController
MyController > Model Key Path: mailboxes == the mailboxes NSMutableArray

Object Class Name

The last thing I need to do for the Mailboxes array controller is to tell it the class name of the objects that are in its source array.

Select the Mailboxes icon and open the Attributes pane of the inspector, which will look similar to the window at the right. Set the Object Class Name to Mailbox.

Note this is the singular form of Mailbox (not mailboxes), because I'm specifying the class name of the objects inside the array.


Setup the Emails Controller


Emails - NSArrayController
Bind to: Mailboxes
Controller Key: selection
Model Key Path: emails
Object Class Name: Email

In the user interface, the email table displays only the messages in the currently selected Mailbox. Each Email resides in a Mailbox, so I'll bind the contentArray of Emails to Mailboxes.

Bring up the Bindings inspector panel for the Emails object and fill in the settings shown here on the left.

I specify selection as the Controller Key because I only want the Email objects from the currently selected Mailbox. The Model Key Path of emails refers to the emails mutable array defined in Mailbox.h.

Remember that the Object Class Name has to be set to Email in the Attributes panel of the inspector. It's actually pretty easy to forget this step entirely, so always check here first if the behavior isn't as you expect.


Visualizing the Relationships

So what exactly has been done here? Let's reflect.

I started out with just the standard MyController blue cube.

I then added an NSObjectController object called ControllerAlias, and connected its content outlet to MyController.

Next, I added an NSArrayController called Mailboxes, and bound it to ControllerAlias with a Model Key Path of mailboxes.

Finally, I added an NSArrayController called Emails, which I bound to Mailboxes with a Controller Key of selection and a Model Key Path of emails.


The Result

Since Emails is bound to the selection of Mailboxes, the contents of the Emails array controller will change whenever a new mailbox is selected.

The reason I keep mentioning this is that's it's a critical concept in bindings.


Bind the Mailbox Column

Now I'm going to set the UI elements to use the NSController objects.

First, select the mailboxes table view and disconnect its datasource and delegate outlets so that we're working without a net. You'll often use delegate connections in conjuction with bindings, but not for this example.

In most cases, you don't want to set bindings for the table itself. Instead, you bind the table's columns.

Select the column in the mailboxes table, and bring up the Bindings panel of the inspector.

Fill in the bindings settings for the column as show in the table below (and the screenshot).

Mailboxes table column
Bind to: Mailboxes
Controller Key: arrangedObjects
Model Key Path: properties.title

I'm binding to the arrangedObjects key of the Mailboxes array controller. You generally use this key when you want all of the objects available in the array.

I bind the key path to properties.title, which accesses the value of the title key from the properties dictionary declared in Mailbox.h.


Connect Target/Action

Now, disconnect the actions for the plus and minus buttons located below the mailbox table. Select each individually and choose Disconnect in the Connections pane of the inspector.

Now Control-drag from the plus button to the Mailboxes array controller. Select the add: action and click Connect. Do the same for the minus button, but connect it to the remove: action.

We're done with the mailboxes table.


"Address" column
Bind to: Emails
Controller Key: arrangedObjects
Model Key Path: properties.address
"Subject" column
Bind to: Emails
Controller Key: arrangedObjects
Model Key Path: properties.subject
"Date" column
Bind to: Emails
Controller Key: arrangedObjects
Model Key Path: properties.date

Configure the Emails Table

Now, repeat the same process for the emails table.

1. Disconnect the datasource and delegate outlets

2. Bind the columns as shown in the diagrams to the right. Note that this time you're binding to the Emails array controller.

3. Disconnect the actions for the plus and minus buttons under the emails table, then reconnect them (use control-drag, don't bind) to the add: and remove: actions of the Emails object.


Configure the Preview Pane

Finally, we have the preview pane, which is a text view. There is no value binding available (*) with NSTextView, but its data binding will work fine for our purposes.

Before you set up the binding, double-click the preview pane to select it, then disconnect the delegate outlet.

Preview Pane
Bind to: Emails
Controller Key: selection
Model Key Path: properties.body

Select the preview pane by double-clicking it, and set the bindings to match those in the table to the right.

Notice that for the preview pane, I'm still binding to the Emails array controller, but I'm using the Controller Key selection because I want to use the currently selected Email object.


* Incidentally, this is why the default value of the body key in the Email properties dictionary is [NSString string]. The data binding can't cope with a literal string like @"message body". A more proper initial value would be [NSData data], but a string results in less confusion for this tutorial.


Test Drive

Everything's hooked up so we should be ready to roll. Double-check that all the delegate and datasource outlets are disconnected, then do a build and run from Xcode.

MailDemo should behave the same as before, but without using any datasource methods. You'll notice the status lines under each table don't get updated, and we'll correct shortly.


Ditch the Glue Code

There are two comments in MyController.m that contain the term glue code

/* begin glue code */ (line 48) ... /* end glue code */ (line 338)

You can go ahead and delete all 288 lines of code between these two comments. All you should have left is init, dealloc, and the get/set methods for the mailboxes array.

Build and run the application again, and you'll see that it still works!


Remove the IBOutlets

Open MainMenu.nib back up in Interface Builder and disconnect all the outlets for MyController by selecting it and bringing up the Connections panel of the inspector.

Then, in MyController.h, delete the declarations of the IBOutlets and UI action methods. You should end up with this:

MyController.h
@interface MyController : NSObject { NSMutableArray * _mailboxes; } - (NSMutableArray *) mailboxes; - (void) setMailboxes: (NSArray *)newMailboxes; @end

This is the entire controller. You have a real, functioning Mac OS X application with multiple sortable, synchronized tables - all in a few dozens lines of code.

The app isn't useful because it can't save files, but it's important to note that it took 288 lines of code just to make the UI work. With bindings, all of that goes away.

In reality, this is very little code considering what it gives you. In many other development environments, getting the same functionality would take much more code. But with Cocoa Bindings, you don't even need this small amount.


Finishing Touches

The last remaining issue is the count status line below the mailbox and email tables.

This is a slightly different situation than binding a table column or a text view. You need to somehow get the count of mailboxes, and then you also need a way to provide a template for display of the information. You need a string constructed like:

<count of mailboxes array> Mailboxes

This is similar to the format strings used in NSLog().

I'm going to use two items in the bindings toolbox to handle this: display patterns and array operators.


Display Patterns and Array Operators

Select the text item below the mailboxes table. Bring up its Bindings in the inspector, and match them to the screenshot on the right.

I'm binding to the arrangedObjects controller key of Mailboxes, but I specify @count operator for the model key path. This returns the number of elements in arrangedObjects array.

In the display pattern box, I enter the string %{value1}@ Mailboxes.

The display pattern combined with the @count will result in a string similar to 7 Mailboxes.

Repeat the same process for the status text field below the emails table, but bind to the Emails array controller instead.

"Mailbox count" text field
Bind to: Mailboxes
Controller Key: arrangedObjects
Model Key Path: @count
Display Pattern: %{value1}@ Mailboxes
"Email count" text field
Bind to: Emails
Controller Key: arrangedObjects
Model Key Path: @count
Display Pattern: %{value1}@ Emails

Congratulations!

If you have a reasonable understanding of what you just accomplished, you've gone a long way towards understanding bindings. The best way to get a better understanding is to create your own project and start playing around with the other tools.

Cocoa Bindings is a very deep topic and touches a lot of areas in Cocoa. Future articles will explore other areas of bindings and related technologies. Consider this article a prerequisite for future topics.


Cocoa Dev Central always welcomes feedback, particularly requests for future topics. Also let us know if anything needs clarification.


Further Reading

Local Page Saving Cocoa Application Data a tutorial to add saving to the MailDemo application
Remote Site Cocoa Dev Central Blog author's personal site
 
Related Site mmalc's Cocoa Bindings a great collection of examples and hints
Related Site Bindings at CocoaDev.com peer-edited docs on bindings
Related Site MacDevCenter on Bindings article on bindings at O'Reilly
 
Apple Cocoa Bindings - Conceptual overview of Cocoa Bindings with examples
Apple Cocoa Bindings - Reference reference for the bindings available to Cocoa objects
Apple NSController class reference for NSController (abstract superclass)
Apple NSArrayController class reference for NSArrayController