-[NSTableView makeViewWithIdentifier:owner]! How Does It Work!?
By: Brian Webster |
So, I’m finally getting around to learning about view based NSTableViews, which were introduced in OS X Lion, with the intention of converting my app PlistEdit Pro to use the new type of table/outline view.
I started by reading through the docs, and looking at the TableViewPlayground sample code. Everything mostly made sense, but there was one part I couldn’t quite understand, which was the usage of the -makeViewWithIdentifier:owner: method. More specifically, I understood how you were supposed to use it, but I couldn’t figure out how it worked. I guess I’ve become less tolerant for letting things remain “magic”, so I decided to do some digging and see if I could figure out how this thing was implemented.
To start with, here’s how this method is typically used:
(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row { > > // Retrieve to get the @"MyView" from the pool or, > // if no version is available in the pool, load the Interface Builder version > NSTableCellView *result = [tableView makeViewWithIdentifier:@"MyView" owner:self]; > > // Set the stringValue of the cell's text field to the nameArray value at row > result.textField.stringValue = [self.nameArray objectAtIndex:row]; > > // Return the result > return result; > }
And here’s what the docs say about the method:
Typically, identifier is associated with a cell view that’s contained in a table’s nib file. When this method is called, the table view automatically instantiates the cell view with the specified owner, which is usually the table view’s delegate. (The owner is useful in setting up outlets and target/actions from the view.) Note that a cell view’s identifier must be the same as its table column’s identifier for bindings to work. If you’re using bindings, it’s recommended that you use the Automatic identifier setting in Interface Builder.
This method may also return a reused view with the same identifier that is no longer available on screen. If a view with the specified identifier can’t be instantiated from the nib file or found in the reuse queue, this method returns
nil
.This method is usually called by the delegate in
tableView:viewForTableColumn:row:
, but it can also be overridden to provide custom views for the identifier. Note thatawakeFromNib
is called each time this method is called, which means thatawakeFromNib
is also called on owner , even though the owner is already awake.
When you set up a view based table view in Interface Builder, you can drop a view onto one of your table’s columns, and give it an identifier using the inspector. This is the identifier that you pass into -makeViewWithIdentifier:owner:. Easy peasy.
However, each time you’re calling this method, “the table view automatically instantiates the cell view with the specified owner”. Instantiates how exactly? This certainly seems to imply it does so by the normal nib loading mechanism, which is further supported by the owner: argument, and the warning in the docs about awakeFromNib being called multiple times when using this pattern. But this view is embedded inside a table column inside your table view, which itself is archived along with whatever other objects are in the same nib file. Is it really going to instantiate this entire nib file again, just so it can go in and pluck out this one cell view, then discard the rest? Or is it using some other mechanism entirely, like calling -copy on an existing view? Neither explanation seemed right, so I started digging a bit further.
I started by taking the .xib file from the TableViewPlayground project and looking at the raw XML, so I could try to figure out how and where those cell views are stored. Opening it up in BBEdit, I found the section containing the information for the table column, which looks like so:
OK, so it looks like NSTableColumn has this “prototypeCellViews” key where those views that you drop onto the table column get stored. This obviously isn’t exposed in the public API, but it does explain where those views are stored, and how you can manage to put multiple cells with different identifiers inside a single table column (unlike a cell based table view, where each table column only has one data cell of its own). But they’re still buried deep inside the nib file - how do you instantiate these things without loading everything else as well?
The next step was to take a look at the compiled .nib file, which is created by Xcode when building your application, by running the ibtool command line tool, which translates the SCM friendly XML into the fast loading binary .nib format. If you look at this in HexFiend, it looks like this:
I’ve dealt with these some before, and they are actually blobs of archived data created by NSKeyedArchiver, which in turn ends up storing its data as a binary plist (thus the “bplist” value at the very beginning). If only I had some sort of property list editor that would let me view it…
Making your way through a raw keyed archive isn’t difficult, but it is a bit cumbersome. It basically ends up being a giant list of objects, all indexed by object ID numbers in that “$objects” dictionary. Whenever any object has a reference to another object, it uses the object ID to refer to the other object. So, to follow the graph down, you start with one ID number, find what it’s referencing, find that ID number in the big $objects dictionary, and repeat the process ad nauseum. Or, if you know what you’re looking for, you skip all that and just do a search! I did a find on “NSTableView”, and here’s what popped up:
Well that does look like a bunch of information an NSTableView would need, things like NSBackgroundColor, NSDelegate, NSEnabled, NSTableViewArchivedReusableViewsKey, NS- wait, reusable who in the what now? Let’s go further down that rabbit hole and see what ID 60 is all about…
Here’s how we can interpret object #60:
- It’s $class points to object ID 70. If we look down at object 70, it looks like that’s telling us that this object is an NSMutableDictionary.
- The dictionary has keys (NS.keys) and values (NS.objects). Looking at the first entry in each list, we have a key value pair with the key pointing the object 61 and the value pointing to object 63.
- Following those links, we see that the key is “SampleWindowCell”, which corresponds to the identifier this nib file has for a custom cell in its table view.
- The value is object 63, which appears to have… a bunch of NSNib related info. Wait, what?
- The class for object 63 is object 67, which we follow and see is in fact the NSNib class.
- Looking at that NSNibFileData key, that points us to object 64, which is a big old data blob.
OK, so what the heck is that data blob then? Let’s copy all that hex data and paste into HexFiend:
Oooh, this looks familiar, doesn’t it? Looks like another keyed archive, doesn’t it? OK, let’s save this to a file and open it in PlistEdit Pro:
Looks just like when we opened MainMenu.nib! And if we dig around in the various objects here, it looks like it does contain the components for our SimpleWindowCell view object. That’s right, we’ve solved the mystery of how these cell views are loaded, and the answer is:
Nibception!!!
Yup, we’ve got ourselves a nib inside a nib here. That’s why it doesn’t have to reload the entire thing, it just reloads the smaller nib embedded as a data blob inside the main nib. The complete sequence of steps is:
- When you do your setup in Interface Builder, it saves the data for those cells in the prototypeCellViews section that we saw in the XML for the .xib file.
- When ibtool compiles the .xib file, rather than just embedding those views inside the table view like is normally done for subviews, it instead sucks each cell view into its own separate NSNib, uses NSKeyedArchived to encode the NSNib object to a data blob, then stores that data blob inside the parent .nib file that’s being created.
- When your program runs and loads the outer nib file, those cell views don’t get instantiated immediately. Instead, those archived NSNib objects get instantiated from the data blobs and stored internally by the NSTableView.
- Finally, when you call -makeViewWithIdentifier:owner:, that finds the appropriate NSNib object for that identifier, then calls -[NSNib instantiateWithOwner:topLevelObjects:], passing along the owner argument that you gave it.
Now of course you don’t actually need to know this to use this API, but knowing how it works does put my mind at ease that there’s no funky magic going on, just some clever engineering from the folks at Apple.
OR IS IT!?