Table View Cells from Nib: How to Change the Blueprint Data to Affect New Cells

null

In a StackOverflow question, Thomas Tempelmann asked:

How to get all TableCellView prototypes from a TableView object?

[…]

How do I get to that TableCellView in its prototype form when I only have a reference to its TableView object, so that I can alter its tooltip for all rows added later?

(Emphasis Thomas’s.)

So given there’s a Nib with a NSTableView and inside you have NSTableViewCell “prototypes”, and the cells have a tooltip (inherited from NSView). Say the original value is “old tooltip”. At some point in time, the value of tooltips for new cells after that point should change to “new tooltip”.

Thomas’s specific ask is to open the blueprint derived from the Nib, and change that.

I believe this is not possible with AppKit or UIKit API.

Here’s why I come to that conclusion, so that in case I’m wrong (or even if I’m right), the reasoning is still valuable.

I’ll also explain ways you can get similar results without fiddling with the Nib, because the specific objective to change the Nib is not the pathway laid out for us by the frameworks, be it AppKit or UIKit.

Table of Contents
  1. There Is No In-Memory Blueprint: Each New Cell is Created Directly from Nib
    1. Inspecting awakeFromNib Usage
    2. Programmatic Write Access to Nib Internals is Not Possible
    3. Inspecting (and Changing) Nib-to-Cell Association
  2. Actual Ways to Solve the Underlying Problem of Changing the Default Tooltip Value
    1. Programmatically Influence Object Restoration from Nib
  3. Using NSTableView Callbacks like Apple Intended
  4. Conclusion: Use the Delegate

There Is No In-Memory Blueprint: Each New Cell is Created Directly from Nib

From the question, there’s this line of argument:

There must be a way, because how would a TableView know how to add a row when using a NSArrayController to manage the table view rows, and my code does not explicitly reference these prototypes at all, which means that the NSArrayController must have a way to find these cell prototypes – and I want to achieve the same.

There is a way to know the Nib where the information comes from, we’ll come to that in a minute. But first, I want to clarify that the cell blueprint is not stored as an object we can fiddle with.

Inspecting awakeFromNib Usage

This is not the full picture, but a key part of the assembly of a cell from Nib and a data source

For each NSTableViewRow and NSTableViewCell that is being created, the owner receives an -awakeFromNib: message. This is true no matter the data source of the table view (both for manual NSDataSource implementation or NSArrayController).

To get there, the NSTableViewDelegate usually calls NSTableView.makeView(withIdentifier:owner:), passing self as the owner. That’s why the owner receives a new awakeFromNib callback:

This method is usually called by the delegate in tableView(_:viewFor:row:), but it can also be overridden to provide custom views for the identifier. Note that awakeFromNib() is called each time this method is called, which means that awakeFromNib is also called on owner, even though the owner is already awake.

(Apple Docs: NSTableView.makeView(withIdentifier:owner:))

If you don’t implement the NSTableViewDelegate, the cell from the Nib will be loaded nevertheless. The table calls its makeView(withIdentifier:owner:) method directly with nil as the owner.

Sticking to a NSArrayController as data source, you can still use this process.

Programmatic Write Access to Nib Internals is Not Possible

Nibs are loaded as opaque data blobs and have no public surface API: neither the NSNib or UINib classes offer API to modify any of its objects.

You can’t modify the cell blueprint’s tooltip in the NSNib object after it’s loaded. But you could load different Nibs for new cells.

Let me put “new” in airquotes, first, though: "new". I’ll explain in a second.

Inspecting (and Changing) Nib-to-Cell Association

It’s possible to design your NSTableCellViews in dedicated Nibs. I don’t know who does that on a regular basis (I usually don’t, but I have once for a social media timeline view). The fact that this is possible is useful information to think about table and cells and Nib associations more atomically. Because your monolithic Nib behaves the same under the hood.

The following is from a source on UIKit, but similar constraints apply to AppKit Nibs if you want to e.g. design different cells in separate Nib files:

This nib is expected to have exactly one top-level object, and that
top-level object is expected to be a UITableViewCell; that being so,
the cell can easily be extracted from the resulting NSArray, as it
is the array’s only element. Our nib meets those expectations! Problem solved.

(Matt Neuburg: Programming iOS 6; Designing a cell in a nib})

While inaccurate, I found working with the following mental model to be helpful to work with ever since I read Matt Neuburg’s text in 2014: the table view controller’s Nib is, for all intents and purposes, treated like you have a dedicated Nib for the cell view.

This means that even though you have 1 large Nib with N cell views inside, the table view will behave as if you had N cell view Nibs.

This cell-to-Nib accociation is also inspectable during runtime, in code. You can change this at any point in time.

While you could change the associated NSNib or UINib object per cell identifier anytime, this won’t help to achieve the goal to replace the default tooltip or any other property of the cells.

The problem with changing the cell-to-Nib association is cell re-use. As long as there are leftover cells on the table view’s re-use queue, you won’t get any new instances. If you change the cell-to-Nib association with the intent to affect all the cells that henceforth appeareth on screen, then you’ll be having a bad time, I believe: cells that appear oon-screenafter the initial setup phase and a bit of scrolling aren’t actually new, but re-used cell objects. They will still look like they did before.

Actual Ways to Solve the Underlying Problem of Changing the Default Tooltip Value

After all this analysis, here’s how to change the tooltip of new cells. (And what ‘new’ actually means.)

Programmatically Influence Object Restoration from Nib

You can’t programmatically set the blueprint of the NSNib that’s being loaded, but you can hook into the process of ‘awaking’ objects:

You can subclass NSNib if you want to extend or specialize nib-loading behavior. For example, you could create a custom NSNib subclass that performs some post-processing on the top-level objects returned from the instantiateNib... methods

(Apple Docs: NSNib; UINib does not talk about subclassing but the mechanism is similar)

So when your NSTableViewCell is being instantiated, you can replace the tooltip as a part of “some post-processing on the top-level objects”.

Note that it says top-level.

To make this work, you will likely want a dedicated Nib per cell type so that the NSTableViewCell is a top-level object!

But how do you change the tooltip’s future string value? With your NSNib subclass, you will either have a store property with a tooltip override, or maybe even a static var on the subclass’s type for convenience. Not that it matters, it’s not really ‘shared’ state.

This may solve the problem in a convoluted way, but it’s not very nice to use. And you still have to store the tooltip: String somewhere outside the Nib itself since there is no data storage like a dictionary of a cell blueprint’s properties. Representing the new tooltip (but not the original one baked into the Nib) becomes your responsibility.

Using NSTableView Callbacks like Apple Intended

So even though you can, technically, change the NSNib instace at runtime, you can’t modify the tooltip value directly inside it. You would need to store the tooltip string separately, and influence cell loading from the Nib to start with the new value. This doesn’t win you much.

All this ignores that you won’t even be affecting tooltips of re-used cells this way. That’s probably going to be a major problem once a screenful of cells has been loaded into memory!

Since the tooltip string needs to be stored somewhere under your control anyway, you can use the proper customization point: NSTableViewDelegate.tableView(_:viewFor:row:).

This is being used to request cells. The delegate asks the table to make a view, but the table view may internally re-use a cell view that wasn’t on screen anymore. No matter where the view object comes from, the delegate needs to tweak the label and other sub-views to reflect the contents for the cell at row and column.

This is arguably what people mean when they talk about ‘new cells’: cells that draw on screen at a position where previously there was no cell. Cell re-use is just a memory-efficient way to get there without actually creating a new cell view object.

Conclusion: Use the Delegate

Representing the tooltip to put into cells as a stored property in the NSTableViewDelegate object (which is probably your view controller in simple setups) is just as much work as storing it in a NSNib subclass.

Using the table view delegate is much nicer API to work with, though.

  • You are not forced to a one-Nib-per-cell rule (to have the cell view become the top-level Nib object to tweak in your subclass.)
  • You will be using the same API that 99% of table view programmers use. No surprises and home-cooked solutions leads to better maintainability.
  • You will have a customization point that’s useful in the future, while NSNib subclasses sound rather limited. (You can even ditch the Nib-loading part in the delegate and create cell views programmatically. That’s a weak sign, but it’s a sign nevertheless, that this is the better, more powerful entry point to affect where cells come from.)

The result of all this investigating is rather boring: use the delegate callbacks that people have been using for many, many boring years.