118

Say I have a List and two buttons in one row, how can I distinguish which button is tapped without the entire row highlighting?

For this sample code, when any one of the buttons in the row is tapped, both button's action callbacks are invoked.

// a simple list with just one row
List {

    // both buttons in a HStack so that they appear in a single row
    HStack {
        Button {
            print("button 1 tapped")
        } label: {
            Text("One")
        }
            
        Button {
            print("button 2 tapped")
        } label: {
            Text("Two")
        }
    }
}

When only one of buttons is tapped once, I see the callbacks for both buttons being called, which is not what I want:

button 1 tapped
button 2 tapped
Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45

4 Answers4

239

You can apply any button style (for example, .bordered, .borderless, .borderedProminent, etc.) EXCEPT for .automatic.

    List([1, 2, 3], id: \.self) { row in
        HStack {
            Button(action: { print("Button at \(row)") }) {
                Text("Row: \(row) Name: A")
            }
            .buttonStyle(.borderless)
            
            Button(action: { print("Button at \(row)") }) {
                Text("Row: \(row) Name: B")
            }
            .buttonStyle(.plain)
        }
    }
Mark Moeykens
  • 15,915
  • 6
  • 63
  • 62
Ramis
  • 13,985
  • 7
  • 81
  • 100
  • 3
    This is the best solution if you need to maintain button animations (highlights, etc.) – sabiland Feb 05 '20 at 12:37
  • 29
    Worked a charm, but why? – Toby Jul 02 '20 at 10:07
  • 22
    Yes. This is a real WTAF for me. How did you discover this? –  Sep 10 '20 at 17:06
  • 5
    @Jessy Spending a lot of time in the documentation. – Ramis Oct 07 '20 at 20:54
  • 1
    You can use `PlainButtonStyle()` instead if you want it to be black / customizable instead of blue and also fixes the bug. – unixb0y Nov 13 '20 at 23:20
  • PlainButtonStyle() also works. I guess giving the style attribute makes the button works on the list row... – Hwangho Kim Dec 01 '20 at 10:31
  • @HwanghoKim thanks. I will update my answer. – Ramis Dec 01 '20 at 10:39
  • 2
    Excellent. It's work perfectly, even with a NavigationLink on all the row. – Luc-Olivier Dec 25 '20 at 00:47
  • 1
    This caused an unwanted issue with Menu/Pickers/DatePickers: When the floating menu is on the screen, you can still tap on the buttons with the style. With a normal button (a single one per row without style), tapping on it dismisses the menu. The same problem happens with `onTapGesture`, I'm almost giving up on SwiftUI for this screen. – Lluis Gerard May 17 '22 at 14:57
  • Also make sure you don't have a `NavigationLink` in the row's child views or on the background of the row or childviews. – waggles Jul 15 '22 at 23:21
29

Seems to be a specific issue concerning Button when contained in a List row.

Workaround:

List {
  HStack {
    Text("One").onTapGesture { print("One") }
    Text("Two").onTapGesture { print("Two") }
  }
}

This yields the desired output.

You can also use a Group instead of Text to have a sophisticated design for the "buttons".

Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45
Matteo Pacini
  • 21,796
  • 7
  • 67
  • 74
  • 2
    Unfortunately this bug isn't limited to Buttons within a List. I am able to reproduce the same issue with multiple NavigationLinks inside a HStack, inside of a List. – BlueSolrac Feb 18 '20 at 20:28
  • 1
    This is just a workaround. Using BorderlessButtonStyle() is the best way. – brokenrhino Mar 03 '20 at 00:44
  • While this works, please keep in mind that this change has more implications. The accessibility API (for example for VoiceOver users) will no longer recognize "One" and "Two" as button. You would have to give it a button trait. I would say that adding a buttonStyle is the better way. – n-develop Nov 15 '22 at 07:36
10

One of the differences with SwiftUI is that you are not creating specific instances of, for example UIButton, because you might be in a Mac app. With SwiftUI, you are requesting a button type thing.

In this case since you are in a list row, the system gives you a full size, tap anywhere to trigger the action, button. And since you've added two of them, both are triggered when you tap anywhere.

You can add two separate Views and give them a .onTapGesture to have them act essentially as buttons, but you would lose the tap flash of the cell row and any other automatic button like features SwiftUI would give.

List {
    HStack {
        Text("One").onTapGesture {
            print("Button 1 tapped")
        }

        Spacer()

        Text("Two").onTapGesture {
            print("Button 2 tapped")
        }
    }
}
Bradley Mackey
  • 6,777
  • 5
  • 31
  • 45
Jonathan Bennett
  • 1,055
  • 7
  • 14
  • 5
    tapAction is no longer available (Xcode 11.2 beta). Use .onTapGesture() – Carla Camargo Oct 08 '19 at 16:02
  • Also you have to be careful as the tap gesture will only be applied to the visible area of the text itself. Every transparent area of your view won't be clickable. – Kn3cht Aug 20 '23 at 15:42
6

You need to create your own ButtonStyle:

  struct MyButtonStyle: ButtonStyle {
    func makeBody(configuration: Configuration) -> some View {
      configuration.label
        .foregroundColor(.accentColor)
        .opacity(configuration.isPressed ? 0.5 : 1.0)
    }
  }

  struct IdentifiableString: Identifiable {
    let text: String
    var id: String { text }
  }

  struct Test: View {
    var body: some View {
      List([
        IdentifiableString(text: "Line 1"),
        IdentifiableString(text: "Line 2"),
      ]) {
        item in
        HStack {
          Text("\(item.text)")
          Spacer()
          Button(action: { print("\(item.text) 1")}) {
            Text("Button 1")
          }
          Button(action: { print("\(item.text) 2")}) {
            Text("Button 2")
          }
        }
      }.buttonStyle(MyButtonStyle())
    }
  }
Anton
  • 1,655
  • 15
  • 16