SwiftUI:如何在水平ScrollView中监听鼠标滚轮滚动事件

4

我正在开发一款macOS软件,发现在SwiftUI中,如果将scrollview设置为.horizontal,则鼠标滚轮滚动时列表不会滚动,但在.vertical模式下可以滚动。

然而,我真的需要这个功能。

所以我尝试了一个选项:使用NSViewRepresentable实现,创建一个NSView覆盖scrollWheel方法以处理鼠标滚轮滚动事件,然后发送通知。

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")
    }
}

然后在SwiftUI中,代码如下:

@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()
        }
    }
    
}

现在可以在.horizontal的scrollView中处理滚动事件,但是有一个新问题:scrollViewItem无法处理点击事件,因为MouseWheelScrollEventView处理鼠标点击事件。但我想让scrollViewItem也能处理鼠标点击事件。

如何同时处理鼠标滚轮滚动事件和点击事件?

ps:我知道使用appkit可以解决这个问题,但是否有办法尝试使用swiftui实现呢?


这里的事件都可以正常工作。已在Xcode 12.0中进行了测试。 - Asperi
@Asperi 你的意思是使用我的方法可以实现。
  1. scrollView 可以处理鼠标滚轮滚动事件
  2. scrollView 内部元素可以处理点击事件,我也使用 xcode(版本 12.0 beta 6 (12A8189n)),但 scrollView 内部元素无法处理点击事件。你可以给我看一下你的代码吗?谢谢
- jaychen
1个回答

9

当你满足以下条件时,可以简单地监听应用程序的currentEvent并跳过一个窥视NSViewRep:

  • 您已经拥有视图的悬停/聚焦布尔值
  • 不需要NSTrackingArea来限制响应(例如,仅在瞬态弹出窗口中有一个ScrollView,用户希望一直与之交互)

下面是我macOS SwiftUI应用程序中短暂弹出窗口的示例。水平ScrollView呈现选项,可以通过缓慢或快速垂直滚动“滚动选择”选项。您可以为相同的想法插入ScrollViewProxy。

在测试实现中,使用.onReceive() {}会重新计算视图,并且单个鼠标滚轮会反复触发。将订阅存储在@State var中可使其按预期工作。

    //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


1
我尝试在Mac Catalyst上使用此解决方案,但它显示NSApp和.currentEvent不可用。您知道如何在Mac Catalyst中解决这个问题吗? - as2457

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接