NSCell Image and Text Sample

Image Text Cell 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.

15 Responses to “NSCell Image and Text Sample”

  1. Patrick Says:

    Wow. Thanks for such a wonderful example. As a new Cocoa programmer, this is a helpful explanation of custom cells.

  2. Nick Says:

    Cheers for the example code.

  3. Mike Says:

    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?

  4. St├ęphane Says:

    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.

  5. martin Says:

    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.

  6. Thomas Says:

    Thanks heaps for the sample code :)

  7. Nick Says:

    It’d be great if you had a followup example for the same thing in an NSOutlineView

  8. martin Says:

    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.

  9. Thomas Says:

    Could you elaborate on the Core Data aspect. Where would you implement the dataElementForCell method if you use keypaths?

  10. Jon Baer Says:

    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!

    • Jon
  11. martin Says:

    @Thomas:

    For each window I use a subclass of NSWindowController (the NIB owner) where I implement the delegate methods (including dataElementForCell).

  12. martin Says:

    @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.

  13. Damir Says:

    i am new Cocoa-Programmer and i was searching for 2 days exactly your example :)

  14. Olivier Says:

    Absolutely great sample Martin!

    Thanks a lot!

  15. glazed Says:

    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.