
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