View-based NSTableView with rows with dynamic heights

I have an application with an NSTableView based on a view. Inside this table view, I have rows that have cells that have content consisting of multi-line NSTextField with word NSTextField enabled. Depending on the text content of NSTextField size of the rows required to display the cell will vary.

I know that I can implement the NSTableViewDelegate method - tableView:heightOfRow: to return the height, but the height will be determined based on the word wrapping used in the NSTextField . The NSTextField word shell is likewise based on how wide the NSTextField ... is, which is determined by the width of the NSTableView .

Soooo ... I think my question is ... what is a good design template for this? Everything that I try seems to end in a tangled mess. Since TableView requires knowing the height of the cells in order to lay them out ... and NSTextField needs knowledge of its layout to determine word wrap ... and the cell needs to know the wrapper of the word to determine its height ... this is a cool mess ... and it drives me crazy.

Suggestions?

If that matters, the final result will also be editable by NSTextFields , which will resize to adjust the text inside them. I already have work on the presentation level, but the tableview has not yet adjusted the cell heights. I draw as soon as I get the height problem, I will use the noteHeightOfRowsWithIndexesChanged method to inform the table about how the height has changed ... but it is still going to ask the delegate about the height ... hence my quandry.

Thanks in advance!

+72
objective-c cocoa osx-lion macos
Sep 21 '11 at 18:10
source share
10 answers

This is a chicken and egg problem. The table must know the height of the row, because it determines where this view will be located. But you want the view already around so you can use it to determine the height of the line. So what comes first?

The answer is to save an extra NSTableCellView (or whatever view you use as your "cell view"), just for measuring the height of the view. In the delegate method tableView:heightOfRow: open your model for the "row" and set objectValue to NSTableCellView . Then set the width of the view as the width of the table, and (whatever you want) calculate the required height for this view. Return this value.

Do not call noteHeightOfRowsWithIndexesChanged: from the delegate method tableView:heightOfRow: or viewForTableColumn:row: This is bad and it will cause mega problems.

To dynamically update the height, you need to respond to the text change (through the target / action) and recalculate the calculated height of this view. Now do not dynamically change the height of the NSTableCellView (or any view that you use as your "cell view"). The table should control this view, and you will struggle with the table view if you try to set it. Instead, in your target / action for the text field in which you are calculating the height, call noteHeightOfRowsWithIndexesChanged: to allow you to resize this single row of the table. Assuming you have the autoresist mask setting right on the subviews (i.e.: subviews NSTableCellView ), everything should change in order! If not, first start working with the mask to resize the preview so that everything is correct with a variable row height.

Do not forget that noteHeightOfRowsWithIndexesChanged: animation noteHeightOfRowsWithIndexesChanged: by default. To make it inanimate:

 [NSAnimationContext beginGrouping]; [[NSAnimationContext currentContext] setDuration:0]; [tableView noteHeightOfRowsWithIndexesChanged:indexSet]; [NSAnimationContext endGrouping]; 

PS: I answer more questions posted on Apple Dev forums than stack overflow.

PSS: I wrote based on the NSTableView view

+120
Nov 08 '11 at 16:57
source share

For those who want more code, here is the complete solution I used. Thanks to corbin dunn for pointing me in the right direction.

I needed to set the height mainly depending on how high a NSTextView in my NSTableViewCell .

In my subclass of NSViewController I temporarily create a new cell by calling outlineView:viewForTableColumn:item:

 - (CGFloat)outlineView:(NSOutlineView *)outlineView heightOfRowByItem:(id)item { NSTableColumn *tabCol = [[outlineView tableColumns] objectAtIndex:0]; IBAnnotationTableViewCell *tableViewCell = (IBAnnotationTableViewCell*)[self outlineView:outlineView viewForTableColumn:tabCol item:item]; float height = [tableViewCell getHeightOfCell]; return height; } - (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item { IBAnnotationTableViewCell *tableViewCell = [outlineView makeViewWithIdentifier:@"AnnotationTableViewCell" owner:self]; PDFAnnotation *annotation = (PDFAnnotation *)item; [tableViewCell setupWithPDFAnnotation:annotation]; return tableViewCell; } 

In my IBAnnotationTableViewCell , which is the controller of my cell (subclass of NSTableCellView ), I have a setup method

 -(void)setupWithPDFAnnotation:(PDFAnnotation*)annotation; 

which sets all outputs and sets the text from my PDFAnnotations. Now I can "easily" calculate the height using:

 -(float)getHeightOfCell { return [self getHeightOfContentTextView] + 60; } -(float)getHeightOfContentTextView { NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[self.contentTextView font],NSFontAttributeName,nil]; NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:[self.contentTextView string] attributes:attributes]; CGFloat height = [self heightForWidth: [self.contentTextView frame].size.width forString:attributedString]; return height; } 

.

 - (NSSize)sizeForWidth:(float)width height:(float)height forString:(NSAttributedString*)string { NSInteger gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ; NSSize answer = NSZeroSize ; if ([string length] > 0) { // Checking for empty string is necessary since Layout Manager will give the nominal // height of one line if length is 0. Our API specifies 0.0 for an empty string. NSSize size = NSMakeSize(width, height) ; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:size] ; NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:string] ; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init] ; [layoutManager addTextContainer:textContainer] ; [textStorage addLayoutManager:layoutManager] ; [layoutManager setHyphenationFactor:0.0] ; if (gNSStringGeometricsTypesetterBehavior != NSTypesetterLatestBehavior) { [layoutManager setTypesetterBehavior:gNSStringGeometricsTypesetterBehavior] ; } // NSLayoutManager is lazy, so we need the following kludge to force layout: [layoutManager glyphRangeForTextContainer:textContainer] ; answer = [layoutManager usedRectForTextContainer:textContainer].size ; // Adjust if there is extra height for the cursor NSSize extraLineSize = [layoutManager extraLineFragmentRect].size ; if (extraLineSize.height > 0) { answer.height -= extraLineSize.height ; } // In case we changed it above, set typesetterBehavior back // to the default value. gNSStringGeometricsTypesetterBehavior = NSTypesetterLatestBehavior ; } return answer ; } 

.

 - (float)heightForWidth:(float)width forString:(NSAttributedString*)string { return [self sizeForWidth:width height:FLT_MAX forString:string].height ; } 
+7
Sep 10 '13 at 12:23
source share

Based on Corbin's answer (btw thanks for shedding light on this):

Swift 3, NSTableView based on auto-view view for macOS 10.11 (and higher)

My setup: I have an NSTableCellView that is laid out using Auto-Layout. It contains (among other elements) a multi-line NSTextField , which can contain up to two lines. Therefore, the height of the presentation of the entire cell depends on the height of this text box.

I am updating the message as a table to update the height in two cases:

1) When resizing a table:

 func tableViewColumnDidResize(_ notification: Notification) { let allIndexes = IndexSet(integersIn: 0..<tableView.numberOfRows) tableView.noteHeightOfRows(withIndexesChanged: allIndexes) } 

2) When changing the object of the data model:

 tableView.noteHeightOfRows(withIndexesChanged: changedIndexes) 

This will cause the table view to ask the delegate for the new row height.

 func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { // Get data object for this row let entity = dataChangesController.entities[row] // Receive the appropriate cell identifier for your model object let cellViewIdentifier = tableCellViewIdentifier(for: entity) // We use an implicitly unwrapped optional to crash if we can't create a new cell view var cellView: NSTableCellView! // Check if we already have a cell view for this identifier if let savedView = savedTableCellViews[cellViewIdentifier] { cellView = savedView } // If not, create and cache one else if let view = tableView.make(withIdentifier: cellViewIdentifier, owner: nil) as? NSTableCellView { savedTableCellViews[cellViewIdentifier] = view cellView = view } // Set data object if let entityHandler = cellView as? DataEntityHandler { entityHandler.update(with: entity) } // Layout cellView.bounds.size.width = tableView.bounds.size.width cellView.needsLayout = true cellView.layoutSubtreeIfNeeded() let height = cellView.fittingSize.height // Make sure we return at least the table view height return height > tableView.rowHeight ? height : tableView.rowHeight } 

First, we need to get our model object for the row ( entity ) and the corresponding cell identifier. Then we check if we really created a view for this identifier. To do this, we must maintain a list with cell views for each identifier:

 // We need to keep one cell view (per identifier) around fileprivate var savedTableCellViews = [String : NSTableCellView]() 

If none are saved, we need to create (and cache) a new one. We update the cell view using our model object and tell it to reinstall everything based on the current table view width. You can use the fittingSize height as the new height.

+6
Mar 17 '17 at 9:34 on
source share

Since I use a custom NSTableCellView , and I have access to NSTextField , my solution was to add a method on NSTextField .

 @implementation NSTextField (IDDAppKit) - (CGFloat)heightForWidth:(CGFloat)width { CGSize size = NSMakeSize(width, 0); NSFont* font = self.font; NSDictionary* attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]; NSRect bounds = [self.stringValue boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:attributesDictionary]; return bounds.size.height; } @end 
+3
Feb 01 '16 at 5:11
source share

This has become much easier on macOS 10.13 with .usesAutomaticRowHeights . Details here: https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/#10_13 (In the section "Automatic line heights NSTableView").

Basically, you just select NSTableView or NSOutlineView in the storyboard editor and select this option in the Size Inspector:

enter image description here

Then you set the material in your NSTableCellView to have upper and lower limits for the cell, and your cell will resize to fit automatically. No code required!

Your application will ignore any heights specified in heightOfRow ( NSTableView ) and heightOfRowByItem ( NSOutlineView ). You can see what heights are calculated for your automatic layout lines using this method:

 func outlineView(_ outlineView: NSOutlineView, didAdd rowView: NSTableRowView, forRow row: Int) { print(rowView.fittingSize.height) } 
+3
Nov 01 '17 at 23:27
source share

Have you seen RowResizableViews ? It is quite old, and I have not tested it, but it can still work.

+2
21 Sep '11 at 21:13
source share

Here is what I did to fix this:

Source: Check out the Xcode documentation in the “nstableview Line Height” section. You will find an example source code named "TableViewVariableRowHeights / TableViewVariableRowHeightsAppDelegate.m"

(Note: I look at column 1 as a table, you need to configure it elsewhere)

in Delegate.h

 IBOutlet NSTableView *ideaTableView; 

in Delegate.m

Table displays row height control delegates

  - (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row { // Grab the fully prepared cell with our content filled in. Note that in IB the cell Layout is set to Wraps. NSCell *cell = [ideaTableView preparedCellAtColumn:1 row:row]; // See how tall it naturally would want to be if given a restricted with, but unbound height CGFloat theWidth = [[[ideaTableView tableColumns] objectAtIndex:1] width]; NSRect constrainedBounds = NSMakeRect(0, 0, theWidth, CGFLOAT_MAX); NSSize naturalSize = [cell cellSizeForBounds:constrainedBounds]; // compute and return row height CGFloat result; // Make sure we have a minimum height -- use the table set height as the minimum. if (naturalSize.height > [ideaTableView rowHeight]) { result = naturalSize.height; } else { result = [ideaTableView rowHeight]; } return result; } 

you will also need this to create a new line height (delegated method)

 - (void)controlTextDidEndEditing:(NSNotification *)aNotification { [ideaTableView reloadData]; } 

Hope this helps.

Final note: this does not support changing column widths.

+2
Sep 17 '12 at 9:32
source share

I searched for a solution for quite a while and came up with the following, which works fine in my case:

 - (double)tableView:(NSTableView *)tableView heightOfRow:(long)row { if (tableView == self.tableViewTodo) { CKRecord *record = [self.arrayTodoItemsFiltered objectAtIndex:row]; NSString *text = record[@"title"]; double someWidth = self.tableViewTodo.frame.size.width; NSFont *font = [NSFont fontWithName:@"Palatino-Roman" size:13.0]; NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]; NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:text attributes:attrsDictionary]; NSRect frame = NSMakeRect(0, 0, someWidth, MAXFLOAT); NSTextView *tv = [[NSTextView alloc] initWithFrame:frame]; [[tv textStorage] setAttributedString:attrString]; [tv setHorizontallyResizable:NO]; [tv sizeToFit]; double height = tv.frame.size.height + 20; return height; } else { return 18; } } 
+2
Aug 25 '15 at 9:41
source share

This is very similar to what I had to do before. I would like to say that I came up with a simple, elegant solution, but, alas, I did not. Not due to lack of attempts. Since you have already noticed that a UITableView needs to know the height before the constructed cells really do everything, they seem pretty circular.

My best solution was to push the logic into the cell, because at least I could highlight the class needed to understand how the cells were laid out. A method like

 + (CGFloat) heightForStory:(Story*) story 

will be able to determine how tall the cell should be. Of course, this is related to measuring text, etc. In some cases, I have developed ways to cache information obtained through this method, which can then be used to create a cell. That was the best I came up with. This is a rabies issue, although there seems to be a better answer.

+1
Sep 30 2018-11-11T00:
source share

Here is a solution based on JanApotheker answer, changed as cellView.fittingSize.height , did not return me the correct height. In my case, I use the standard NSTableCellView , NSAttributedString for the text of the textField cell and one column table with restrictions for the textField cell set to IB.

In my opinion, the controller, I declare:

 var tableViewCellForSizing: NSTableCellView? 

In viewDidLoad ():

 tableViewCellForSizing = tableView.make(withIdentifier: "My Identifier", owner: self) as? NSTableCellView 

Finally, for the tableView delegate method:

 func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { guard let tableCellView = tableViewCellForSizing else { return minimumCellHeight } tableCellView.textField?.attributedStringValue = attributedString[row] if let height = tableCellView.textField?.fittingSize.height, height > 0 { return height } return minimumCellHeight } 

mimimumCellHeight is a constant value of 30 for backup but is never used. attributedStrings is my NSAttributedString model array.

This works great for my needs. Thanks for all the previous answers that pointed me in the right direction to this nasty problem.

+1
May 29 '17 at 16:56
source share



All Articles