SwiftUI - Multiple Buttons in a List row - ios

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

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

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".

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

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

Related

How can I add multiple gesture handlers to Text without breaking scrolling?

I have text in a scroll-view that I would like to add a context menu on. I also want to close the keyboard when the context menu is open.
Here is some sample code:
struct TestView: View {
var body: some View {
let texts = Array(1...100).map { "Test \($0)" }
ScrollView {
VStack(spacing: 10) {
ForEach(texts, id: \.self) { text in
Text(text)
.frame(maxWidth: .infinity)
.contextMenu {
Button {
UIPasteboard.general.string = text
} label: {
Label("Copy", systemImage: "doc.on.clipboard")
}
}
.simultaneousGesture(LongPressGesture(minimumDuration: 0.5).onEnded { _ in
print("Do something else when the context menu is opened. (i.e. close keyboard)")
})
}
}
}
}
}
This code prevents me from being able to scroll on the scrollview starting from the text though.
I've seen in other answers that adding onTapGesture before the other gestures can fix it, but it doesn't seem to work in this case.

swiftui bring view to center and blur background after long press like messages app

I can't seem to find how to bring a view to the center like a popup and blur background like when you long press a message in the messages app.
I know there is a simple native way to do this but I can't seem to find out how.
You are looking for contextMenu(menuItems:). It shows those buttons you see, and automatically blurs the background content to focus the selected view.
Example:
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 30) {
ForEach(0 ..< 20) { _ in
Text(String(Int.random(in: 1 ... 100000000)))
.padding()
.contextMenu {
Button {
//
} label: {
Label("Reply", systemImage: "arrowshape.turn.up.left")
}
Button {
//
} label: {
Label("Copy", systemImage: "doc.on.doc")
}
Button {
//
} label: {
Label("Translate", systemImage: "arrow.left.arrow.right")
}
Button {
//
} label: {
Label("Moreā€¦", systemImage: "ellipsis.circle")
}
}
}
}
}
}
}
Result:

Swiftui Button Toggle

I have a series of buttons like this:
ForEach(context.items){item in
Button(action: {
self.variableSelection.toggle()
}) {
Text("\(item)")
if self.variableSelection{
Text("Extra text")
}
}
}
What is happening is:
A list of items appears and when I click a button the "Extra text" appears on the bottom of each button.
What I wanted to happen is for the "Extra text" to appear only below the button I clicked. How can I achieve that?
Thanks
Extract row into separated view, so each row has own state of expanded/collapsed, like
struct ItemRowView: View {
let item: String
#State private var variableSelection = false
var body: some View {
Button(action: {
self.variableSelection.toggle()
}) {
Text("\(item)")
if self.variableSelection{
Text("Extra text")
}
}
}
}
so now cycle
ForEach(context.items){item in
ItemRowView(item: item)
}

Strange buttons behaviour in a list (SwiftUI)

In the following SwiftUI code I noticed some unexpected behaviour.
I wonder if this is a bug, if this is normal or if I am only missing something obvious.
List {
ForEach(self.myList, id: \.self.name) {
item in
HStack {
Spacer()
Button(action: {
print("Button One tapped!")
....
}) {
item.name.map(Text.init)
.font(.largeTitle)
.foregroundColor(.secondary)
}
Spacer()
Button(action: {
print("Button Two tapped!")
....
}) {
Image(systemName: "pencil.circle")
.font(.title)
.foregroundColor(.secondary)
.padding(.leading, 17)
}
}
}
.onDelete(perform: deleteFunc)
}
Now here is what happens when tapping one of the two buttons on a row.
I can see these two messages:
Button One tapped!
Button Two tapped!
I expect to see only one message, depending on the button tapped.
If the order of the messages varied according to the button tapped; I could use a boolean or two to enforce the end result I want. But the two messages always appear in the same order.
Has anyone had the same experience? Or does anyone see any mistake?
Use PlainButtonStyle (or any custom one), because default button style is detected by List automatically to highlight entire row.
Here is a simplified (from your code) demo:
struct DemoListWithButtons: View {
var body: some View {
List {
ForEach(0..<5, id: \.self) {
item in
HStack {
Spacer()
Button(action: {
print("Button One tapped!")
}) {
Text("First")
}.buttonStyle(PlainButtonStyle()) // << here !!
Spacer()
Button(action: {
print("Button Two tapped!")
}) {
Text("Second")
}.buttonStyle(PlainButtonStyle()) // << here !!
}
}
}
}
}

SwiftUI button inactive inside NavigationLink item area

I have a view for a list item that displays some news cards within a navigationLink.
I am supposed to add a like/unlike button within each news card of navigationLink, without being took to NavigationLink.destination page.
It seems like a small button inside a big button.
When you click that small one, execute the small one without executing the bigger one.
(note: the click area is covered by the two buttons, smaller one has the priority)
(In javascript, it seems like something called .stopPropaganda)
This is my code:
var body: some View {
NavigationView {
List {
ForEach(self.newsData.newsList, id:\.self) { articleID in
NavigationLink(destination: NewsDetail(articleID: articleID)) {
HStack {
Text(newsTitle)
Button(action: {
self.news.isBookmarked.toggle()
}) {
if self.news.isBookmarked {
Image(systemName: "bookmark.fill")
} else {
Image(systemName: "bookmark")
}
}
}
}
}
}
}
}
Currently, the button action (like/dislike) will not be performed as whenever the button is pressed, the navigationLink takes you to the destination view.
I have tried this almost same question but it cannot solve this problem.
Is there a way that makes this possible?
Thanks.
as of XCode 12.3, the magic is to add .buttonStyle(PlainButtonStyle()) or BorderlessButtonStyle to the button, when said button is on the same row as a NavigationLink within a List.
Without this particular incantation, the entire list row gets activated when the button is pressed and vice versa (button gets activated when NavigationLink is pressed).
This code does exactly what you want.
struct Artcle {
var text: String
var isBookmarked: Bool = false
}
struct ArticleDetail: View {
var article: Artcle
var body: some View {
Text(article.text)
}
}
struct ArticleCell: View {
var article: Artcle
var toggle: () -> ()
#State var showDetails = false
var body: some View {
HStack {
Text(article.text)
Spacer()
Button(action: {
self.toggle()
}) {
Image(systemName: article.isBookmarked ? "bookmark.fill" : "bookmark").padding()
}
.buttonStyle(BorderlessButtonStyle())
}
.overlay(
NavigationLink(destination: ArticleDetail(article: article), isActive: $showDetails) { EmptyView() }
)
.onTapGesture {
self.showDetails = true
}
}
}
struct ContentView: View {
#State var articles: [Artcle]
init() {
_articles = State(initialValue: (0...10).map { Artcle(text: "Article \($0 + 1)") })
}
func toggleArticle(at index: Int) {
articles[index].isBookmarked.toggle()
}
var body: some View {
NavigationView {
List {
ForEach(Array(self.articles.enumerated()), id:\.offset) { offset, article in
ArticleCell(article: article) {
self.toggleArticle(at: offset)
}
}
}
}
}
}

Resources