Data for a custom cell in a NSTableView

Creating a good looking interface is a lot of work. There was some discussion lately about the Human Interface Guidelines (HIG) and when you should break them. Daniel Jalkut has written a very nice blog entry on how he introduced his own HIG in order to improve the UI of FlexTime.

I’m not an UI designer. I get most of my ideas from playing around with other applications. One thing I learned when extending the UI is that it could take a lot of time to do so.

Inspired by the user interface of NewsFire, Mail, iPhoto, and others I wanted to display an icon and two lines of text in a single table column.

In order to do that you have to draw the content by yourself. You have to define a new NSCell subclass and do all your custom drawing there.

The next thing you have to do is to tell the NSTableColumn instance to use your cell class when drawing content.

NSTableColumn* column = [[myTableView tableColumns] objectAtIndex:0];
ImageTextCell* cell = [[[ImageTextCell alloc] init] autorelease];
[column setDataCell: cell];

That was easy. The cell is used to draw the content for each row. To do that the cell of course needs the data. Here starts the design problem.

How gets the data to the cell?

I’m using Cocoa bindings for the table. Through bindings you can bind a single value to a column per row.

A single value. But at least I need three values. An image and two text values and perhaps some state information, too. All this information is already stored in a single data object.

I found these options to solve the design problem:

  1. Set the data on the cell right before it draws

    NSTableView provides a number of notifications which are send to its delegate. One of this notification methods is tableView:willDisplayCell:forTableColumn:row:. This method is called before a cell draws its content. By using this method we could set the data on the cell before it draws.

    // code in NSTableView delegate
    - (void)tableView:(NSTableView *)aTableView 
            willDisplayCell:(id)aCell
            forTableColumn:(NSTableColumn *)aTableColumn
            row:(int)rowIndex
    {
         // get data object
         MyDataObject* data = [dataArray objectAtIndex; rowIndex];
         [aCell setMainText: [data title]];
         [aCell setSubText: [data subTitle]];
         [aCell setImage: [data icon]];
    }
    

    The good thing about the solution is that I am able to set any kind of data regardless where it is stored. The bad thing is of course that I always have to set the NSTableView delegate and I’m storing data temporary in the cell. And in reality I have to check if the given cell is my custom cell and more…

  2. Bind the data object to the column

    Adopt the custom cell to ask the bound object for the data.

    // code in custom cell implementation
    NSString* mainText = [[self objectValue] mainText];
    NSString* subText = [[self objectValue] subText];
    NSImage* image = [[self objectValue] image];
    

    The bad thing about this solution is that it defines an informal contract between the cell and it’s data. Whenever I want to (re-)use the custom cell my data objects have to implement the given methods. I’m unable to use other objects or a simple NSDictionary without adopting the custom cell code

  3. A custom data source for the cell

    My preferred solution is a combination of solution 1 and 2. It uses a delegate which is responsible to provide the necessary data for the cell. The custom cell implementation defines data source methods which are implemented by the delegate.

    // data source methods for the custom cell
    @interface NSObject(ImageTextCellDelegate)
        - (NSImage*) iconForCell: (ImageTextCell*) cell data: (NSObject*) data;
        - (NSString*) primaryTextForCell: (ImageTextCell*) cell data: (NSObject*) data;
        - (NSString*) secondaryTextForCell: (ImageTextCell*) cell data: (NSObject*) data;
    @end
    

    The delegate implements the data source methods. During initialization of the custom cell the delegate is being set.

    ImageTextCell* cell = [[[ImageTextCell alloc] init] autorelease];
    [cell setDelegate: self];
    

Data-sources are the old concept of Cocoa. Bindings make a lot things much easier. But not everything can be done with bindings.

In these cases it’s good to remember a major benefit of a data source. The benefit of decoupling.

Update: I posted another article with a sample project about this topic.

10 Responses to “Data for a custom cell in a NSTableView”

  1. Nick Brawn’s Weblog · The interface refactoring continues Says:

    […] Data for a custom NSCell in NSTableView […]

  2. Markus Says:

    Regarding solution 2:
    we agree that the custom cell must know which kind of data it can expect (e.g. two strings and one image) - the problem is how to get it from the bound object.
    Why not parametrize the custom cell with the keys of data-object?
    Something like this (in the custom cell’s class):

    • (id)initWithPathsForMainText:(NSString *)p1 subText:(NSString *)p2 image:(NSString *)p3

    So you can reuse your custom cell with nearly every kind of data-object (at least with such ones hat can provide an image and two strings)…

  3. martin Says:

    What a great idea! Never thought about using key path for this. Great!

    In my particular case I’m doing more stuff in the delegate (adjust colors….) and therefore I’m also sending the cell itself to the delegate.
    So in this case it would would not work with the key path.

    I think the difference is similar to the bindings vs. datasource stuff. Bindings are great and easy to use, but you could get to the limits and are stuck. Datasources are flexible but you have to code a lot.

    Your idea reminds me to not forget the power of the dynamics of Objective-C. I must get out this (static) Java stuff from my head.

  4. St├ęphane Says:

    Bindings are much more flexible than you think.
    If your cell has, say, 3 ivars, myImage, myMainText and mySubtext values. You can simply do the following:

    NSTableColumn *aCol;

    [[aCol dataCell] bind:@”myImage” toObject:… withKeyPath:@”data.image” options:…];
    [[aCol dataCell] bind:@”myMainText” toObject:… withKeyPath:@”data.mainText” options:…];
    [[aCol dataCell] bind:@”mySubtext” toObject:… withKeyPath:@”data.subText” options:…];

    myMainText could be the cell’s objectValue, to avoid adding that ivar.
    In your cell subclass, your setters need to tell the controlView to refresh itself whenever new value is different from old one.
    Look here for such an example: http://wonder.cvs.sourceforge.net/wonder/Wonder/Utilities/RuleModeler/ - see RMTextFieldCell, which is used as a tableView data cell that highlights some parts of its string value according to a search filter.

  5. martin Says:

    Hello St├ęphane,

    That sounds very promising and I gave it a try but unfortunatley I couldn’t make it work.
    The problem is that for each line of the table I have a specific object. In order to get the right data I have to create a binding to these objects.
    So I would have to rebind during the loop that fills the table.
    The only thing I could imagine is to bind to the cell’s object-value and to rebind each time the cell value is being changed.

    Am I missing something?

  6. Mint Says:

    Hello

    I am trying to develop a custom NSTextfieldCell to display in a tableview, the cell will be used to show one string and two icons.

    I bind my tablecolumn to the arrangedObjects.key keypath.

    I allways get the same problem: the cell receive the array (of icon name) of the arranged objects and not a single value. I am unable to display my cell

    I believe my custom cell (and custom tablecolumn) are KVO compliant.

    I did have a look at mmalc samples, and also RMTextFieldCell and i still don’t succeed to get the value for each cell.

    Can someone have an answer to this problem. I would like to avoid to have to use the old way to populate a tableview.
    Thanks !!

  7. Mint Says:

    I did answer to my own question:

    I have subclassed NSTableColumn and NSTextFielCell
    (my NSTextFieldCell display on line of text and 2 icons using binding and coredata)

    in NSTableColumn i subclassed:
    dataCellForRow like this:

    -(id)dataCellForRow:(int)row
    {
    OOCell *cell = [self dataCell];
    if(row >= 0)
    {
    [cell setFolderImageName:[folderImageName objectAtIndex:row]];
    if(badgeImageCount!=nil)
    {

     [cell setBadgeImageCount:[[badgeImageCount objectAtIndex:row]intValue]];
    
     }
     else
     {
     [cell setBadgeImageCount:0];
     }
     }
    

    return cell;
    }

    In fact the tableColumn use the arrangedObjects array to send data to the cell as i suppose the cells are displayed in the same order as the arrangedObjects array

    I also implemented

    • (void)observeValueForKeyPath:(NSString *)keyPath
      ofObject:(id)object
      change:(NSDictionary *)change
      context:(void *)context
      in the tableColumn

    Graphics Bindings and Clock Control from mmalc web page are very good example on what to do to subclass a NSCell and use binding

    I hope i will have the time to post my solution in a blog in sometimes

  8. Mike Says:

    Is there sample source code of this in action anywhere? I learn best by tearing it apartb and seeing what makes it tick. ;)

  9. naisioxerloro Says:

    Hi.
    Good design, who make it?

  10. martin Says:

    If you mean the website design it’s just an adoption of the kubrick design I done for myself.