NSCell Image and Text Sample

Some time ago I wrote about the restrictions of bindings and the fact that custom cells often need more than one data element - see Data for a custom cell in a NSTableView. There were some very good comments on the article and also the request for a sample code.
So I took some code from Sofa Control and put together a sample project for download. The project has a class called ImageTextCell that is a subclass of NSCell and draws two text lines and an icon.
It works like this:
Step 1: Set custom cell on NSTableColumn
As a first step you have to configure your NSTableView to use the custom cell instead of the standard one.
To do that instantiate the custom cell and set that instance as the data cell of a column in the NSTableView. Best place to do that is the awakeFromNib in a controller class (see MainController.m)
// appList is a NSTableView object
NSTableColumn* column = [[appList tableColumns] objectAtIndex:0];
ImageTextCell* cell = [[[ImageTextCell alloc] init] autorelease];
[column setDataCell: cell];
As the custom cell needs more vertical space than the standard cell you have to set the row height of the NSTableView to 34 pixel in Interface Builder.
Step 2: Getting the data to the cell
The tricky thing is how to get the necessary data to the custom cell. In the example we are using a complex object called AppInfo. The AppInfo object has an image and two text elements. These data shall be displayed by the custom cell.
We have a NSArray that holds a number of these AppInfo objects. The column of the NSTableView is bound to display these objects. The binding is configured in Interface Builder.
With this the cell has access to the AppInfo objects. Each time the drawWithFrame method is called the cell can get access to the AppInfo object with [self objectValue]. As we do want to reuse the cell we don’t want to to hardcode the access to AppInfo objects.
Therefore we use an indirection by introducing delegate methods that are responsible to retrieve the data for the cell.
- (NSImage*) iconForCell: (ImageTextCell*) cell data: (NSObject*) data;
- (NSString*) primaryTextForCell: (ImageTextCell*) cell data: (NSObject*) data;
- (NSString*) secondaryTextForCell: (ImageTextCell*) cell data: (NSObject*) data;
The methods are implemented in the controller and you have to set the controller as the delegate on the cell instance (see awakeFromNib in MainController.m).
NSCell copies objects
Each time the method setObjectValue on NSCell is called it creates a copy of the given object. Therefore the AppInfo object implements the NSCopying method.
There are cases where you do not want to create copies of your objects (e.g. when you are using CoreData objects). For these cases I don’t bind the data object itself to the NSTableColumn but instead I bind object identifiers. Some kind of primary key that uniquely identifies an object.
In this case the cell’s objectValue is the object identifier and we have to resolve the correct data object. The ImageTextCell therefore supports an optional delegate method to map an object identifier to the correct data object. A typical implementation looks like this.
- (NSObject*) dataElementForCell: (ImageTextCell*) cell {
NSObject* uniqueId = [cell objectValue];
... build up a core data fetch request
NSObject* dataObject = ...executeFetch...
return dataObject;
}
KeyPath instead of delegate methods
In the previous article Markus suggested to use keypath.
Instead of implementing delegate methods one could set keypaths on the ImageTextCell object that are used to retrieve the values from the data object. This works perfectly for simple cases where the getter methods of the data object map perfectly to the display values of the cell.
Instead of setting a delegate and implementing the delegate methods you would configure the cell as following:
[cell setPrimaryTextKeyPath: @"displayName"];
[cell setSecondaryTextKeyPath: @"details"];
[cell setIconKeyPath: @"icon"];
Interface Builder Integration
It would be great if there is an easy way to define binding information for custom cells in Interface Builder. Leopard comes with a totally new Interface Builder and perhaps a way to extend the binding inspector for custom cells as well. We’ll see.
May 6th, 2007 at 1:51 am
Wow. Thanks for such a wonderful example. As a new Cocoa programmer, this is a helpful explanation of custom cells.
May 6th, 2007 at 10:12 am
Cheers for the example code.
May 6th, 2007 at 1:19 pm
Thanks for the sample code. Is it necessary to have an arraycontroller? I have an app that searches a website and has the results saved in an array, the reloads that data into the table view. I cannot figure out how to get that data into the customcell? Any have a clue to help this noob?
May 6th, 2007 at 5:02 pm
There is a way to define binding information in IB: create a palette for your custom cell class, then implement -valueClassForBinding:, -exposedBindings, and invoke +exposeBinding: — see protocol. BTW, I like Markus’ suggestion to use keyPath parameters instead of delegate methods; that’s how Apple does it too, e.g. in NSTreeController.
To avoid having to cope with the copy of object by the cell, you can consider that ‘value’ is bound to ‘arrangedObjects.displayName’; then you bind ‘arrangedObjects.icon’ to a new ‘icon’ binding, and ‘arrangedObjects.details’ to a new ‘details’ binding (your NSArrayController contains AppInfo objects). In your cell subclass, you implement -icon, -setIcon:, -details, -setDetails:, and in -dealloc you unbind ‘icon’ and ‘details’. That’s all you need to do - again, see http://wonder.cvs.sourceforge.net/wonder/Wonder/Utilities/RuleModeler/, with RMTextFieldCell used as custom table data cell in RMModelEditor.nib, bound in code by RMModelEditor.
May 6th, 2007 at 9:18 pm
I’m glad that the code helps some people.
If one does not want to use a NSArrayController but the (old school) NSTableDataSource instead it’s easy.
To adopt the example do the following:
1. In Interface Builder remove the NSArrayController and set the Datasource of the table to the MainController instance.
2. Add the following code to MainController.m:
-(int)numberOfRowsInTableView:(NSTableView *)aTableView { return [applications count]; } -(id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex { return [applications objectAtIndex: rowIndex]; }The object that is being returned by the method tableView:objectValueForTableColumn:row is the one that is being set as the object value on the cell. Therefore it’s the object you get as the data parameter in the cell’s delegate methods.
May 15th, 2007 at 1:55 pm
Thanks heaps for the sample code
May 16th, 2007 at 3:52 am
It’d be great if you had a followup example for the same thing in an NSOutlineView
May 16th, 2007 at 11:16 am
I plan to provide an update of the sample code with a number of additions. Thanks to the information of Stéphane it will also include an IB integration.
@Nick: Using the same mechanism in a NSOutlineView shall be pretty easy and I extend the sample to show that as well.
June 19th, 2007 at 10:52 pm
Could you elaborate on the Core Data aspect. Where would you implement the dataElementForCell method if you use keypaths?
June 21st, 2007 at 7:12 pm
Hi Martin,
Im using this class in a sideproject, http://jonbaer.textdriven.com/facetastic/
The one question I have is on text wrapping in the cell, I have come up empty in how to wrap the secondary text, any ideas?
Thanks!
June 22nd, 2007 at 11:11 am
@Thomas:
For each window I use a subclass of NSWindowController (the NIB owner) where I implement the delegate methods (including dataElementForCell).
June 22nd, 2007 at 11:15 am
@Jon:
Nice usage.
If you want to wrap the text you have to adopt the drawing code in ImageTextCell.m.
You have to replace “drawAtPoint:withAttributes” with “drawWithRect:options:attributes:”. In the options you can specify which kind of wrapping you like. Check out the “NSString Application Kit Additions Reference” for the details.
September 28th, 2007 at 6:22 pm
i am new Cocoa-Programmer and i was searching for 2 days exactly your example
November 6th, 2007 at 5:28 pm
Absolutely great sample Martin!
Thanks a lot!
March 11th, 2008 at 10:00 am
Guys…
check this out: http://kupuk.com/2007/10/08/custom-nscells-with-nsmanagedobjects/
It describes a solution for using managed objects in the multi-value custom cells.