SwiftUI. Fix custom buttons inside List item called one by one
Hi everyone.
The issue I faced today made me write this short how-to. Not only for those who potentially could face the same issue, but for myself as well in order just not to forget.
So here is a brief of what I have faced or, let it be more precise, what I have discovered.
When you put custom buttons inside a List item view, for some reason all button actions are getting called, even if you tap only one button.
Now details.
We have a list with items which is HStack with two buttons. Like this:
struct ContentView: View {
var body: some View {
List {
ForEach(0..<10) { index in
HStack {
Button(action: {
print("Button 1 tapped")
openView1()
}) {
Text("Button 1")
}
Spacer()
Button(action: {
print("Button 2 tapped")
openView2()
}) {
Text("Button 2")
}
}
.padding()
}
}
}
func openView1() {
// Navigate to view1
print("Opening View 1")
}
func openView2() {
// Navigate to view2
print("Opening View 2")
}
}
Now if you run this code and tap on Button 1, you will see the output in logs like this:
1. Opening View 1
2. Opening View 2
It ain’t obvious since we tap a Button 1 and action for the button 1 should be called.
If you wrap ForEach with VStack, everything should work as expected. Moreover, it will work as expected.
The fact that the buttons work correctly when wrapped in a VStack but not in a List suggests that the issue is likely related to how List manages its rows and interaction handling in SwiftUI.
Possible Reasons:
1. Row Selection Interference:
• In a List, each row may have some default selection behavior, even if it’s not visibly selectable. This could interfere with the individual button actions.
2. Row Recycling:
• List in SwiftUI reuses views for performance reasons, which can sometimes cause issues with event handling, especially if the views inside the List are complex.
3. Gesture Conflicts:
- The List might be intercepting taps or gestures, causing both button actions to trigger.
The third reason isn’t our case since we do not have any custom gestures defined on a List.
Second reason could be potential one, but issue is much simpler. And we smoothly come to a reason number 1.
In a List, each row may have some default selection behavior, even if it’s not visibly selectable. This could interfere with the individual button actions.
So what should we do next? The answer lies inside the reason. The default selection behaviour which somehow should be changed.
There is a modifier in SwiftUI called .buttonStyle
The definition in source code is the following:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension View {
/// Sets the style for buttons within this view to a button style with a
/// custom appearance and custom interaction behavior.
///
/// Use this modifier to set a specific style for button instances
/// within a view:
///
/// HStack {
/// Button("Sign In", action: signIn)
/// Button("Register", action: register)
/// }
/// .buttonStyle(.bordered)
///
public func buttonStyle<S>(_ style: S) -> some View where S : PrimitiveButtonStyle
}
So that’s what we are looking for. The .buttonStyle sets the style for the buttons with a custom appearance and custom interaction behaviour. And that is exactly what we are looking for.
Finally, after we change button style to a PlainButtonStyle(), the button handlers works as expected.
The final code looks as this:
struct ContentView: View {
var body: some View {
List {
ForEach(0..<10) { index in
HStack {
Button(action: {
print("Button 1 tapped")
openView1()
}) {
Text("Button 1")
}
.buttonStyle(PlainButtonStyle())
Spacer()
Button(action: {
print("Button 2 tapped")
openView2()
}) {
Text("Button 2")
}
.buttonStyle(PlainButtonStyle())
}
.padding()
}
}
}
func openView1() {
// Navigate to view1
print("Opening View 1")
}
func openView2() {
// Navigate to view2
print("Opening View 2")
}
}
Hope this helps anyone who struggled with the same issue and gives a little better understanding on what happens inside.
Happy coding!
☕ Enjoying this content? Support me!
Creating quality content takes time and effort, and your support helps keep it going! If you found this article helpful, consider buying me a coffee — it’s a small gesture that makes a big difference.
Thanks for your support! 💛