如何检测 SwiftUI 中的右键单击?

How can I detect a right-click in SwiftUI?

我写 a simple Mines app 是为了帮助我了解 SwiftUI。因此,我希望主要点击(通常是 LMB)到 "dig"(显示那里是否有地雷),然后二次点击(通常是 RMB)来放置一个标志。

我正在挖掘!但是我不知道如何放置标志,因为我不知道如何检测二次点击。

Here's what I'm trying:

BoardSquareView(
    style: self.style(for: square),
    model: square
)
.gesture(TapGesture().modifiers(.control).onEnded(self.handleUserDidAltTap(square)))
.gesture(TapGesture().onEnded(self.handleUserDidTap(square)))

正如我之前暗示的那样,handleUserDidTap 返回的函数在单击时被正确调用,但是 handleUserDidAltTap 返回的函数仅在我按住 Control 键时被调用。这是有道理的,因为代码就是这么写的……但我没有看到任何 API 可以让它注册二次点击,所以我不知道还能做什么。

我也试过这个,但行为似乎是一样的:

BoardSquareView(
    style: self.style(for: square),
    model: square
)
.gesture(TapGesture().modifiers(.control).onEnded(self.handleUserDidAltTap(square)))
.onTapGesture(self.handleUserDidTap(square))

就目前 SwiftUI 的情况而言,这不可能直接实现。我相信它会在未来,但目前,TapGesture 显然主要集中在没有“右键单击”概念的 iOS 用例上,所以我认为这就是为什么它被忽略了。请注意,“长按”概念是 LongPressGesture 形式的第一个 class 公民,并且几乎专门用于支持该理论的 iOS 上下文。

就是说,我确实想出了一种方法来完成这项工作。您需要做的是退回到旧技术,并将其嵌入到您的 SwiftUI 视图中。

struct RightClickableSwiftUIView: NSViewRepresentable {
    func updateNSView(_ nsView: RightClickableView, context: NSViewRepresentableContext<RightClickableSwiftUIView>) {
        print("Update")
    }
    
    func makeNSView(context: Context) -> RightClickableView {
        RightClickableView()
    }
}

class RightClickableView: NSView {
    override func mouseDown(with theEvent: NSEvent) {
        print("left mouse")
    }
    
    override func rightMouseDown(with theEvent: NSEvent) {
        print("right mouse")
    }
}

我对此进行了测试,它在一个相当复杂的 SwiftUI 应用程序中对我有用。这里的基本做法是:

  1. 将您的听力组件创建为 NSView
  2. 用实现 NSViewRepresentable.
  3. 的 SwiftUI 视图包装它
  4. 将你的实现放入你想要的 UI 中,就像你对任何其他 SwiftUI 视图所做的那样。

这不是一个理想的解决方案,但它现在可能已经足够好了。我希望这能解决您的问题,直到 Apple 进一步扩展 SwiftUI 的功能。

不幸接受的解决方案不适合我,因为点击 NSStatusItem.button 时没有视觉反馈。对我有用的是检测按钮触摸处理程序中的右键单击:

func setupStatusBarItem() {
        statusBarItem.button?.action = #selector(didPressBarItem(_:))
        statusBarItem.button?.target = self
        statusBarItem.button?.sendAction(on: [.leftMouseDown, .rightMouseDown])
}

@objc func didPressBarItem(_ sender: AnyObject?) {
        if let event = NSApp.currentEvent, event.isRightClick {
            print("right")
        } else {
            print("left")
        }
}

extension NSEvent {
    var isRightClick: Bool {
        let rightClick = (self.type == .rightMouseDown)
        let controlClick = self.modifierFlags.contains(.control)
        return rightClick || controlClick
    }
}

灵感来自 article

尽管我喜欢(并投赞成票)已接受的答案,但我找到了一种视图无需遵守 NSViewRepresentable 即可响应鼠标右键事件的方法。这种方法更干净地集成到我的应用程序中,因此可能值得考虑将其作为面临此问题的其他人的一种可能性。

我的解决方案首先是愿意接受在 macOS 中右键单击和控制左键单击传统上被视为等同的约定。此解决方案不允许以不同于控制左键单击的方式处理控制右键单击。但是任何其他修饰符都会被处理,因为它只会向它们添加 .control 并将其转换为左键单击。

如果您使用 SwiftUI 上下文菜单,可能会 破坏它们。我还没有测试过。

所以想法是使用控制键修饰符将鼠标右键事件转换为鼠标左键事件。

为了实现这一点,我对 NSHostingView 进行了子类化,并在 NSEvent

上提供了一个方便的扩展
// -------------------------------------
fileprivate extension NSEvent
{
    // -------------------------------------
    var translateRightMouseButtonEvent: NSEvent
    {
        guard let cgEvent = self.cgEvent else { return self }
        
        switch type
        {
            case .rightMouseDown: cgEvent.type = .leftMouseDown
            case .rightMouseUp: cgEvent.type = .leftMouseUp
            case .rightMouseDragged: cgEvent.type = .leftMouseDragged
                
            default: return self
        }
        
        cgEvent.flags.formUnion(.maskControl)
        
        guard let nsEvent = NSEvent(cgEvent: cgEvent) else { return self }
        
        return nsEvent
    }
}

// -------------------------------------
class MyHostingView<Content: View>: NSHostingView<Content>
{
    // -------------------------------------
    @objc public override func rightMouseDown(with event: NSEvent) {
        super.mouseDown(with: event.translateRightMouseButtonEvent)
    }
    
    // -------------------------------------
    @objc public override func rightMouseUp(with event: NSEvent) {
        super.mouseUp(with: event.translateRightMouseButtonEvent)
    }
    
    // -------------------------------------
    @objc public override func rightMouseDragged(with event: NSEvent) {
        super.mouseDragged(with: event.translateRightMouseButtonEvent)
    }
}

然后在AppDelegate.didFinishLaunching我改了

        window.contentView = NSHostingView(rootView: contentView)

        window.contentView = MyHostingView(rootView: contentView)

当然,必须对可能引用 NSHostingView 的任何其他代码进行类似的更改。通常 AppDelegate 中的引用是唯一的,但在一个重要的项目中可能还有其他引用。

鼠标右键事件随后在 SwiftUI 代码中显示为带有 .control 修饰符的 TapGesture

            Text("Right-clickable Text")
                .gesture(
                    TapGesture().modifiers(.control)
                        .onEnded
                        { _ in
                            print("Control-Clicked")
                        }
                )

这并不能完全解决扫雷器用例,但contextMenu是一种在 SwiftUI macOS 应用程序中处理右键单击的方法。

例如:

.contextMenu {
    Button(action: {}, label: { Label("Menu title", systemImage: "icon") })
}

将响应右键单击 macOS 和长按 iOS

将此添加到您的视图中。适用于 macOS

.contextMenu {
    Button("Remove") {
        print("remove this view")
    }
}