Navigation bar menu with multiple selection in iOS app with SwiftUI - ios

I want to achieve behavior close to native iOS reminders app on iPad.
There is dropdown Menu in NavigationBar that shows tags in it. I thought that I can do it with Picker but as I see there is now multiple selection support in it.
I did it using
...
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
MenuView()
} label: {
Image(systemName: "number")
}
Button("Done") {}
}
}
And MenuView's code is
struct MenuView: View {
#State var tags = ["#tag1", "#tag2", "#tag3", "#tag4"]
var body: some View {
ForEach(tags, id:\.self) { tag in
Button {
} label: {
if selectedTags.contains(tag) {
Label(tag, systemImage: "checkmark")
} else {
Text(tag)
}
}
}
Section {
Button {
} label: {
Label("Configure", systemImage: "ellipsis")
}
}
}
}
The main problem right now is when I press any of this buttons, drop down is closed, while I want this button to only toggle selected status.
I tried to use Text instead of buttons but didn't find way to handle tap gestures from them, they look like being disabled by some view modifier.
What is the optimal way to handle it?

Related

Can't hide items within ToolbarItemGroup, disappears on iPhone

I have a view with a set of toolbar items where I sometimes want to hide some of the items when activating a certain state.
Consider the following SwiftUI code:
#State var hideToolbarItems = false
var body: some View {
NavigationSplitView {
Text("Hello, World!")
.navigationTitle("MyToolbarItems")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
hideToolbarItems.toggle()
} label: {
Text("Toggle")
}
}
ToolbarItemGroup(placement: .navigationBarTrailing) {
if !hideToolbarItems {
Button {
} label: {
Text("Button1")
}
Button {
} label: {
Text("Button2")
}
Button {
} label: {
Text("Button3")
}
}
}
}
} detail: {
}
}
When previewing this it seems to work as expected, the ToolbarItemGroup disappears when hideToolbarItems becomes true and appears again when hideToolbarItems is false. On an iPad device it works almost the same, the ToolbarItemGroup becomes grouped under a "..."-icon until the hideToolbarItems has changed back and forth, then it won't become grouped under a "..."-icon anymore.
But the real problem is that the ToolbarItemGroup won't show at all on an iPhone device. Has anyone solved this problem on iPhone? I can't seem to find a good solution.
I am running Xcode 14.2 and use iOS 16.0.
SwiftUI effects different result by platforms.
ToolbarItemGroup may be compact button only in iPad.
if you want to show buttons as compact anytime, you can use Menu.
ToolbarItemGroup(placement: .navigationBarTrailing) {
if !hideToolbarItems {
Menu {
Button {
} label: {
Text("Button1")
}
Button {
} label: {
Text("Button2")
}
Button {
} label: {
Text("Button3")
}
} label: {
Image(systemName: "ellipsis.circle")
}
}
}

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.

Hide toolbar item on Menu open

I trying to build a menu like the Notes app on iOS. On press of the ellipsis-circle button, it should release focus on the text and hide the Done toolbar item.
This is my code:
#FocusState private var isEditing: Bool
var body: some View {
AnnotatedTextEditor(text: $text)
.focused($isEditing)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
Button(role: .destructive, action: delete) {
Label("Delete", systemImage: .trash)
}
} label: {
Label("Menu", systemImage: .ellipsisCircle)
}
.onPress {
isEditing = false
}
if (isEditing) {
Button("Done", action: done)
}
}
}
}
The AnnotatedTextEditor is a UITextView wrapped in a UITextViewRepresentable for SwiftUI compatibility.
Working: The text view is unfocused and the done toolbar item disappears when the menu toolbar item is presses.
Not working: the menu does not open.
When I show the Done button un-conditionally, it works as expected, so I assume the issue is that the toolbar changes on the state change.
Is there any workaround to achieve hiding the toolbar's done button on menu open?
Screenshots:
My app (not working as expected):
iOS Notes app (working as expected):

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