Multiple Buttons in a SwiftUI List Element

If it looks like a button and behaves like a button, then it probably should be a button

ยท

3 min read

While writing another post for my accessibility series, I created a simple UI that contained a few Buttons inside of List row.

A comparable example view could look like this:

struct ExampleView: View {
    var body: some View {
        List {
            HStack {
                Button {
                    debugPrint("First button tapped")
                } label: {
                    Image(systemName: "moon.stars.fill")
                }

                Button {
                    debugPrint("Second button tapped")
                } label: {
                    Image(systemName: "testtube.2")
                }

                Button {
                    debugPrint("Third button tapped")
                } label: {
                    Image(systemName: "lock.doc")
                }
            }
            .font(.title)
        }
    }
}

It's just a list containing a single Element (the HStack), which itself contains three buttons. Every button prints a simple text using debugPrint when it is tapped. But if you run this example, tap one of the buttons and take a look at the output window, you will see this:

"First button tapped"
"Second button tapped"
"Third button tapped"

You tapped just one button, but all actions were fired. That's not great. While checking stackoverflow for a solution, I saw an answer saying "Just use onTapGesture instead of a Button. It will work just fine." So the example would look like this:

// WARNING:
// This is not a good solution. Please don't do it this way!

struct ExampleView: View {
    var body: some View {
        List {
            HStack {
                Image(systemName: "moon.stars.fill")
                    .onTapGesture {
                        debugPrint("First button tapped")
                    }

                Image(systemName: "testtube.2")
                    .onTapGesture {
                        debugPrint("Second button tapped")
                    }

                Image(systemName: "lock.doc")
                    .onTapGesture {
                        debugPrint("Third button tapped")
                    }
            }
            .font(.title)
        }
    }
}

And yes, this will fix the problem of all actions being called when just tapping one of the buttons. BUT please don't do that. Or at least don't leave it like that. The problem with this solution is, that you are losing some important information about your "buttons". Why? Because it is not a button anymore. It's an image with a gesture recognizer. It's not the same. Try using your app with VoiceOver now. While stepping through the image you will hear the following:

Clear night, Image
Testtube 2, Image
Lock Doc, Image

There is no clue about the attached tap gesture. That's bad. SwiftUI comes with a lot of great accessibility features out of the box. A Button has the isButton accessibility trait (for obvious reasons), which will help VoiceOver users navigate and use your app.

If something looks like a button and behaves like a button, it should be a Button.

Of course, you could just add .accessibilityAddTraits(.isButton) to the image, but that feels wrong, doesn't it? Luckily, there is a simple fix for that. Just assign a ButtonStyle to your buttons and the problem is solved.

struct ExampleView: View {
    var body: some View {
        List {
            HStack {
                Button {
                    debugPrint("First button tapped")
                } label: {
                    Image(systemName: "moon.stars.fill")
                }
                .buttonStyle(.borderless)

                Button {
                    debugPrint("Second button tapped")
                } label: {
                    Image(systemName: "testtube.2")
                }
                .buttonStyle(.borderless)

                Button {
                    debugPrint("Third button tapped")
                } label: {
                    Image(systemName: "lock.doc")
                }
                .buttonStyle(.borderless)
            }
            .font(.title)
        }
    }
}

You could even simplify it a little more by moving the .buttonStyle(.borderless) to the HStack:

struct ExampleView: View {
    var body: some View {
        List {
            HStack {
                Button {
                    debugPrint("First button tapped")
                } label: {
                    Image(systemName: "moon.stars.fill")
                }

                Button {
                    debugPrint("Second button tapped")
                } label: {
                    Image(systemName: "testtube.2")
                }

                Button {
                    debugPrint("Third button tapped")
                } label: {
                    Image(systemName: "lock.doc")
                }
            }
            .buttonStyle(.borderless)
            .font(.title)
        }
    }
}

It's just a single line that will fix the problem. ๐ŸŽ‰

ย