NSTableView scrollRowToVisible with animation
Asked Answered
V

5

8

I am trying to implement an action to scroll to the top of a NSTableView, and the bottom of the NSTableView. I am using scrollRowToVisible but I'd love the action to be animated. Is there a way to do this?

Vermilion answered 28/10, 2011 at 4:24 Comment(0)
W
23

While the NSTableView does not have a scroll property you can directly animate, you can instead, with a bit of math animate the scrolling of the NSClipView that the NSTableView lives in.

Here is how I did this (within a custom subclass of NSTableView) to smoothly animate the row at rowIndex to be scrolled to the center of the view, if possible:

        NSRect rowRect = [self rectOfRow:rowIndex];
        NSRect viewRect = [[self superview] frame];
        NSPoint scrollOrigin = rowRect.origin;
        scrollOrigin.y = scrollOrigin.y + (rowRect.size.height - viewRect.size.height) / 2;
        if (scrollOrigin.y < 0) scrollOrigin.y = 0;
        [[[self superview] animator] setBoundsOrigin:scrollOrigin];
Words answered 12/12, 2011 at 20:23 Comment(5)
Awesome I'll give this a try!Vermilion
This worked great! Here is the final method I came up with, same idea but slightly different. It also flashes the scrollbars on Lion: gist.github.com/1558664Vermilion
Looks broken, at least when combined with Core Animation and Core Image filters on Lion. I'm trying to do a motion-blurred scrolling NSTableView, and the content doesn't get painted when scrolling more than one screen away.Papyrus
Same issue here. The table goes blank and then I see rows close to the destination row move into place. It's like the regular layout of a tableview during scrolling gets broken and it runs out of views to recycle.Dressy
This is the only code that has scrolled ALL the way to the bottom of my NSTableView reliably. The only caveat is that it did not work well with NSDrawNinePartImage (distortion and image glitches). I ended up just reinventing my own nine part image method with NSImageViews and it works flawlessly!Eveleen
T
18

If you're targeting 10.8+ and your table view is layer backed, you can do this:

[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
    context.allowsImplicitAnimation = YES;
    [self.tableView scrollRowToVisible:someRow];
} completionHandler:NULL];
Trilbi answered 19/8, 2015 at 19:48 Comment(2)
Works like a charm!Heel
But only when layer-backed 😐Avoidance
L
2

It does not seem to be possible. NSTableView has not supported any kind of animations up to 10.6. Starting from MasOSX10.7 some simple animations added to the class. You can animate inserting, removing and moving rows to new positions. This is it so far.

Lennalennard answered 28/10, 2011 at 5:4 Comment(0)
H
2

There's no easy way, but I would approach it by subclassing NSAnimation, and as it progresses from 0.0 to 1.0, multiply that by the total scroll distance to get your offset, and successively call scrollToPoint: to give the appearance of a smooth scrolling action. It should work in theory, though I'm not sure how well the scrollview would cooperate.

Hannus answered 28/10, 2011 at 5:32 Comment(1)
Calling setBoundsOrigin: 60 times per second actually gives some pretty amazing results when I started implementing my own custom scrolling mechanism for NSTableView. Thanks for the tip in the right direction.Lisbethlisbon
S
0

CuriousKea actually proposed a working but dirty solution.

  1. No need to subclass NSTableView. What will you do with NSOutlineView? DRY, use a protocol and it extensions instead.
  2. Y position of scrollOrigin calculated incorrectly. NSTableView scrolls to bottom of row, not to a vertical center. As a result animation duration was wrong.
  3. No support for async/await syntax.

The right way to implement scroll animation for NSTableView is:

@MainActor
protocol ScrollableTableView where Self: NSTableView {
    func scroll(to rowIndex: Int, withAnimation animated: Bool) async
    func scroll(to rowIndex: Int, withAnimation animated: Bool, completion: (() -> Void)?)
}

extension ScrollableTableView {
    func scroll(to rowIndex: Int, withAnimation animated: Bool = true) async {
        guard let superview else {
            return
        }
        
        let scrollOrigin = scrollOrigin(forRow: rowIndex)
        
        await animate(duration: animated ? 0.25 : 0, timingFunction: .easeInEaseOut) {
            superview.animator().setBoundsOrigin(scrollOrigin)
        }
    }
    
    func scroll(to rowIndex: Int, withAnimation animated: Bool = true, completion: (() -> Void)? = nil) {
        guard let superview else {
            return
        }
        
        let scrollOrigin = scrollOrigin(forRow: rowIndex)
        
        animate(duration: animated ? 0.25 : 0, timingFunction: .easeInEaseOut) {
            superview.animator().setBoundsOrigin(scrollOrigin)
        } completion: {
            completion?()
        }
    }
    
    private func scrollOrigin(forRow rowIndex: Int) -> CGPoint {
        guard let superview else {
            return .zero
        }
        
        let rowRect = rect(ofRow: rowIndex)
        let viewRect = superview.frame
        
        var scrollOrigin = rowRect.origin
        scrollOrigin.y += rowRect.size.height - viewRect.size.height
        scrollOrigin.y = max(0, scrollOrigin.y)
        
        return scrollOrigin
    }
    
    private func animate(duration: Double,
                         timingFunction timingFunctionName: CAMediaTimingFunctionName,
                         animations: () -> Void,
                         completion: (() -> Void)?) {
        NSAnimationContext.runAnimationGroup { context in
            context.duration = duration
            context.timingFunction = CAMediaTimingFunction(name: timingFunctionName)
            animations()
        } completionHandler: {
            completion?()
        }
    }
    
    private func animate(duration: Double,
                         timingFunction timingFunctionName: CAMediaTimingFunctionName,
                         animations: () -> Void) async {
        await NSAnimationContext.runAnimationGroup { context in
            context.duration = duration
            context.timingFunction = CAMediaTimingFunction(name: timingFunctionName)
            animations()
        }
    }
}

extension NSTableView: ScrollableTableView {}
Saviour answered 4/8, 2023 at 7:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.