Stitch
Stitch

Reputation: 165

SwiftUI: Menu inside list cell with onTapGesture triggers gesture

I have a SwiftUI List on iOS14.3 (Xcode12.3) and inside it's cells I want to have a Menu popup up when the user is tapping on a button. Additionally these cells have an onTapGesture and the problem is that this gesture is also triggered when the menu button is pressed.

Example:

List {
    HStack {
        Text("Cell content")
        Spacer()
        // Triggers also onTapGesture of cell
        Menu(content: {
            Button(action: { }) {
                Text("Menu Item 1")
                Image(systemName: "command")
            }
            Button(action: { }) {
                Text("Menu Item 2")
                Image(systemName: "option")
            }
            Button(action: { }) {
                Text("Menu Item 3")
                Image(systemName: "shift")
            }
        }) {
            Image(systemName: "ellipsis")
                .imageScale(.large)
                .padding()
        }
    }
    .background(Color(.systemBackground))
    .onTapGesture {
        print("Cell tapped")
    }
}

If you tap on the ellipsis image, the menu opens but also "Cell tapped" will be printed to the console. This is a problem for me, because in my real world example I am collapsing the cell within the tap gesture and of course I don't want that to happen when the user presses the menu button.

I found out, that when I long press on the button the gesture won't be triggered. But this is not acceptable UX in my opinion, took me a while to find that out for my self.

I also noticed, when I replace the Menu with a regular Button, then the cells gesture is not triggered while pressing (only with BorderlessButtonStyle or PlainButtonStyle, otherwise he is not active at all). But I don't know how to open a Menu from it's action.

List {
    HStack {
        Text("Cell content")
        Spacer()
        // Does not trigger cells onTapGesture, but how to open Menu from action?
        Button(action: { print("Button tapped") }) {
            Image(systemName: "ellipsis")
                .imageScale(.large)
                .padding()
        }
        .buttonStyle(BorderlessButtonStyle())
    }
    .background(Color(.systemBackground))
    .onTapGesture {
        print("Cell tapped")
    }
}

Upvotes: 10

Views: 5411

Answers (2)

pawello2222
pawello2222

Reputation: 54466

Alternatively, you can override Menu onTapGesture:

struct ContentView: View {
    var body: some View {
        List {
            HStack {
                Text("Cell content")
                Spacer()
                Menu(content: {
                    Button(action: {}) {
                        Text("Menu Item 1")
                        Image(systemName: "command")
                    }
                    Button(action: {}) {
                        Text("Menu Item 2")
                        Image(systemName: "option")
                    }
                    Button(action: {}) {
                        Text("Menu Item 3")
                        Image(systemName: "shift")
                    }
                }) {
                    Image(systemName: "ellipsis")
                        .imageScale(.large)
                        .padding()
                }
                .onTapGesture {} // override here
            }
            .contentShape(Rectangle())
            .onTapGesture {
                print("Cell tapped")
            }
        }
    }
}

Also, there's no need to use .background(Color(.systemBackground)) to tap on spacers. This is not really flexible - what if you want change background color?

You can use contentShape instead:

.contentShape(Rectangle())

Upvotes: 13

Asperi
Asperi

Reputation: 257693

I'm not sure if this is a bug or expected behavior, but instead of fighting with it I would recommend to avoid such overlapping (because even with success today it might stop working on different systems/updates).

Here is possible solution (tested with Xcode 12.1 / iOS 14.1)

    List {
        HStack {

            // row content area  
            HStack {
                Text("Cell content")
                Spacer()
            }
            .background(Color(.systemBackground)) // for tap on spacer area
            .onTapGesture {
                print("Cell tapped")
            }

            // menu area
            Menu(content: {
                Button(action: { }) {
                    Text("Menu Item 1")
                    Image(systemName: "command")
                }
                Button(action: { }) {
                    Text("Menu Item 2")
                    Image(systemName: "option")
                }
                Button(action: { }) {
                    Text("Menu Item 3")
                    Image(systemName: "shift")
                }
            }) {
                Image(systemName: "ellipsis")
                    .imageScale(.large)
                    .padding()
            }
        }
    }

Upvotes: 3

Related Questions