Search code examples
xcodemacosnstableviewappkit

NSTableView selected row flickering while scrolling with arrow keys


I have a view based NSTableView and can't figure out how to work around a visual glitch where the currently selected row flickers while scrolling up or down with the arrow keys.

The selected row should appear 'glued' to either the top or bottom of the view, depending on scroll direction. The Finder shows this correct behavior in list view but a regular table view seems to not behave this way out of the box. I'm confused as to why that is and see no obvious way to circumvent it. Can anybody point me to possible causes / solutions?

Edit No. 1

A cell based NSTableView behaves in the desired way by default, so this is presumably a bug specific to the view based implementation. I don't want to use a cell based table for unrelated reasons though.

Edit No. 2

I've tried making the table view's parent view layer backed, as well as intercepting the up / down arrow keystrokes to do my own scrolling, but so far I haven't been able to eliminate the flickering.

Edit No. 3

I've created a small sample project that reproduces the issue.


Solution

  • It looks like the selection changes and the old and new selected rows are redrawn. Next the selected row is animated up/down. Disabling scroll animation fixes the issue. Scroll animation can be disabled by subclassing NSClipView and overriding scroll(to:).

    override func scroll(to newOrigin: NSPoint) {
        setBoundsOrigin(newOrigin)      
    }
    

    It might have some side effects.

    Edit

    Copied from zrzka's solution, with some adjustments. Scroll animation is temporarily disabled when using the up arrow or down arrow key.

    class TableView: NSTableView {
    
        override func keyDown(with event: NSEvent) {
            if let clipView = enclosingScrollView?.contentView as? ClipView,
                (125...126).contains(event.keyCode) && // down arrow and up arrow
                event.modifierFlags.intersection([.option, .shift]).isEmpty {
                clipView.isScrollAnimationEnabled = false
                super.keyDown(with: event)
                clipView.isScrollAnimationEnabled = true
            }
            else {
                super.keyDown(with: event)
            }
        }
        
    }
    
    class ClipView: NSClipView {
    
        var isScrollAnimationEnabled: Bool = true
        
        override func scroll(to newOrigin: NSPoint) {
            if isScrollAnimationEnabled {
                super.scroll(to: newOrigin)
            } else {
                setBoundsOrigin(newOrigin)
                documentView?.enclosingScrollView?.flashScrollers()
            }
        }
        
    }