Search code examples
swiftscrollnstableview

View-Based NSTableView Scrolling Performance


I have an NSTableView and its parent is the view for an NSMenuItem. The NSTableView is a view-based table. I'm populating the table using bindings with the data coming from an NSArrayController.

The performance of scrolling in the NSTableView is abysmal. It seems like the scrolling function is not "continuous" and only scrolls once I have lifted my fingers off the trackpad. The behavior is sort of like when you have a text field and it only sends its action upon hitting the return key as opposed to sending the action continuously.

If I open and close the menu enough times, the table will scroll smoothly for a part of the NSTableView, but quickly reverts to jerky scrolling. I can also click and drag to effectively scroll through the rows relatively smoothly, but again, the two-fingered scroll gesture is super-clunky.

Below is the code for how the table is being populated. In IB, the text field is bound to the name property, and the NSImageView is bound to the icon property of a custom class called AmphRunningApp.

AmphRunningApp Class:

import Foundation
import Cocoa

class AmphRunningApp: NSObject
{
    @objc var app: NSRunningApplication
    @objc var name: String
    @objc var icon: NSImage

init(app: NSRunningApplication, name: String, icon: NSImage)
{
    self.name = name
    self.app = app
    self.icon = icon
}

Populating the table:

@IBAction @objc func updateRunningAppsTable(sender: Any)
{
    // Get all currently running apps
    // and sort them alphabetically by localized name:
    
    let runningApps = NSWorkspace.shared.runningApplications.sorted
    {
        $0.localizedName! < $1.localizedName!
    }

    // For every running app,
    // add it to the array controller:
    
    for app in runningApps
    {
        var duplicateApp = false
        
        // Check that this app does not already exist in the array controller
        for dupeApp in self.menuRunningAppAC.arrangedObjects as! [AnyObject]
        {
            if dupeApp.name == app.localizedName
            {
                duplicateApp = true
            }
        }

        // If this app has not already been added
        // to the array controller, add it
        if duplicateApp == false
        {
            self.menuRunningAppAC.addObject(AmphRunningApp(app: app, name: app.localizedName!, icon: app.icon!))
        }
    }

    self.statusMenuAppTable.reloadData()
}

enter image description here


Solution

  • The issue seems to be related to the performClick() function on NSStatusItem's button property, and GCD. If I simply assign a menu to my NSStatusItem (and thus open it with any click (i.e. left-click or right-click), the scrolling is buttery smooth. If I don't use GCD to call the performClick() function, scrolling is also buttery smooth.**

    JACKED-UP CLUNKY SCROLLING:

    @objc func statusItemClicked(_ sender: Any?)
    {
        NSApp.activate(ignoringOtherApps: true)
    
        if (NSApp.currentEvent != nil)
        {
            print("right click")
            let event = NSApp.currentEvent!
    
            if event.type == NSEvent.EventType.rightMouseUp || event.modifierFlags.contains(.control) // RIGHT-CLICK
            {
                
    
                // THIS IS THE PROBLEM:
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.01)
                {
                    self.statusItem.menu = self.statusItemMenu
                    self.statusItem.button?.performClick(nil)
                }
                // END PROBLEM
    
    
            }
            else // LEFT-CLICK
            {
                print("left click")
            }
    }
    



    BUTTERY-SMOOTH SCROLLING:

    @objc func statusItemClicked(_ sender: Any?)
    {
        NSApp.activate(ignoringOtherApps: true)
    
        if (NSApp.currentEvent != nil)
        {
            print("right click")
            let event = NSApp.currentEvent!
    
            if event.type == NSEvent.EventType.rightMouseUp || event.modifierFlags.contains(.control) // RIGHT-CLICK
            {
                self.statusItem.menu = self.statusItemMenu
                self.statusItem.button?.performClick(nil)
            }
            else // LEFT-CLICK
            {
                print("left click")
            }
    }