Search code examples
swiftevent-handlingmouseeventswiftuiright-click

How can I detect a right-click in SwiftUI?


I'm writing a simple Mines app to help me get to know SwiftUI. As such, I want primary click (usually LMB) to "dig" (reveal whether there's a mine there), and secondary click (usually RMB) to place a flag.

I have the digging working! But I can't figure out how to place a flag, because I can't figure out how to detect a secondary click.

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

As I implied earlier, the function returned by handleUserDidTap is called properly on click, but the one returned by handleUserDidAltTap is only called when I hold down the Control key. That makes sense because that's what the code says... but I don't see any API which could make it register secondary clicks, so I don't know what else to do.

I also tried this, but the behavior seemed identical:

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

Solution

  • As things stand with SwiftUI right now, this isn't directly possible. I am sure it will be in the future, but at the moment, the TapGesture is clearly focused mainly on the iOS use cases which don't have a concept of a "right click" so I think that is why this was ignored. Notice the "long press" concept is a first-class citizen in the form of the LongPressGesture, and that is almost exclusively used in an iOS context, which supports this theory.

    That said, I did figure out a way to make this work. What you have to do is fall back on the older technology, and embed it into your SwiftUI view.

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

    I tested this, and it worked for me inside a fairly complex SwiftUI application. The basic approach here is:

    1. Create your listening component as an NSView.
    2. Wrap it with a SwiftUI view that implements NSViewRepresentable.
    3. Plop your implementation into the UI where you want it, just like you would do with any other SwiftUI view.

    Not an ideal solution, but it might be good enough for right now. I hope this solves your problem until Apple expands SwiftUI's capabilities further.