Search code examples
swiftmacosuser-interfacecocoanstableview

Horizontal and vertical nested scroll views


My view contains a horizontally scrolling Scroll View containing multiple vertically scrolling Table Views.

See diagram:

enter image description here

However, the Table Views are blocking horizontal scrolling in the Scroll View when the cursor is on them.

How can I allow the scroll to pass on to the superview?


Code for Application

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

     @IBOutlet weak var window: NSWindow!
    var viewController: ViewController!

    func applicationDidFinishLaunching(aNotification: NSNotification) {
        viewController = ViewController(nibName: "ViewController", bundle: nil)  
        viewController.view.translatesAutoresizingMaskIntoConstraints = false
        window.contentView!.addSubview(viewController.view)
        //...
    }
    //...
}

View Controller

class ViewController: NSViewController {

    @IBOutlet weak var scrollView: NSScrollView!
    //...

    func addListView(listPath: String) {
        let listView = ListViewController(nibName: "ListView", bundle: nil)
        listView!.listPath = listPath

        listView!.view.translatesAutoresizingMaskIntoConstraints = false
        scrollView.contentView.addSubview(listView!.view)
        //...
    }
    //...
}

List View

class ListViewController: NSViewController {

    @IBOutlet weak var itemsTitleView: NSTextField!
    @IBOutlet weak var itemsTableView: MyTableView!
    //...
}

TableView

class MyTableView: NSTableView {

    override func drawRect(dirtyRect: NSRect) {
        super.drawRect(dirtyRect)

        // Drawing code here.
    }

    var mainScrollView: NSScrollView? {
        let app = NSApplication.sharedApplication()
        let delegate = app.delegate as! AppDelegate
        return delegate.viewController.scrollView
}

    override func mouseDown(theEvent: NSEvent) {
        // do nothing
    }

    override func scrollWheel(theEvent: NSEvent) {
        if theEvent.scrollingDeltaY == 0 {
            // It's a horizontal scroll -> redirect it
            if let tableScrollView = enclosingScrollView, mainScrollView = mainScrollView {
                mainScrollView.scrollWheel(theEvent)
            }
        } else {
            // It's a vertical scroll -> behave normally
            super.scrollWheel(theEvent)
        }
    }

}

Solution

  • The first thing I would try is to catch the scroll event as it's delivered to the table view. Once you've made sure the event object represents a horizontal-scroll gesture, redirect it to the main scroll view.

    class MyTableView: NSTableView {
    
        var mainScrollView: NSScrollView? {
            // Your tables will need some way to access the main scroll view.
            // I'm assuming here that there's a reference to it on the AppDelegate
            let app = NSApplication.sharedApplication()
            let delegate = app.delegate as! AppDelegate
            return delegate.mainScrollView
        }
    
    
        override func scrollWheel(theEvent: NSEvent) {
    
            if theEvent.scrollingDeltaY == 0 {
                // It's a horizontal scroll -> redirect it
                if let tableScrollView = enclosingScrollView, mainScrollView = self.mainScrollView {
                    mainScrollView.scrollWheel(theEvent)
                }
            } else {
                // It's a vertical scroll -> behave normally
                super.scrollWheel(theEvent)
            }
        }
    }
    

    I have to say though, anytime you start messing with events like this, you need to be prepared for side-effects; in the simple demo-app I created to test this code, it seemed to work okay, but don't be surprised if this approach throws up additional challenges.