How to Toggle SwiftUI 3-Columns NavigationSplitView's Sidebar With 2-Fingers Swipe Gesture on MacOS

40 views Asked by At

This gesture mechanism is inspired by the popular Bear MacOS notes app. In my app, there is a 3-column NavigationSplitView. I would like to implement a mechanism such that I could allow users to use 2-fingers swipe gesture (without pressing on touchpad) to toggle between the different NavigationSplitViewVisibility states to open/ close the sidebars:

enter image description here

I followed one of the answers mentioned in SwiftUI: Two-finger swipe ( scroll ) gesture to implement a NSViewPresentable that conforms the ScrollViewDelegateProtocol, then overlays the custom representable scroll view on my NavigationSplitView to detect scrolling gesture:

struct ContentView: View {
    struct RepresentableScrollView: NSViewRepresentable, ScrollViewDelegateProtocol{
        func updateNSView(_ nsView: ScrollView, context: Context) {
        }
        
        func scrollWheel(with event: NSEvent) {
            if let scrollAction = scrollAction {
                scrollAction(event)
            }

            NotificationCenter.default.post(name: Notification.Name("scrollView"), object: event)
        }
        
        func onScroll(_ action: @escaping (NSEvent) -> Void) -> Self {
            var newSelf = self
            newSelf.scrollAction = action
            return newSelf
        }
        
        
        typealias NSViewType = ScrollView
        private var scrollAction: ((NSEvent) -> Void)?
        
        func makeNSView(context: Context) -> ScrollView {
            let view = ScrollView()
            view.delegate = self;
            return view
        }
    }
    
    var scrollView: some View {
        RepresentableScrollView()
        ...
    }
    
    var body: some View {
        NavigationSplitView(columnVisibility: $splitViewVisibility) {
            ...
        }
        .overlap(scrollView)
    }
}

This works fine to detect scrolling gesture but the scrolling generates many events all at once. So I decided to publish the scrolling event and throttle it to get only get the last scrolling event within a time frame (inspired by SwiftUI: How to listen mouse wheel scroll event in horizontal scrollView):

struct ContentView: View {
    ...
    @State private var splitViewVisibility: NavigationSplitViewVisibility = .automatic
    let scrollViewPublisher = NotificationCenter.default.publisher(for: Notification.Name(rawValue: "scrollView")).throttle(for: .seconds(0.5), scheduler: DispatchQueue.main, latest: true)

    var scrollView: some View {
        RepresentableScrollView()
            .onReceive(scrollViewPublisher) {
                output in
                
                let deltaX = (output.object as! NSEvent).scrollingDeltaX
                
                if (deltaX < 0) {
                    switch (splitViewVisibility) {
                    case .all:
                        self.splitViewVisibility = .doubleColumn
                    case .doubleColumn:
                        self.splitViewVisibility = .detailOnly
                    default:
                        print("Can't swipe left anymore!")
                    }
                } else if (deltaX > 0) {
                    switch (splitViewVisibility) {
                    case .detailOnly:
                        self.splitViewVisibility = .doubleColumn
                    case .doubleColumn:
                        self.splitViewVisibility = .all
                    default:
                        print("can't swipe right anymore!")
                    }
                }
            }
    }
    

With that, I was able to receive the throttled scrolling event to attempt to transition between different states.

However, the end result of it doesn't work as expected. It is still not able to smoothly transition to different visibility states. The behavior either seems stuck or laggy, and I'm not able get to the .detailOnly visibility state at all.

Is there an easier way to implement this 2-finger swipe gesture to toggle between different NavigationSplitView visibility states?

0

There are 0 answers