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.