Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

View-based NSTableView, make row height dependent on content

I need a little help with NSTableView and dynamic row height

What I have:

A single-column view-based NSTableVIew bound to an array controller. Each NSTableCellView contains three subviews: NSImageView, NSTextField (single line) and NSTextField (multiline). Basically, this is a chat interface, so you would have a list of messages, senders and avatars.

What I want to achieve:

When text is longer than minimum height of the row, the row expands to fit content. Much like iMessage, the bubbles expand to fit message.

...which seems like a very natural thing to do, but out of all the relevant solutions I found online, (Ref 1, Ref 2), none of which works for me.

Ref 2 looks fantastically written but none of that applies to my application since the example project uses third-party auto layout code and the entire thing is designed for iOS. Ref 1 gave a very promising solution written in, well, English. I tried to set it up using a "dummy view" like described in the solution, but failed to even properly change and measure the height.

Here's my code for tableView:heightOfRow:, _samplingView is the dummy view, which has working constraints and is identical to the one in tableView.

- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
    NSTextField *textField;
    NSTextFieldCell *messageCell;
    for (NSView *subview in [_samplingView subviews]) {
        if ([[subview identifier] isEqualToString:@"message"]) {
            textField = (NSTextField*)subview;
            messageCell = ((NSTextField*)subview).cell;
        }
    }
    Message *message = [[_messagesArrayController arrangedObjects] objectAtIndex:row];
    _samplingView.objectValue = message;

    CGFloat width = [[[tableView tableColumns] objectAtIndex:1] width];
    [_samplingView setBounds:NSMakeRect(0, 0, width, CGFLOAT_MAX)];
    [_samplingView display];

    CGFloat optimalHeight = 10 + [messageCell cellSize].height; //messageCell's size stays the same when I change samplingView to try to measure the height
    return optimalHeight;

}

Result: all row heights remain the same, somehow when I change the width of _samplingView, it doesn't re-size messageCell's size. I thought auto-layout would take care of this compression/expansion and allow me to measure the height. Indeed, I am very confused.

Edit: for reference, this is what my view looks like

     +-----------+---------------------------------------------------+
     |           |  NSTextField                                      |
     |NSImageView|  sender                                           |
     |  avatar   +---------------------------------------------------+
     |           |                                                   |
     |           |  NSTextField (multiline)                          |
     +-----------|  message                                          |
     |           |                                                   |
     |           |  (high compression/hugging priority)              |
     |           |  (this view should decide the height of row)      |
     |           |                                                   |
     +-----------+---------------------------------------------------+
like image 542
Dovizu Avatar asked Jan 18 '14 11:01

Dovizu


2 Answers

You need to call the tableView's noteHeightOfRowsWithIndexesChanged: method when the width of samplingView changes. This method is mentioned in your "Ref 1." As it says in the method's documentation:

If the delegate implements tableView:heightOfRow: this method immediately re-tiles the table view using the row heights the delegate provides.

For NSView-based tables, this method will animate. To turn off the animation, create an NSAnimationContext grouping and set the duration to 0. Then call this method and end the grouping.

The table view has the row heights cached. Conceptually, when you want to change a row height, you have to mark the height as dirty to get the delegate method you've implemented re-invoked for the rows you specify. There is nothing like cocoa-bindings using KVO to watch for changes in all the keyPaths that affect the rowHeight, at least not by default.

like image 82
stevesliva Avatar answered Nov 15 '22 06:11

stevesliva


First, you should not be setting the bounds of the dummy view. If you were not using auto layout, you would set the frame.

Given that you are using auto layout, you should not do either. You should temporarily add a constraint to the dummy view restricting its width.

Then, don't call -display. Call -layoutSubtreeIfNeeded and then query the dummy view's fittingSize. The row height should be the height from the fittingSize. I'm not sure why you're trying to base it on the messageCell's height plus some arbitrary magic number. It should not be necessary to access the text field or its cell.

You say you have the constraints for the cell view working properly. However, you will need to make sure you have constraints that make the cell view's height large enough to hold the subviews. This can be sort of a problem, since the table view will control the height of the real (non-dummy) cell view and if the table view temporarily makes it too short to hold the subviews with desired spacing, you'll get unsatisfiable constraints errors. So, the solution is to reduce the priority of the vertical constraint on the cell view's bottom (or height) to be below 1000 (required). It could be 250 (low). It just needs to be greater than 50, which is NSLayoutPriorityFittingSizeCompression. That's the priority of the constraints that fittingSize temporarily uses to try to make the view as small as possible.

That should get the height right for initial content.

As your Ref 1 (and stevesilva) notes, you will need to somehow observe changes in the content of your variable-height multiline text field. When it changes, you'll need to tell the table view that the row height (may have) changed by calling -noteHeightOfRowsWithIndexesChanged: on it.

Lastly, you should strive to make -tableView:heightOfRow: as efficient as possible. Therefore, I suggest that you compute all of the row heights the first time any is requested and cache them. -tableView:heightOfRow: should just return a value from the cache. Therefore, when you observe that a message has changed, you should compute a new height for that row and compare it to the cached height. If and only if the height has actually changed — not all changes to the content will change the height — store the new value in the cache and tell the table the height has changed.

like image 34
Ken Thomases Avatar answered Nov 15 '22 08:11

Ken Thomases