Search code examples
macoscocoanstableviewnsscrollview

Keep same NSTableView scrolled position while inserting rows


I've got a view-based NSTableView showing a timeline of messages/informations. Row heights are variable. New messages are regularly added at the top of the table by using insertRows:

NSAnimationContext.runAnimationGroup({ (context) in
    context.allowsImplicitAnimation = true
    self.myTable.insertRows(at: indexSet, withAnimation: [.effectGap])
})

While the user stays at the top of the table, the messages keep being inserted at the top, pushing down the existing ones: a usual behavior in this context.

Everything works ok, except that if the user has scrolled down, the new inserted messages shouldn't make the table scroll.

I want the tableView to stay where it is while the user scrolls or if the user has scrolled down.

In other words, the tableView should only be pushed down by new inserted rows if the top row is 100% visible.

I've tried to give the illusion of the table not moving by quickly restoring its location like this:

// we're not at the top anymore, user has scrolled down, let's remember where
let scrollOrigin = self.myTable.enclosingScrollView!.contentView.bounds.origin
// stuff happens, new messages have been inserted, let's scroll back where we were
self.myTable.enclosingScrollView!.contentView.scroll(to: scrollOrigin)

But it doesn't behave as I want. I've tried many combinations but I think I'm not understanding something about the relation between the clip view, the scroll view and the table view.

Or maybe I'm in the XY-problem zone and there's a different way to get this behavior?


Solution

  • Forget the scroll view, clip view, contentView, documentView and focus on the table view. The bottom of the visible part of the table view shouldn't move. You might have missed the flipped coordinate system.

    NSPoint scrollOrigin;
    NSRect rowRect = [self.tableView rectOfRow:0];
    BOOL adjustScroll = !NSEqualRects(rowRect, NSZeroRect) && !NSContainsRect(self.tableView.visibleRect, rowRect);
    if (adjustScroll) {
        // get scroll position from the bottom: get bottom left of the visible part of the table view
        scrollOrigin = self.tableView.visibleRect.origin;
        if (self.tableView.isFlipped) {
            // scrollOrigin is top left, calculate unflipped coordinates
            scrollOrigin.y = self.tableView.bounds.size.height - scrollOrigin.y;
        }
    }
    
    // insert row
    id object = [self.arrayController newObject];
    [object setValue:@"John" forKey:@"name"];
    [self.arrayController insertObject:object atArrangedObjectIndex:0];
    
    if (adjustScroll) {
        // restore scroll position from the bottom
        if (self.tableView.isFlipped) {
            // calculate new flipped coordinates, height includes the new row
            scrollOrigin.y = self.tableView.bounds.size.height - scrollOrigin.y;
        }
        [self.tableView scrollPoint:scrollOrigin];
    }
    

    I didn't test "the tableView to stay where it is while the user scrolls".