Table View Cells from Nib: How to Change the Blueprint Data to Affect New Cells
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.
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 theNSArrayController
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
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 thatawakeFromNib()
is called each time this method is called, which means thatawakeFromNib
is also called onowner
, 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 NSTableCellView
s 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 aUITableViewCell
; 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.
- For AppKit, we have
NSTableView.registeredNibsByIdentifier
to inspect, andNSTableView.register(_:forIdentifier:)
to change Nib association per cell. - For UIKit, we have
UITableView.register(_:forCellReuseIdentifier:)
to change the association, but no way to inspect registered associations as far as I know. (Please share approaches in the comments if you know how to do this!)
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 customNSNib
subclass that performs some post-processing on the top-level objects returned from theinstantiateNib...
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.