How to re-position the toggle button in NSOutlineView’s view-based ‘group style’ cell view

Published on 25/05/2012

Along with view-based NSTableView’s, Lion finally introduced what the Apple documentation refers to as ‘group style’ cell views. These have been floating around for a while in application like iTunes and offer users an unobtrusive way of collapsing items in an NSOutlineView.

This is wonderful UI and it’s a pleasure to finally be able to easily implement it in our own NSOutlineView instances. But, as I found earlier this week, things can get a little hairy when you combine ‘group style’ cell views with slightly more complex view hierarchies.

To fully understand the problem, it’s first important to realise that all instances of NSTableCellView (the class you typically subclass to create custom view-based cells) are contained within NSTableRowView instances. When ‘group style’ NSTableCellView’s are instantiated, they’re inserted into their parent NSTableRowView’s along with an NSButton instance. These NSButton instances appear to adopt a height equivalent to that of their NSTableCellView sibling, ensuring that they appear vertically centred.

But as we can see from the following illustration, this only works for simple instances of the ‘group style’ cell view, where the view’s NSTextField is also centred vertically. If we choose to move our NSTextField off-centre, things quickly get ugly.

Unfortunately, for all of my searching, there appears to be no officially sanctioned method of adjusting the ‘group style’ cell view’s toggle button position. Which means, for the moment, we’ll have to adjust it ourselves.

To accomplish this we must first tell our NSOutlineView to make use of a custom NSTableRowView subclass. Thanks to NSOutlineViewDelegate’s handy outlineView:rowViewForItem: method, this is fairly painless:

- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView rowViewForItem:(id)item
{
    CustomTableRowView *rowView = [[CustomTableRowView alloc] initWithFrame:NSZeroRect];
    return rowView;
}

Once we’ve done that, we just need to make our NSTableRowView subclass performs a quick search of its subviews to detect and reposition the NSButton instance:

- (void)drawRect:(NSRect)dirtyRect
{
	[super drawRect:dirtyRect];
 
	NSButton *toggleButton = nil;
 
	// Iterate through our children
	for(id child in [self subviews])
	{
		// Let's assume this is the NSButton we're looking for...
		if([child isKindOfClass:[NSButton class]])
		{
			toggleButton = child;
			break;
		}
	}
 
	if(toggleButton)
	{
		NSRect frame = [toggleButton frame];
 
		/* Make whatever adjustments you'd like here */
 
		toggleButton.frame = frame;
	}
}

Et voila! Nicely positioned ‘group style’ toggle buttons regardless of the complexity of your accompanying cell views.