Custom Cells in an NSTableView

2.2k views Asked by At

What's the best way to populate an NSTableView with custom cells?

I always utilize Cocoa Bindings when I'm populating standard data, and I always utilize datasources when populated tables with custom cells. What I'm wondering is if there's a way of mixing the two concepts for an optimal design.

Using Xcode 3 (and therefore, IBPlugins) is sadly not an option.

1

There are 1 answers

0
ipmcc On

I, personally, wouldn't mix bound data and data sources. I've encountered nothing but pain trying to do that. I do have a couple of approaches that might help you though.

One thing I have been able to do is set a custom cell class in IB that is a subclass of one of the cells that IB knows about, and then override whatever you need to in order to make it do what you want. But first some background, and some missed attempts:

When binding cell-based NSTableViews, you typically set the bindings on the column itself and not the cell within the column. If you use a custom NSCell subclass in a table column, you'll notice that bindings like value are no longer available on the column, unlike when the cell is an NSTextFieldCell. I've tried to sorta trick IB by setting the value binding with it set up as an NSTextFieldCell, then switching out the cell -- the binding still appears in the bindings inspector, but it always crashes at runtime with this error: [<NSTableColumn 0x10252e910> valueForUndefinedKey:]: this class is not key value coding-compliant for the key value.

That brings me to the approach of subclassing one of the cells that IB knows how to bind to. I made a subclass of NSTextFieldCell, drilled-down to the "Text Field Cell - Text Cell" within the Table Column, and then set my custom subclass in the Identity Inspector. I was able to confirm that bindings still worked and IB still treats it like an NSTextFieldCell. From there, I could override any methods I wanted in my custom cell class and get custom behavior. I have no reason to believe that you couldn't do this with image cells as well. Naturally, this is kind of a bogus approach, but depending on just how "custom" your custom cells are, it might beat writing a bunch of custom code to hook up a data source.

What I found out upon further experimentation is that this is an "IB problem" and not really an NSTableView/bindings problem. And there's another pretty good way around it.

Say you want to use a custom cell, and you want to bind it to some arbitrary model object. You have your NSTableColumn Value binding bound to an NSArrayController that's vending a list of custom model objects, each with a property, call it dataForCustomCell that returns whatever the custom cell needs to do it's thing. You would set up a TextFieldCell column (like the default in IB), then bind the value binding of the NSTableColumn to Array Controller > arrangedObjects and enter the model key path dataForCustomCell. At this point, assuming the object returned by dataForCustomCell implements NSCopying (if it doesn't your app will crash, but that's not really relevant right this second) what you would see if you ran your app is that the NSTextFieldCell would call - (NSString*)description on the object returned by dataForCustomCell and put that text in the cell.

Now for the fun part: At -awakeFromNib time in your owning object (NSView, NSViewController, etc, etc), replace the dataCell (and the headerCell if you like), like this:

- (void)awakeFromNib
{
    [super awakeFromNib];
    // Assuming you've got your NSTableView plugged into an IBOutlet property called table
    NSTableColumn* col = [[self.table tableColumns] objectAtIndex:0];
    col.dataCell = [[[MYCustomCell alloc] init] autorelease];
}

Since the bindings are on the NSTableColumn and not the cell itself, you can swap out the cell without worrying about re-hooking up any bindings. In your custom cell class, override -(void)setObjectValue: and you'll get a call from the bindings mechanism at run-time, pushing in the object that came from the dataForCustomCell property on the model object corresponding to the currently drawing row of the table. (You'll also get a call passing in nil for every cell, but it seems safe to ignore that or just pass it on to super.)

One drawback of this approach is that you only get the one "Value" binding that NSTextFieldCell has. The workaround for that is to bind that Value binding to a bigger/higher "granule" in your model and then drill down and fan out multiple values in your implementation of -setObjectValue: if needed.

It ain't perfect, but it's a 'couple of lines of code' fix, instead of a 'gazillions of lines of code' fix.

Alternately, assuming you're targeting fairly recent versions of MacOS, you can also work with view-based NSTableViews. They're pretty nice, and handle bindings in a much more sensible way than NSCell-based tables do. It's a completely different way of doing things though, so it's hard to say how your task would map to it. There's a great video on the Apple developer site that brings you up to speed on NSView-based NSTableView.