NSTableView dynamic cell row height?

I'm just playing around with Xcode 7 and trying some Objects in the Interface Builder Library for OS X. One problem I can't solve myself:

I drag a new NSTableView to my ViewControllerScene. I can add text to the cells and I can add text in a new line, by pressing the ALT-key while hiting the Return-Key. So I have text in 2 or more lines. When I left the cell, the TableView only shows the first line, the rest of the text is invisible.

How can I change the cell-height of this cell dynamicly?
[If the users text have 3 lines, the height of this cell should show 3 lines height, if the users text is in the next line cell just 1 line, this row should be just big enough for 1 line.]


Is there a magical attribute I don't see? Or how would I do this dynamic cell row height in Swift for NSTableView's Cells / Rows?

Hi MuM,


I have a similar issue with dynamically sized cells in a NSTableView which I need addressed.


I may be wrong but I believe you may have to implement -tableView:heightOfRow: to return the appropriate height of the row in your table.


If there is any more interest in this post, I'd like to know how to setup a view-based NSTableView which includes a textField that grows in height as the wrapped text goes to the next line and shrinks when the user deletes lines of that textField. An example of the functionality that I am looking for is something similar (but within a table) to how the textfield works in Apple's Messages.


Any assistance would be greatly appreciated.

I’m afraid there’s no magic bullet for this. PeterNeg is right that you have to use -tableView:heightOfRow:, but it’s quite complicated to set up, because you’ll need to maintain a cache or row heights (so that you don’t recalculate them every time your delegate is queried, which will slow things down), and you’ll have to do all the height calculations on your own using NSLayoutManager methods. You’ll also need to observe changes to the frame of the edited text field, and changes to the text, and tell your table to resize accordingly.


Below is a full breakdown of how I achieve this in my app. Mine is done in NSOutlineView and in Objective-C, but the principle is the same, so you can adapt this for NSTableView and Swift.


1. Keep a dictionary of row heights. This will store the row height NSNumber against a unique ID of some sort associated with your model object.


2. Maintain an isRecalculatingRowHeights BOOL.


3. Implement a -recalculateRowHeights method which:


a. Removes all objects from the rowHeights dictionary.


b. Calls -noteHeightOfRowsWithIndexesChanged: on all rows. Bracket this call with -beingGrouping and -endGrouping for NSAnimationContext, setting the current context’s animation duration to 0 while the row heights change (otherwise, you’ll see animation as they resize, which you don’t want in this case).


c. Set isRecalcuatingRowHeights to YES at the beginning of this method and NO at the end of it.


4. Implement an -updateHeightOfRow: method which does the same but only notes the height changed for the passed-in row, removing the entry from the -rowHeights dictionary for the object at that row (rather than clearing the whole dictionary).


5. Call -recalculateRowHeights in the -outlinerViewColumnDidResize: delegate method.


6. Subclass NSTableCellView and in -viewWillMoveToWindow:, add self as an observer of the window’s “firstResponder”, or remove self as an observer if window is nil.


7. Maintain an isObservingFieldEditor BOOL property in the cell view.


8. In -observeValueForKeyPath:…, when a change in firstResponder on the window is detected:


a. Check to see if you are already observing the field editor using the BOOL created in (7). If so, remove the cell view as an observer of frame and text changes (see below).


b. Next, check to see if the first responder is of NSText type and a descendant of the table cell view. If so, add the table cell view as an observer of both NSViewFrameDidChangeNotification and NSTextDidChangeNotification, and note that you are observing the field editing using the BOOL.


9. Whenever you receive notification that the frame of the field editor has changed, send out your own notification, e.g. MyTableViewCellEditorShouldUpdateRowHeightNotification, with the cell view as the object.


10. Whenever you receive notification that the text of the field editor has changed, check whether the used rect of the text container matches the field editor frame height (e.g. if ([fieldEditor.layoutManager usedRectForTextContainer:fieldEditor.textContainer].size.height != [fieldEditor frame].size.height). This tells you whether or not the frame is already big enough to contain all the text (and only just big enough). If not, send out the same notification as in (9). (This is necessary because you’ll only receive the frame did change notification when the text field gets bigger - it won’t get smaller. So you listen for changes to the text to check if it could get smaller, e.g. because of text deletion.)

(Note: you should really add the text container inset to the used rect here; the text container inset is 0 for text fields, so it makes no difference, but that may change.)


11. In your outline controller/delegate/datasource, register as an observer of the notification you created in (9/10). Whenever you receive the notification, get the row that is affected by calling -rowForView: on [notification object] (because the cell view is the object of the notification you created in (9), and then call the -updateHeightOfRow: method you created in (4).


12. In the -outlineView:heightOfRowByItem: delegate method, get the height from your -rowHeights dictionary. If there is an entry for the height, return it (this keeps things fast). If there is no entry for the current item, you need to recalculate and recache the height (because one of your -recalculate… methods have been called). The best way of doing this is to keep an instance of your custom NSTableCellView class around purely for height calculation purposes. You’ll need to set this cell to have the same width as that of cell that’s actually in the table. You can do this by getting the column width. If it’s the outline column, you’ll need to subtract from this the indentationPerLevel * levelForItem:item in order to account for the space taken up by disclosure triangles. You can then calculate the required height as follows:


a. Get the vertical padding by subtracting the cell view’s text field frame height from the cell view’s frame height.


b. Get the height for the text based on its width using NSLayoutManager - Douglas Davidson posted code on how to that here:


http://www.cocoabuilder.com/archive/cocoa/54083-height-of-string-with-fixed-width-and-given-font.html


(Note that I have found that to precisely match NSTextField’s sizing, you’ll want to turn on NSTypesetterBehavior_10_2_WithCompatibility and inset the width by 2 points each side.)


c. Once you have the desired height of the text field, add back on the vertical padding so that you have the desired height of the whole cell view.


(I have a category on NSTableCellView that does these calculations, calling on string height measuring functions based on Douglas David’s code there, which is my main reason for keeping a spare NSTableCellView around for height measuring.)


d. Add the new value to your -rowHeights dictionary so that you don’t have to recalculate it until heights are affected by an editor or resize, then return it.


It’s actually not as onerous as it sounds; it just takes a little set-up.

Just a thought Keith, do you think there would be a way to have a dynamic row height based on autolayout? I keep thinking to myself there must be a simplar way to do this. I assumed the calls I was making in my app were just the long way.


I'm interested in your soloution though. I might try to implement something according to what you have outlined as I have now got to the stage where I simply want to start this all over again!

Actually, there is a way to have dynamic row height based on autolayout. Look at the WWDC video "Mysteries of Auto Layout, Part 1", mystery #4, "Self-Sizing Table View Cells".

Does that work on OS X? I took a look at the video, but couldn't get it working in NSTableView in either Yosemite or El Capitan. The WWDC session only showed it working in UITableView, I believe. (I don't think it would work for live editing anyway, as the NSTextField wouldn't resize until after the field editor had finished editing, so you'd still need to monitor changes to the field editor.)

Good question, but I don't know the (intended) answer. I'm not sure about the live editing aspect either, but I wanted to direct your attention to the video because it's directly related to what you've been working on for a while.

Thanks - I appreciate it. The help you and Ken gave me previously allowed me to put together the above solution, which is working really well. I'll keep my eye on how Auto-Layout progresses with this, though, since I'm gradually transitioning everything over to Auto-Layout and a solution like the one in the video would take a lot of code out of the equation.

NSTableView dynamic cell row height?
 
 
Q