How to Mimic the Artwork Column in Cocoa

October 4th, 2009

iTunes’ Artwork column is interesting.

The Artwork column in iTunes

It’s the leftmost column in the iTunes table view. Not only can it be unfurled at will, but it spans multiple table view rows. I’ve recently had the need to use something like the Artwork column. In fairness, my implementation is probably a bit more similar to the header in iPhone Spotlight, in that it spans across multiple rows, doesn’t necessarily take up a fixed minimal height and doesn’t invent extra rows for the duration of that minimal height. But that header also keeps the icon sticky at the top, which mine doesn’t.

It’s practically impossible to find any information about how to implement this. I’ve tapped my regular knowledgeable Cocoa contacts for hints, including the ones that are about to go kill some trees about it, and the consensus has been that it’s a doozy with no one clear approach.

A few different approaches emerged:

  • Creating a new table column and merging cells. The one reference implementation on cell merging seems to be a Dan Wood article from 2003.

    Let me be clear: age doesn’t play into whether the approach is viable since the fundamental structure of NSTableView hasn’t changed for a number for years. Lots of NeXTStep-era material is still illuminating. However, it is effectively left as an exercise to the reader how to implement integration with new features like the expansion rectangle for automatic “show the entire cell” tooltips. And the article only shows how to merge cells horizontally. I wanted vertically, which means that the code has to be reworked.

    I did this, and it did work, for some values of work. But it took a lot of effort to get there and it didn’t work very seamlessly.

  • Dumping a view into the table view. The most popular link I was shown in my research was Joar Wingfors’ seminal view-inside-a-cell trick, which was the first Cocoa trick I marveled at years back and which holds some merit. Really, this is what I want, isn’t it? Yes, mostly. The trouble is that you don’t really dump a view “inside” a cell — there’s no inside of a cell! A cell is like a magic stencil that you move around and fill out, and any illusion of actual position is mediated by a view pulling the strings. The trick hinges on placing the view inside NSTableView‘s own view hierarchy and updating it constantly as the cell is ordered to draw itself in new coordinates.

    What left me stranded with the merged cells approach was that of fighting the table view structure itself; a cell wants to be at one coordinate, and not spread thinly across many. You have to do a lot of fallible and fragile footwork to convince NSTableView that a cell at one coordinate is really the same as another, and that it has a larger extent, and that that cell should be drawn instead, despite it technically being offscreen and therefore good for culling.

    This approach didn’t live and die with cells being merged. I also quickly tried dumping a view directly inside the table view, but this was about as unsupported as you’d get. You’re highly likely to stand in the way of any assumptions under which the table view is working (the only controls that are by definition careful about messing with any subviews they didn’t stick there themselves are container views), and it didn’t work that well out of the gates. Even if I’d gotten it working, it would have been even more prone to eventual failure. I can’t rely on an implementation detail.

  • Keeping a separate view with some level of scroll view mechanism inside it, and synchronize the two. This is what ultimately turned out to work well enough.

What I did, very roughly, was this:

  1. Set up a table view.
  2. Establish a subclass of NSClipView and a custom view.
  3. Make both the clip view subclass and the custom view flipped. This may angry the blood with some people, but if you don’t, it’ll start out in the bottom left instead of the top left.
  4. Place your custom clip view next to the table view, and set its document view to be your custom view. Don’t forget the autoresizing mask if you’re doing this programmatically.
  5. Register for the table view’s scroll view’s clip view’s NSViewBoundsDidChangeNotification; inside it, set the scroll point of your custom clip view. This is genius and comes straight from CocoaDev. I love that site.
  6. Arrange, somehow, for your custom view to be resized to the appropriate height, updated with the prerequisite span information for the headers and (populated with views or custom drawing cells or custom drawing directly the headers that should be visible) (parenthesised for clarity and grouping), whenever the table view is updated. This is the mother of all reader exercises, but it will also vary heavily based on the design you settle on.

Some of you may have expected example code for this. I don’t have time to put such code together, and enough of the implementation most likely will differ that very little will be generally applicable. However, since I anguished over this for more or less a full week before I got it to work, and because Scott Stevenson cheered me on about helping Google since I had so much trouble finding it, I thought I’d document what little is firm about this.

This solution is still relatively new. I may still find that it falls apart in some situations. It’s also fairly imperfect with lots of manual synchronization, and the headers don’t integrate naturally under a column header in the table view if that’s what you’re interested in. But it’s an approach I found worked. If anyone has any ideas or code samples I am, along with the rest of the Cocoa-related readers, of which I understand there are a few, willing to listen in the comments.

Update: Jacob, the creator of Opacity, writes in with an alternate approach (includes source code): draw a slice of the image in a corresponding cell, one by one. Not a perfect solution for some more advanced scenarios, but good enough and a trifle to implement. (Also includes code for the “toggling” part of the functionality.)