'SwiftUI: can a child view suppress its parent view's contextMenu?

I have a .contextMenu on a large view in my SwiftUI app. Inside is a child view with an .onLongPressGesture.

On iOS, context menus are triggered by a long press. So pressing the smaller view always triggers both the context menu and my own LongPressGesture.

I'd like to stop my little view from triggering the parent's context menu.

struct ContentView: View {
    @State var swap: Bool = false

    var body: some View {
        VStack {
            Text("Press the smaller view to swap colors")
                .padding()
                .background { swap ? Color.blue : Color.red }
                .onLongPressGesture { swap.toggle() }
        
            Text("Press the larger view for context menu")
                .padding()
        }
        .foregroundColor(.white)
        .padding()
        .background { swap ? Color.red : Color.blue }
        .contextMenu { Text("Menu Goes Here") }
    }
}

A large red rectangle containing a small blue rectangle. The smaller rectangle says “press the smaller view to swap colors” and the larger says “press the outer box for context menu”

Things I've tried:

  • Using .highPriorityGesture makes no difference
  • All the different GestureMask options affect subviews, not superviews
  • A LongPressGesture with a short duration ensures my custom gesture triggers first, but doesn't prevent the menu from appearing
  • A DragGesture with a zero minimum distance does the same as above
  • The solution outlined in SwiftUI: Cancel TapGesture on parent view involves 2 gestures that are both under the author's control, so they can pick a winner. The context menu's internals are opaque to me.

Is there a way for a subview to prevent gestures bubbling up to its superviews?



Solution 1:[1]

You can conditionally suppress the .contextMenu by wrapping all its content in if block, because an empty menu won't ever appear:

.contextMenu {
    if !isPressingSmallerView {
        Text("Menu Goes Here")
    }
}

Then the trick to setting isPressingSmallerView is to use .updating() on our LongPressGesture:

.gesture(
    LongPressGesture()
        .updating($isPressingSmallerView) { value, state, _ in
            state = value
        }
        .onEnded { _ in
            swap.toggle()
        }
)

The complete solution in context:

struct ContentView: View {
    @GestureState var isPressingSmallerView: Bool = false
    @State var swap: Bool = false

    var body: some View {
        VStack {
            Text("Press the smaller view to swap colors")
                .padding()
                .background { swap ? Color.blue : Color.red }
                .gesture(
                    LongPressGesture()
                        .updating($isPressingSmallerView) { value, state, _ in
                            state = value
                        }
                        .onEnded { _ in
                            swap.toggle()
                        }
                )
        
            Text("Press the larger view for context menu")
                .padding()
        }
        .foregroundColor(.white)
        .padding()
        .background { swap ? Color.red : Color.blue }
        .contextMenu {
            if !isPressingSmallerView {
                Text("Menu Goes Here")
            }
        }
    }
}

This is a small contrived example, so the @GestureState variable is local to the ContentView.

Depending on your view hierarchy, you may want to pass that state up from child to parent with a PreferenceKey

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1