Adding Views to Table Views with Core Data

I’ve been working on a new Cocoa app for Scientists for a few months now. It’s brand new software, so I’ve used the opportunity to learn Core Data (CD). Recently, I discovered an interesting use for CD: adding non-control NSViews to an NSTableView.

The view I was working with was a spinning NSProgressIndicator. I basically wanted to reproduce the sort of behavior you would see in the Mail app, where the indicator would spin during a refresh. I figured this must be a pretty standard problem, and that a solution would only be as far away as my Google search field in Safari, but that wasn’t altogether true…

The problem is that NSProgressIndicator is not an NSControl, and does not have an associated NSCell, which is required by NSTableView. I did find solutions to this online, it’s just that they weren’t straightforward. For example, I found a discussion on CocoaDev that was very helpful in understanding the issues, but didn’t provide any elegant or complete solutions. The CocoaDev page did provide me with a link to a more complete example by Joar Wingfors. The code in question worked a charm, but it was far from simple, coming in at several hundred lines. Joar’s code did inspire me though, and I have now come up with a solution using Core Data that puts a spinning indicator in my table view with less than 50 lines. Here’s how.

The trick — and this is the part that doesn’t sit completely right with me — is to add an attribute for the NSView to the Core Data entity represented in the table view. My entity was called Host, and it had an boolean attribute called isRefreshing which was set according to whether data for the entity was in the process of updating. To this entity, I added a second attribute calledrefreshProgressIndictor. Importantly, this attribute was made transient, with undefined type, so that Core Data would not attempt to save the NSProgressIndicator to file.

The refreshProgressIndicator attribute gets initialized to a newly created progress indicator in one of the awake... methods of Host:


-(void)commonAwake {
    NSProgressIndicator *indicator =
        [[[NSProgressIndicator alloc] initWithFrame:NSMakeRect(0,0,16,16)]
            autorelease];
    [indicator setStyle:NSProgressIndicatorSpinningStyle];
    [indicator setDisplayedWhenStopped:NO];
    [indicator animate:self];
    [indicator bind:@"animate" toObject:self
        withKeyPath:@"isRefreshing" options:nil];
    [self setValue:indicator forKey:@"refreshProgressIndicator"];
}
	
-(void)awakeFromInsert {
    [super awakeFromInsert];
    [self commonAwake];
}
	
-(void)awakeFromFetch {
    [super awakeFromFetch];
    [self commonAwake];
}

Note also that the animate binding of the progress indicator gets bound to the isRefreshing attribute of the Host. That way, whenever the isRefreshing attribute changes value, the progress indicator will immediately be informed by KVO, and start/stop spinning as appropriate.

I don’t feel good about adding a view like NSProgressIndicator to a model class; it messes with MVC, and disturbs me somewhat. But the solution it leads to is elegant, and given that the attribute is transient, I am able to live with it. How about you?

We now have our progress indicators, one for each Host. The next question is: How will they get displayed in the table view? Not surprisingly, we need some sort of cell. The code for this is extremely minimal. Here is the interface


@interface ViewCell : NSCell {
    NSView *view;
}
	
@end

and here the implementation


@implementation ViewCell
	
-(void)setObjectValue:(id )object {
    view = (id)object;
}
	
-(void)drawInteriorWithFrame:(NSRect)cellFrame
    inView:(NSView *)controlView {
    if( [view superview] != controlView ) {
        [controlView addSubview:view];
    }
    NSSize viewSize = [view frame].size;
    float dx = 0.5f * (cellFrame.size.width - viewSize.width);
    float dy = 0.5f * (cellFrame.size.height - viewSize.height);
    NSRect viewFrame = NSInsetRect(cellFrame, dx, dy);
    [view setFrame:viewFrame];
}
	
@end

The ViewCell assumes that the object passed to it will be the view that will appear in the table view. As the name suggests, it is not specialized to progress indicators, but should work with any NSView. When the table view draws a cell, it first calls setObjectValue. The ViewCell stores the object passed in the instance variable view for use later in the drawing methods.

The drawInteriorWithFrame:inView: method is called to actually draw the cells contents. In this case, rather than do that, the view is added as a subview to the control view passed in. That way, when the progress indicator is redrawn, the control view will also redisplay, and we will end up with an animated progress indicator, rather than a static image.

The ViewCell does not resize the view passed in drawInteriorWithFrame:inView:, but does position it in the center of the cell frame. Variations on this approach are possible, of course, and will depend on your objectives.

The ViewCell is typically created and added to the table view in the awakeFromNib method of anNSArrayController class.


-(void)awakeFromNib {
    [super awakeFromNib];
    NSTableColumn *col =
        [hostsTableView tableColumnWithIdentifier:@"refreshProgress"];
    [col setDataCell:[[[ViewCell alloc] init] autorelease]];
}

All that’s left is to bind the table column in IB to the refreshProgressIndictor attribute of the Hostentity via the arrangedObjects property of the NSArrayController. Once that’s done, theNSProgressIndicator views will be delivered to the setObjectValue: of the ViewCell, and we can conclude that some progress has been made.

Leave a Comment

Filed under Cocoa

Comments are closed.