Swiftui - Two Buttons in a List

SwiftUI - Multiple Buttons in a List row

You need to use BorderlessButtonStyle() or PlainButtonStyle().

    List([1, 2, 3], id: \.self) { row in
HStack {
Button(action: { print("Button at \(row)") }) {
Text("Row: \(row) Name: A")
}
.buttonStyle(BorderlessButtonStyle())

Button(action: { print("Button at \(row)") }) {
Text("Row: \(row) Name: B")
}
.buttonStyle(PlainButtonStyle())
}
}

SwiftUI - Two buttons in a List

Set the button style to something different from the default, e.g., BorderlessButtonStyle()

struct Test: View {
var body: some View {
NavigationView {
List {
ForEach([
"Line 1",
"Line 2",
], id: \.self) {
item in
HStack {
Text("\(item)")
Spacer()
Button(action: { print("\(item) 1")}) {
Text("Button 1")
}
Button(action: { print("\(item) 2")}) {
Text("Button 2")
}
}
}
.onDelete { _ in }
.buttonStyle(BorderlessButtonStyle())
}
.navigationBarItems(trailing: EditButton())
}
.accentColor(.red)
}
}

How do I keep multiple button actions separate in SwiftUI ForEach content?

This is default behaviour of List, it identifies Button in row and makes entire row active, use instead .onTapGesture as below

List {
ForEach(tasks, id: \.self) { task in
HStack {

Image(systemName: task.isComplete ? "square.fill" : "square")
.padding()
.onTapGesture {
task.isComplete.toggle()
try? self.moc.save()
print("Done button tapped")
}

Text(task.name ?? "Unknown Task")
Spacer()

Image("timer")
.onTapGesture {
print("timer button tapped")
}
}
}
.onDelete(perform: deleteTask)
}

Two Buttons inside HStack taking action of each other

Need to use onTapGesture instead of action like this way.

Button(action: {}) {
Text("watch")
}
.frame(minWidth: 0, maxWidth: .infinity)
.background(Color.red)
.onTapGesture {
print("watch")
}

SwiftUI Multiple Buttons with Popovers in HStack Behavior

Normally you'd use popover(item:content:), but you'll get an error... even the example in the documentation crashes.

*** Terminating app due to uncaught exception 'NSGenericException', reason: 'UIPopoverPresentationController (<UIPopoverPresentationController: 0x14a109890>) should have a non-nil sourceView or barButtonItem set before the presentation occurs.'

What I came up with instead is to use a singular @State presentingItem: Item? in ContentView. This ensures that all the popovers are tied to the same State, so you have full control over which ones are presented and which ones aren't.

But, .popover(isPresented:content:)'s isPresented argument expects a Bool. If this is true it presents, if not, it will dismiss. To convert presentingItem into a Bool, just use a custom Binding.

Binding(
get: { presentingItem == item }, /// present popover when `presentingItem` is equal to this view's `item`
set: { _ in presentingItem = nil } /// remove the current `presentingItem` which will dismiss the popover
)

Then, set presentingItem inside each button's action. This is the part where things get slightly hacky - I've added a 0.5 second delay to ensure the current displaying popover is dismissed first. Otherwise, it won't present.

if presentingItem == nil { /// no popover currently presented
presentingItem = item /// dismiss that immediately, then present this popover
} else { /// another popover is currently presented...
presentingItem = nil /// dismiss it first
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
presentingItem = item /// present this popover after a delay
}
}

Full code:

/// make equatable, for the `popover` presentation logic
struct Item: Equatable {
let id = UUID()
var name: String
}

struct ContentView: View {

@State var presentingItem: Item? /// the current presenting popover
let items = [
Item(name: "item1"),
Item(name: "item2"),
Item(name: "item3")
]

var body: some View {
HStack {
MyGreatItemView(presentingItem: $presentingItem, item: items[0])
MyGreatItemView(presentingItem: $presentingItem, item: items[1])
MyGreatItemView(presentingItem: $presentingItem, item: items[2])
}
.padding(300)
}
}

struct MyGreatItemView: View {
@Binding var presentingItem: Item?
let item: Item /// this view's item

var body: some View {
Button(action: {
if presentingItem == nil { /// no popover currently presented
presentingItem = item /// dismiss that immediately, then present this popover
} else { /// another popover is currently presented...
presentingItem = nil /// dismiss it first
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if presentingItem == nil { /// extra check to ensure no popover currently presented
presentingItem = item /// present this popover after a delay
}
}
}
}) {
Text(item.name)
}

/// `get`: present popover when `presentingItem` is equal to this view's `item`
/// `set`: remove the current `presentingItem` which will dismiss the popover
.popover(isPresented: Binding(get: { presentingItem == item }, set: { _ in presentingItem = nil }) ) {
PopoverView(item: item)
}
}
}

struct PopoverView: View {
let item: Item /// no need for @State here
var body: some View {
print("new PopoverView")
return Text("View for \(item.name)")
}
}

Result:

Presenting popovers consecutively works

Buttons in SwiftUI List ForEach view trigger even when not tapped ?

Whenever you have multiple buttons in a list row, you need to manually set the button style to .borderless or .plain. This is because buttons “adapt” to their context.
According to the documentation:

If you create a button inside a container, like a List, the style resolves to the recommended style for buttons inside that container for that specific platform.

So when your button is in a List, its tap target extends to fill the row and you get a highlight animation. SwiftUI isn’t smart enough to stop this side effect when you have more than 2 buttons, so you need to set buttonStyle manually.

CellTestView()
.buttonStyle(.borderless)

Result:

Tapping top and bottom button results in separate print statements



Related Topics



Leave a reply



Submit