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

16

我正在编写一个简单的扫雷应用,以帮助我了解SwiftUI。因此,我希望主要点击(通常是左键)可以“挖掘”(显示是否有地雷),而次要点击(通常是右键)可以放置旗帜。

我已经实现了挖掘功能!但我无法弄清楚如何放置旗帜,因为我无法检测到次要点击。

以下是我尝试的内容

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

1
你的第一个链接已经失效了。是私有仓库吗? - Gil Birman
.onTapGesture() 看看吧。 - ABC
糟糕,你是对的@GilBirman!已修复;对此感到抱歉。 - Ky -
@Raymond 我先尝试了那个。除非我漏掉了什么重要的东西,否则它似乎与.gesture(TapGesture().onEnded(.......))的行为完全相同。 - Ky -
5个回答

12

就目前而言,使用SwiftUI是无法直接实现这一点的。我相信将来会有可能,但目前来看,TapGesture 明显主要关注于没有“右键单击”概念的iOS用例,所以我认为这就是为什么被忽略的原因。请注意,“长按”概念作为 LongPressGesture 的一流公民存在,几乎专门用于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的SwiftUI视图将其包装。
  3. 将您的实现插入到UI中,就像您对任何其他SwiftUI视图所做的那样。

虽然不是理想的解决方案,但它可能已经足够了。我希望这可以解决您的问题,直到苹果进一步扩展SwiftUI的功能。


1
谢谢。这基本上就是我现在正在做的事情。真的很遗憾,这是他们对框架的第一印象。我会将其标记为已接受,直到(希望)有更好的解决方案出现。 - Ky -
3
好的,看起来这是今天最好的答案。悬赏由你获得!希望有一天我们会有一个官方的解决方案。 - Ky -
1
@B.T. 那仍然是推荐的解决方案吗?还是SwiftUI 2.0带来了一些改进? - mica
1
@mica 从官方文档来看,似乎他们没有为任何以鼠标样式交互方式表达的手势添加本地支持。它们都是用“轻拍”和手指样式交互来表达的,这意味着从这种哲学中获得右键单击将会很困难。文档在这里:https://developer.apple.com/documentation/swiftui/gestures 和这里:https://developer.apple.com/documentation/swiftui/eventmodifiers - B.T.
1
@B.T. 你如何将 RightClickableSwiftUIView 应用到 Text() 或其他 SwiftUI 视图中? - mica
你可以尝试使用ZStack,并将可右键单击的视图“置于”Text()视图之上。 - B.T.

5
这并没有完全解决扫雷游戏的使用情况,但是contextMenu是一种在SwiftUI macOS应用程序中处理右键单击的方式。
例如:
.contextMenu {
    Button(action: {}, label: { Label("Menu title", systemImage: "icon") })
}

将会响应在 macOS 上的右键单击和在 iOS 上的长按操作


2
尽管我喜欢(并点赞)被接受的答案,但我找到了一种方法,使视图能够响应右键单击事件,而无需符合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)

to

        window.contentView = MyHostingView(rootView: contentView)

当然,任何其他可能涉及到NSHostingView的代码都需要进行类似的更改。通常在AppDelegate中的引用是唯一的,但在重要项目中可能会有其他引用。

然后,在SwiftUI代码中,右键单击事件将显示为带有.control修饰符的TapGesture

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

1
顺便说一句 - 我吃了亏,如果您在同一个“View”上有多个.gesture调用,请确保将带修饰符的放在前面。我先放了一个未经修改的手势,结果总是被调用。交换顺序后问题得到解决。谢谢。 - Chip Jarred

1
很遗憾,接受的解决方案对我来说不适用,因为在单击 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
    }
}

文章启发。

0
将此代码添加到您的视图中,适用于MacOS。
.contextMenu {
    Button("Remove") {
        print("remove this view")
    }
}

2
谢谢您提供这个有趣的解决方案,但它并不适用于我的问题,似乎只是重申了Colin的答案 - Ky -
这是正确的解决方案。 - user1046037

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