Automatically adjust height of a NSTableView

10,121

Solution 1

This answer is for Swift 4, targeting macOS 10.10 and later:

1. Answer

You can use the table view's fittingSize to calculate the size of your popover.

tableView.needsLayout = true
tableView.layoutSubtreeIfNeeded()

let height = tableView.fittingSize.height

2. Answer

I understand your desire to move that code out of the view controller but since the table view itself knows nothing about the number of items (only through delegation) or model changes, I would put that in the view controller. Since macOS 10.10, you can use preferredContentSize on your NSViewController inside a popover to set the size.

func updatePreferredContentSize() {
    tableView.needsLayout = true
    tableView.layoutSubtreeIfNeeded()

    let height = tableView.fittingSize.height
    let width: CGFloat = 320

    preferredContentSize = CGSize(width: width, height: height)
}

In my example, I'm using a fixed width but you could also use the calculated one (haven't tested it yet).

You would want to call the update method whenever your data source changes and/or when you're about to display the popover.

I hope this solves your problem!

Solution 2

You can query the frame of the last row to get the table view's height:

- (CGFloat)heightOfTableView:(NSTableView *)tableView
{
    NSInteger rows = [self numberOfRowsInTableView:tableView];
    if ( rows == 0 ) {
        return 0;
    } else {
        return NSMaxY( [tableView rectOfRow:rows - 1] );
    }
}

This assumes an enclosing scroll view with no borders!

You can query the tableView.enclosingScrollView.borderType to check whether the scroll view is bordered or not. If it is, the border width needs to be added to the result (two times; bottom and top). Unfortunately, I don't know of the top of my head how to get the border width.

The advantage of querying rectOfRow: is that it works in the same runloop iteration as a [tableView reloadData];. In my experience, querying the table view's frame does not work reliably when you do a reloadData first (you'll get the previous height).

Solution 3

Interface Builder in Xcode automatically puts the NSTableView in an NSScrollView. The NSScrollView is where the headers are actually located. Create a NSScrollView as your base view in the window and add the NSTableView to it:

NSScrollView * scrollView = [[NSScrollView alloc]init];
[scrollView setHasVerticalScroller:YES];
[scrollView setHasHorizontalScroller:YES];
[scrollView setAutohidesScrollers:YES];
[scrollView setBorderType:NSBezelBorder];
[scrollView setTranslatesAutoresizingMaskIntoConstraints:NO];

NSTableView * table = [[NSTableView alloc] init];
[table setDataSource:self];
[table setColumnAutoresizingStyle:NSTableViewUniformColumnAutoresizingStyle];
[scrollView setDocumentView:table];

//SetWindow's main view to scrollView

Now you can interrogate the scrollView's contentView to find the size of the NSScrollView size

NSRect rectOfFullTable = [[scrollView contentView] documentRect];

Because the NSTableView is inside an NSScrollView, the NSTableView will have a headerView which you can use to find the size of your headers.

You could subclass NSScrollView to update it's superview when the table size changes (headers + rows) by overriding the reflectScrolledClipView: method

Solution 4

I'm not sure if my solution is any better than what you have, but thought I'd offer it anyway. I use this with a print view. I'm not using Auto Layout. It only works with bindings – would need adjustment to work with a data source.

You'll see there's an awful hack to make it work: I just add 0.5 to the value I carefully calculate.

This takes the spacing into account but not the headers, which I don't display. If you are displaying the headers you can add that in the -tableView:heightOfRow: method.

In NSTableView subclass or category:

- (void) sizeHeightToFit {
    CGFloat height = 0.f;
    if ([self.delegate respondsToSelector:@selector(tableView:heightOfRow:)]) {
        for (NSInteger i = 0; i < self.numberOfRows; ++i)
            height = height +
                     [self.delegate tableView:self heightOfRow:i] +
                     self.intercellSpacing.height;
    } else {
        height = (self.rowHeight + self.intercellSpacing.height) *
                 self.numberOfRows;
    }

    NSSize frameSize = self.frame.size;
    frameSize.height = height;
    [self setFrameSize:frameSize];
}

In table view delegate:

// Invoke bindings to get the cell contents
// FIXME If no bindings, use the datasource
- (NSString *) stringValueForRow:(NSInteger) row column:(NSTableColumn *) column {
    NSDictionary *bindingInfo = [column infoForBinding:NSValueBinding];

    id object = [bindingInfo objectForKey:NSObservedObjectKey];
    NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];

    id value = [[object valueForKeyPath:keyPath] objectAtIndex:row];

    if ([value isKindOfClass:[NSString class]])
        return value;
    else
        return @"";
}

- (CGFloat) tableView:(NSTableView *) tableView heightOfRow:(NSInteger) row {

    CGFloat result = tableView.rowHeight;

    for (NSTableColumn *column in tableView.tableColumns) {
        NSTextFieldCell *dataCell = column.dataCell;
        if (![dataCell isKindOfClass:[NSTextFieldCell class]]) continue;

        // Borrow the prototype cell, and set its text
        [dataCell setObjectValue:[self stringValueForRow:row column:column]];

        // Ask it the bounds for a rectangle as wide as the column
        NSRect cellBounds = NSZeroRect;
        cellBounds.size.width = [column width]; cellBounds.size.height = FLT_MAX;
        NSSize cellSize = [dataCell cellSizeForBounds:cellBounds];

        // This is a HACK to make this work.
        // Otherwise the rows are inexplicably too short.
        cellSize.height = cellSize.height + 0.5;

        if (cellSize.height > result)
            result = cellSize.height;
    }

    return result;
}
Share:
10,121

Related videos on Youtube

IluTov
Author by

IluTov

I'm a Swiss developer. Just a guy who enjoys programming.

Updated on June 29, 2022

Comments

  • IluTov
    IluTov almost 2 years

    I have asked this question once before, but I'm just not very satisfied with the solution.

    Automatically adjust size of NSTableView

    I want to display a NSTableView in a NSPopover, or in a NSWindow.
    Now, the window's size should adjust with the table view.

    Just like Xcode does it:


    enter image description here enter image description here


    This is fairly simple with Auto Layout, you can just pin the inner view to the super view.

    My problem is, that I can't figure out the optimal height of the table view. The following code enumerates all available rows, but it doesn't return the correct value, because the table view has other elements like separators, and the table head.

    - (CGFloat)heightOfAllRows:(NSTableView *)tableView {
        CGFloat __block height;
        [tableView enumerateAvailableRowViewsUsingBlock:^(NSTableRowView *rowView, NSInteger row) {
            // tried it with this one
            height += rowView.frame.size.height;
    
            // and this one
            // height += [self tableView:nil heightOfRow:row];
        }];
    
        return height;
    }
    

    1. Question

    How can I fix this? How can I correctly calculate the required height of the table view.

    2. Question

    Where should I run this code?
    I don't want to implement this in a controller, because it's definitely something that the table view should handle itself.
    And I didn't even find any helpful delegate methods.

    So I figured best would be if you could subclass NSTableView.
    So my question 2, where to implement it?


    Motivation

    Definitely worth a bounty

  • IluTov
    IluTov over 11 years
    Thanks for your answers. But it's actually about the rows, not the columns. I tried your solution, just returns the height of the table view with all the unnecessary whitespace.
  • Fruity Geek
    Fruity Geek over 11 years
    Yes. The contentView is a NSClipView which is never smaller than the size of your NSScrollView. The NSTableView relies on being inside a NSScrollView for drawing headers and minimizing the controls that are rendered to save memory. So your NSScrollView must have a minimum size (even if it's very very small). You can then tie the parent view or window of the scroll view to be the NSScrollView's full desired size.
  • IluTov
    IluTov over 11 years
    I'm not sure how this should help me... The thing is, that the NSTableView sometimes is smaller than the scroll view or clip view, that's the point...
  • Fruity Geek
    Fruity Geek over 11 years
    The NSTableView will not be smaller than your NSScrollView if your NSScrollView's minimum size is very very small (1px by 1px).
  • IluTov
    IluTov over 11 years
    Ok, I get what you mean now, but still, what if the content of the table view changes again and is now smaller?
  • Fruity Geek
    Fruity Geek over 11 years
    Check out reflectScrolledClipView: in NSScrollView. You can subclass NSScrollView and only override this method to publish changes to the parent of NSScrollView.
  • IluTov
    IluTov over 11 years
    Ok thats one problem less, but [[scrollView contentView] documentRect] still returns the wrong value. I have a small table view with 3 rows, the pixels returned were 282...
  • Fruity Geek
    Fruity Geek over 11 years
    The NSTableView is the NSScrollView's documentView property. You can get the height of rows in the NSTableView by [[scrollView documentView] bounds]
  • IluTov
    IluTov over 11 years
    Actually no, because the whitespace is part of the table view. You can see it if you have a table view with alternating rows, you can see that the table view is always at least as large as the scroll view
  • Fruity Geek
    Fruity Geek over 11 years
    So to go back to your original question, you wanted to know why you weren't finding the size of your header views. You weren't finding them because they aren't in the NSTableView unless the NSTableView is inside a NSScrollView. Once you put the NSTableView inside an NSScrollView, you can interrogate the headerView of the NSTableView to find the size of the headers to add to your calculated size.
  • IluTov
    IluTov over 11 years
    But it's not only the header, it's the separators too, I'm not even using a header in the table view I'm testing it with. I can upload the project onto Github if you want, you'll see that it doesn't return the correct size...
  • Fruity Geek
    Fruity Geek over 11 years
    The separators are drawn by your NSTableRowView. Your delegate must implement tableView:heightOfRow: correctly. When you change a row's height (by adding a separator) you must invalidate the existing row height by calling noteHeightOfRowsWithIndexesChanged:.
  • IluTov
    IluTov over 11 years
    It does implement tableView:heightOfRow:, I never change the rows height, I think we could go on like this for hours ^^ I'll upload the project and I'll come back to you. Thanks for your help so far!
  • IluTov
    IluTov over 11 years
    Sorry it took a while to get back to you
  • IluTov
    IluTov over 11 years
    github.com/iluuu1994/ITResizingTableView It almost works, problem is, that it can't resize if the content is smaller, and there are a few pixels which are wrong. Maybe you can help me here.
  • IluTov
    IluTov over 11 years
    I'm not looking for the frame, I'm looking for the optimal frame
  • Remizorrr
    Remizorrr over 11 years
    If you have 100 rows, frame.height will be right what you need.
  • IluTov
    IluTov over 11 years
    I need to know the height of 2, or 3, or 4 rows. The blank space belongs to the table-view too.
  • IluTov
    IluTov over 11 years
    Hey man, thanks a lot. I really appreciate it. I chose Geoffrey Marshall's answer however, because he got it working without any dirty hacks.