Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: How to listen mouse wheel scroll event in horizontal scrollView

I'm working on a macos software and I found that in swiftui the scrollview is set to .horizontal, so the list doesn't scroll when the mouse wheel scrolls, but it does in .vertical mode.

But I really need this feature.

So I tried one option:implementation NSViewRepresentable to make a NSView override scrollWheel method that can handle mouse wheel scroll event. then post a notification.

struct MouseWheelScrollEventView : NSViewRepresentable {
    
    class MouseView : NSView {
        override var acceptsFirstResponder: Bool {
            true
        }
        
        override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
            return true
        }
                
        override func scrollWheel(with event: NSEvent) {
            NotificationCenter.default.post(name: Notification.Name("mouseevent"), object: event)
        }
    }
    
    
    func makeNSView(context: Context) -> some NSView {
        let view = MouseView()
        
        DispatchQueue.main.async {
            view.window?.makeFirstResponder(view)
        }
        return view
    }
    
    func updateNSView(_ nsView: NSViewType, context: Context) {
        print("update")
    }
}

then in SwiftUI, the code like this:

@available(OSX 11.0, *)
struct ContentView: View {
    
    let mouseWheelScrollEventPublisher = NotificationCenter.default.publisher(for: Notification.Name(rawValue: "mouseevent"))

    @State var deltaY = 0.0
    
    @State var currentIndex = 1
    
    var body: some View{
        
        ZStack{
            ScrollView(.horizontal,showsIndicators:false) {
                ScrollViewReader { value in
                    
                    LazyHStack(alignment: .top) {
                        ForEach(1...100, id: \.self) { index in
                       
                            if(self.currentIndex == index) {
                                Text("\(String(index))")
                                    .frame(width: 80, height: 80)
                                    .background(Color.yellow)
                                    .onTapGesture(perform: {
                                        print(index)
                                    })
                            }else {
                                Text("\(String(index))")
                                    .frame(width: 80, height: 80)
                                    .background(Color.blue)
                                    .onTapGesture(perform: {
                                        print(index)
                                    })
                            }
                        }
                    }
                    .onReceive(mouseWheelScrollEventPublisher) { output in
                        let event = output.object as! NSEvent
                        let deltaY = event.deltaY
                        
                        if(deltaY < 0 && self.currentIndex < 100) {
                            self.currentIndex += 1
                            value.scrollTo(currentIndex)
                        }else if(deltaY > 0 && self.currentIndex > 1) {
                            self.currentIndex -= 1
                            value.scrollTo(currentIndex)
                        }
                        
                    }
                }
            }
            .frame(width: 600)
          
            MouseWheelScrollEventView()
        }
    }
    
}

now the scroll event can handle in .horizontal scrollView, but there is a new question: the scrollViewItem can not handle the click event, because MouseWheelScrollEventView handle the mouse click event. But I want the scrollViewItem also can handle the mouse click event.

How can I hanle both mouse wheel scroll event and click event?

ps: I know that using appkit can solve the problem, but is there any way to try to use swiftui implementation?

like image 522
jaychen Avatar asked Oct 19 '25 05:10

jaychen


1 Answers

You can simply listen to the app's currentEvent and skip a snooping NSViewRep when:

  • you already have a hover/focus boolean for the view
  • an NSTrackingArea isn't necessary to restrict a response (e.g., only one ScrollView in a transient popover that a user would expect to interact with all the time)

Below is an example from a transient popover in a macOS SwiftUI App of mine. A horizontal ScrollView presents options, which can be "scroll selected" by vertical scrolling slowly or quickly. You could plug ScrollViewProxy in for the same idea.

In a test implementation, using .onReceive() {} would recalculate the view and a single mouse scroll would trigger repeatedly. Storing the subscription in an @State var lets it work as desired.

    //horizontal scroll view   
.onAppear { trackScrollWheel() }

func trackScrollWheel() {
    NSApp.publisher(for: \.currentEvent)
        .filter { event in event?.type == .scrollWheel }
        .throttle(for: .milliseconds(200),
                  scheduler: DispatchQueue.main,
                  latest: true)
        .sink { [weak vm] event in
            vm?.goBackOrForwardBy(delta: Int(event?.deltaY ?? 0))
        }
        .store(in: &subs)
}

@State var subs = Set<AnyCancellable>() // Cancel onDisappear

like image 96
Ryan Avatar answered Oct 20 '25 19:10

Ryan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!