SwiftUI: Меню внутри ячейки списка с жестом запуска onTapGesture

#swiftui #swiftui-list

#swiftui #swiftui-список

Вопрос:

У меня есть SwiftUI List на iOS14.3 (Xcode12.3), и внутри его ячеек я хочу, чтобы Menu всплывающее окно появлялось, когда пользователь нажимает на кнопку. Кроме того, в этих ячейках есть onTapGesture и проблема в том, что этот жест также запускается при нажатии кнопки меню.

Пример:

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

Если вы нажмете на изображение с многоточием, откроется меню, которое также "Cell tapped" будет выведено на консоль. Это проблема для меня, потому что в моем примере с реальным миром я сворачиваю ячейку жестом касания и, конечно, я не хочу, чтобы это происходило, когда пользователь нажимает кнопку меню.

Я обнаружил, что при длительном нажатии на кнопку жест не срабатывает. Но, на мой взгляд, это неприемлемый UX, мне потребовалось некоторое время, чтобы понять это самому.

Я также заметил, что когда я заменяю Menu на обычный Button , то жест ячейки не срабатывает при нажатии (только с BorderlessButtonStyle помощью или PlainButtonStyle , в противном случае он вообще не активен). Но я не знаю, как открыть меню из его действия.

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

Комментарии:

1. Это проект iOS или macOS?

2. О, извините, это на iOS14.3 с Xcode 12.3

3. Я тестировал его также на macOS сейчас, и там жест cells не запускается при открытии меню. Несмотря на то, что меню на Mac выглядит совсем по-другому (на самом деле это выпадающий список), я бы ожидал одинакового поведения на обеих платформах. Итак, я только что сообщил об ошибке Apple.

Ответ №1:

Кроме того, вы можете переопределить меню 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")
            }
        }
    }
}
 

Кроме того, нет необходимости использовать .background(Color(.systemBackground)) для нажатия на разделители. Это не очень гибко — что, если вы хотите изменить цвет фона?

Вы можете использовать contentShape вместо:

 .contentShape(Rectangle())
 

Комментарии:

1. Спасибо, хотя я думаю, что ответ @Asperi немного более элегантный и, вероятно, более перспективный, я отмечу это как принятый ответ. На самом деле, в моем реальном примере ячейка значительно сложнее, а кнопка меню не является последним подразделом с правой стороны. Однако основная проблема заключается в том, что я повторно использую эту ячейку в разных местах своего приложения и хочу иметь разные onTapGestures в зависимости от того, где она используется, поэтому я применю этот жест вне тела ячеек. В этих обстоятельствах ваше решение намного лучше.

2. Кроме этого, спасибо за подсказку с contentShape вместо background, чтобы сделать разделители доступными. Не знал этого, я все еще новичок в SwftUI, работая над своим первым большим приложением, использующим его.

3. Похоже, этот ответ больше не работает на iOS 15.5 (по крайней мере). Хуже того: результат случайный (иногда вы получаете двойные нажатия, а иногда нет). Кто-нибудь еще это видит? Альтернатива ?!

Ответ №2:

Я не уверен, является ли это ошибкой или ожидаемым поведением, но вместо того, чтобы бороться с этим, я бы рекомендовал избегать такого перекрытия (потому что даже при сегодняшнем успехе он может перестать работать в разных системах / обновлениях).

Вот возможное решение (протестировано с 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()
            }
        }
    }
 

Комментарии:

1. Спасибо, это, конечно, также решает проблему и, возможно, является более элегантным и перспективным решением, но я принял ответ @pawello2222. Причину смотрите в моем комментарии к его ответу.