Please note: this article is part of the older "Objective-C era" on Cocoa with Love. I don't keep these articles up-to-date; please be wary of broken code or potentially out-of-date information. Read "A new era for Cocoa with Love" for more.
In my last post, I showed how you might redesign the interface to the iPhone's UITableView
if you wanted to reimplement it on the Mac using Cocoa Bindings. This time, I'll show you how to make those bindings editable in Interface Builder so you can use the entire class with no code at all.
Introduction
I'm going to use integration with Interface Builder to construct a simple browser for the Address Book using the ColumnView
class to select entries:
The column on the left is handled by the ColumnView
class, a class which supports layout of rows in sections, with variable row heights, variable row classes and selection.
In fact, if the ABAddressBook
and its children were KVO compliant, there would be no project-specific code in this program at all (just the generic ColumnView
class, its children and default application template code).
You can download the project here: ColumnViewSample.zip (70kb)
Interface Builder integration
Last time, I focussed on designing a view for use with bindings in code and didn't look at integrating that effort with Interface Builder.
The redesign eliminated all 7 controller methods that would ordinarily be required to configure a UITableView
.
However, the code still required a great big binding instruction:
[columnView
bind:ColumnViewSectionArrayBinding
toObject:[[AddressBookDataController sharedController] groupsController]
withKeyPath:@"arrangedObjects"
options:
[NSDictionary dictionaryWithObjectsAndKeys:
@"members", ColumnViewSectionContentKeyOption,
kABGroupNameProperty, ColumnViewSectionHeaderDataKeyOption,
@"Last", ColumnSectionRowDisplayKeyOption,
nil]];
to hook everything together.
The purpose of Interface Builder integration is to eliminate even this statement; instead, the entire program can be set up using data alone.
Setting up an IBPlugin
The first required step is to setup an Interface Builder plugin for the ColumnView
class.
I've previously written a post showing all the steps required to create an IBPlugin for a view class. These are the steps I followed.
The ColumnPlugin project is in a subfolder of the ColumnViewSample project folder. You must build and run ColumnPlugin in the Release configuration to install it in your ~/Library/Frameworks
directory. After that, you will be able to use the ColumnView
's bindings in Interface Builder.
Note: You will get an error — Unable to resolve plug-in dependency for "ColumnViewSampleWindow.xib" — unless you build the "Release" build of ColumnPlugin.xcodeproj before you build the ColumnViewSample project.
Exposing bindings
Of course, the only reason why building the ColumnPlugin project will allow you to use the ColumnView
's bindings is because the ColumnView
class implements methods which enable Interface Builder integration. Let's look at how that works.
The first required step is to expose the binding. This is very simple, just invoke the exposeBinding:
method in the +intialize
method for the class.
+ (void)initialize
{
if (self == [ColumnView class])
{
[self exposeBinding:ColumnViewSectionsArrayBinding];
}
}
The string ColumnViewSectionsArrayBinding
is initialized as sectionsArray
, so this binding affects the sectionsArray
property on ColumnView
.
If you select an object with a binding like this, it will appear in Interface Builder's bindings panel (select the object and press Command-4) at the bottom under the "Parameters" section.
Custom binding options
If you add no other support, this will be sufficient to support basic binding of an arbitrary object to this property.
However, I designed my class to expect a number of keys to be set at the same time as the binding. These include:
sectionContentKey
—A key path (relative to each section object) where the rows array can be found (if not present, it is assumed that the section is the rows array).sectionClassKey
— A key path (relative to each section object) where the default class to use for all rows in the section can be found (if not present the defaultRowView
class is used). This property is overridden by therowClassKey
.rowClassKey
— A key path (relative to each row object) where the class for the row can be found (if not present, it is assumed the section is the rows array).rowDisplayKey
— A key path (relative to each row object) where a separate object used for display is found (if not present, the row object is used directly for display).headerDataKey
— A key path (relative to each section object) where the object for the header is found (if not present, no header is shown for the section).headerClassKey
— A key path (relative to each section object) where the class for the header row is found (if not present, the defaultRowView
class is used).allSectionsSortKey
— A key path (relative to each row object) by which every section should be sorted.sectionRowSortKey
— A key path (relative to each section object) where the key by which that section should be sorted can be found (this will override the allSectionsSortKey).
So we need to set up these properties as editable fields in the bindings editor.
The primary way to set up the options for a binding is through the optionDescriptionForBinding:
method:
- (NSArray *)optionDescriptionsForBinding:(NSString *)binding
{
if ([binding isEqualToString:ColumnViewSectionsArrayBinding])
{
NSArray *options =
[NSArray arrayWithObjects:
AttributeDescription(ColumnViewSectionContentKeyOption, sectionContentKey ? sectionContentKey : @"self"),
AttributeDescription(ColumnSectionRowDisplayKeyOption, rowDisplayKey),
AttributeDescription(ColumnSectionClassKeyOption, sectionClassKey),
AttributeDescription(ColumnSectionRowClassKeyOption, rowClassKey),
AttributeDescription(ColumnViewSectionHeaderDataKeyOption, sectionHeaderDataKey),
AttributeDescription(ColumnViewSectionHeaderClassKeyOption, sectionHeaderClassKey),
AttributeDescription(ColumnViewAllSectionsSortKeyOption, allSectionsSortKey),
AttributeDescription(ColumnViewSectionRowSortKeyOption, sectionRowSortKey),
nil];
return options;
}
return [super optionDescriptionsForBinding:binding];
}
Here AttributeDescription
is just a helper method to create an NSAttributeDescription
object.
Fixing custom bindings so they work
Having made it this far with my design and Interface Builder integration I discovered that Interface Builder doesn't support custom bindings options.
If you try to use a bindings option that isn't on Apple's official list of Bindings Options, the option will get set on your class briefly and then immediately cleared.
There is a fix for this unfortunate behavior: get the values when they are initially set, save them to persistent attributes (which you need to inform Interface Builder about) and then ignore any attempt in Interface Builder to set your options to nil
.
Informing Interface Builder you have persistent attributes is done in the CustomViewIntegration.m file of the ColumnPlugin:
- (void)ibPopulateKeyPaths:(NSMutableDictionary *)keyPaths {
[super ibPopulateKeyPaths:keyPaths];
[[keyPaths objectForKey:IBAttributeKeyPaths]
addObjectsFromArray:
[NSArray arrayWithObjects:
ColumnViewSectionContentKeyOption,
ColumnSectionRowDisplayKeyOption,
ColumnSectionClassKeyOption,
ColumnSectionRowClassKeyOption,
ColumnViewSectionHeaderDataKeyOption,
ColumnViewSectionHeaderClassKeyOption,
ColumnViewAllSectionsSortKeyOption,
ColumnViewSectionRowSortKeyOption,
nil]];
}
This will make sure that Interface Builder tracks these values in Undo/Redo.
After this is done, you need to ensure the attributes are saved when the Interface Builder file is saved by implementing encodeWithCoder
on the ColumnView
itself:
- (void)encodeWithCoder:(NSCoder *)encoder
{
[super encodeWithCoder:encoder];
[encoder encodeObject:sectionContentKey forKey:ColumnViewSectionContentKeyOption];
[encoder encodeObject:rowDisplayKey forKey:ColumnSectionRowDisplayKeyOption];
// And so on the for the other 6 properties...
and load them correctly when the NIB file is loaded:
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
if (self != nil)
{
sectionContentKey = [[decoder decodeObjectForKey:ColumnViewSectionContentKeyOption] retain];
rowDisplayKey = [[decoder decodeObjectForKey:ColumnSectionRowDisplayKeyOption] retain];
// And so on the for the other 6 properties...
and finally, ensure that any attempt by Interface Builder to set them to nil
is ignored:
- (void)bind:(NSString *)bindingName toObject:(id)observedObject
withKeyPath:(NSString *)observedKeyPath options:(NSDictionary *)options
{
if ([bindingName isEqualToString:ColumnViewSectionsArrayBinding])
{
// ...Other work associated with setting the binding goes here...
if ([options objectForKey:ColumnViewSectionContentKeyOption])
{
self.sectionContentKey = [options objectForKey:ColumnViewSectionContentKeyOption];
self.rowDisplayKey = [options objectForKey:ColumnSectionRowDisplayKeyOption];
// ...and so on..
}
// ... and so on for the other bindings
What I have done is considered the ColumnViewSectionContentKeyOption
option a mandatory inclusion — this will allow me to detect if Interface Builder is improperly setting my bindings options to nil or if the setting is valid. If there is no value for the ColumnViewSectionContentKeyOption
key in the dictionary then the options are considered invalid and will be skipped — leaving them at their previous values and allowing us to save them in initWithCoder:
when the file is saved.
Setting it all up in Interface Builder
Now that we have Interface Builder integration, hooking up the components in Interface Builder for our program is easy.
Provide the ColumnView with data
An NSArrayController
with the Address Book Data Controller's groups
array as its content is bound to the sectionArray
of our ColumnView
. ColumnView
prefers that you use an NSArrayController
for the top-level content (instead of directly binding an NSArray
), since it can use the selection and sorting from the NSArrayController
for the sections in the view.
The sectionArray
binding has the "section Content Key", "row Display Key", "section Header Data Key" and "section Row Sort Key" values filled in, so the ColumnView will know where to find the rows for each section, the object used to display the row, the name for each section and the sort order for each section respectively.
Update the detail view when the selection changes
The ColumnView
has a KVO-compliant selectedSectionArrayController
property which returns the NSArrayController
for the content in the selected section. Using the selection
property of this NSArrayController
, we can get the selected row's data.
To bind this selectedSectionArrayController
property, we need to add an NSObjectController
at the top level which will have our ColumnView
connected to its content
outlet.
By binding all of the text fields in the detail view through this controller, their values will all update based on the current selection. i.e. the "First Name" text field is bound to the NSObjectController
, with the controller key selection
and the model key selectedSectionArrayController.selection.First
.
Fields will automatically show "No selection" and "(None)" when there is no selection or a given selection does not contain a value for the field.
Conclusion
You can download the project here: ColumnViewSample.zip (70kb)
The final ColumnViewSample project shows a Master-Detail-style interface, constructed with almost no code. This is due to classes which are designed to work with bindings. A code-less implementation only works when when your classes are fully KVC and KVO compliant as bindings cannot operate without these design patterns.
Implementing custom bindings options in Interface Builder for the various key-path options wasn't totally necessary — you can set basic string keys like these in a normal Interface Builder Attributes panel — but I wanted to keep these keys closely associated with the binding. As an added benefit, this approach allowed me to use Interface Builder's built-in interface for editing these fields, rather than requiring that I implement my own.
ColumnView
remains a fairly basic replication of UITableView
-like functionality for the Mac. There is no UINavigationController
for drill-down hierarchies. It lacks the animation and edit modes of UITableView
. It does not attempt to reuse RowView
s for efficiency like UITableView
does. Nor have I really tested it thoroughly — it was just an experiment on my part.
Despite these shortcomings, if you want section-based layout of views though and you're prepared to add other features as you need them, ColumnView
may be useful as an alternative to NSCollectionView
or NSTableView
.
Designing a view with Bindings (UITableView on the Mac)
Dynamic ivars: solving a fragile base class problem