SwiftUI List with NavigationLink and Buttons - ios

I have a List of counters - every element in the List is a counter with a number and plus/minus buttons users can click to increase or decrease the counter. Now I added a NavigationLink so that users can go to a detail view of every counter. The problem is now wherever you click on the list the detail view gets always pushed - even if you click on one of the buttons (counter increases then the detail view gets pushed via the NavigationLink) - I only want to use the NavigationLink if the user clicks on the number or somewhere else but of course not if the users clicks on of the buttons. How can this be done?
NavigationView {
List {
ForEach(counters, id: \.self) { counter in
NavigationLink(destination: SingleCounterView(currentCounter: counter)) {
CounterCell(counter: counter)
}
}
}
.buttonStyle(PlainButtonStyle())
.listStyle(GroupedListStyle())
}

I've made this test, to show a CounterCell with plus and minus buttons in a NavigationView. If you tap on the buttons the counter increments, if you tap on the chevron or outside the buttons the destination appears.
#State var counters = ["a","b","c"]
var body: some View {
NavigationView {
List {
ForEach(counters, id: \.self) { counter in
NavigationLink(destination: Text(counter)) {
CounterCell(counter: counter)
}
}
}
.buttonStyle(PlainButtonStyle())
.listStyle(GroupedListStyle())
}.navigationViewStyle(StackNavigationViewStyle())
}
struct CounterCell: View {
#State var counter: String
#State var inc = 0
var body: some View {
HStack {
Button(action: { self.inc += 1 }) {
Text("plus")
}
Button(action: { self.inc -= 1 }) {
Text("minus")
}
Text(" counter: \(counter) value: \(inc)")
}
}
}

Related

Swift UI: What is the easiest way to create unique instances of a struct that save individual data?

I would like to build a list app that is similar to "Notes" or "Reminders". Whenever I click a row of the list (like in "Reminders", when you click on a list), I would like it to take me to a newly generated view, with individual data (the list, with a title, and data).
I built a first screen that shows a List, and each row is a Navigation View to a new screen which contains a new list. The second list has three rows, each row an HStack with a preset string to the left, and a textfield to the right. The textfield to the right is editable.
Upon click, the only difference between separate instances of default second views should be the title, being a string equal to the string in the row of the first list.
I would like to click on the textfield, write something down, and that string to remain saved to the second view.
The code I am showing here does not create separate instances of second view. Whenever I click a row in the first list, I get the same second view, with the same data.
So if I click the row with "First Entry", I am taken to second view. I change the "Name" textfield to "Charles", save it in AppStorage, and when I exit this view and click the "Second Row", I am taken to the same iteration of second view, with "Name: Charles".
Thank you kindly!
import SwiftUI
/// THE FIRST VIEW
struct ContentView: View {
//1. Theme Color
#State var themeColor: Color = Color.orange
//2. Default Entry
#State var firstListDefault: [String] = ["First Entry", "Second Entry", "Third Entry"]
//3. Empty String for Text Field
#State var toAddEntry = ""
// BODY
var body: some View {
//A. List
List {
//B. For each - To a second view that takes a title = to entry
ForEach(firstListDefault, id: \.self) {index in
NavigationLink {
SecondView(title: index)
}
//C. Row Label
label: {
HStack {
Image(systemName: "circle.inset.filled").foregroundColor(themeColor)
Text(index)
}
}
//D. DELETE AND MOVE + NAVIGATION TITLE
}
.onDelete(perform: { IndexSet in
firstListDefault.remove(atOffsets: IndexSet)
})
.onMove(perform: { IndexSet, Int in
firstListDefault.move(fromOffsets: IndexSet, toOffset: Int)
})
.navigationTitle(Text("Client List"))
//E. NEW ENTRY TEXT FIELD
TextField("New Entry", text: $toAddEntry)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.onSubmit {
firstListDefault.append(toAddEntry)
toAddEntry = ""
}
}
//F. TOOLBAR
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton().foregroundColor(themeColor)
}
}
}
}
/// THE SECOND VIEW
struct SecondView: View {
#AppStorage("name") var inputText1 = ""
#AppStorage("file") var inputText2 = ""
#AppStorage("due") var inputText3 = ""
let title: String
var body: some View {
List {
HStack {
Text("Client Name:")
TextField("...", text: $inputText1).autocorrectionDisabled()
.textInputAutocapitalization(.never)
.multilineTextAlignment(.trailing)
}.navigationTitle(Text(title))
HStack {
Text("File Number:")
Spacer()
TextField("...", text: $inputText2).autocorrectionDisabled()
.textInputAutocapitalization(.never)
.multilineTextAlignment(.trailing)
}
HStack {
Text("Due date:")
Spacer()
TextField("...", text: $inputText3).autocorrectionDisabled()
.textInputAutocapitalization(.never)
.multilineTextAlignment(.trailing)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
ContentView()
}
}
}

iOS button and navigationLink next to each other in List

I have a List, each element has its own HStack that contains Button and NavigationLink to the next view but both (checkbox button and navigation link) is activated wherever I click on single HStack element.
That means icon on the button changes when I click on the element but application also loads the next view. The same happens when I want to go to the next view by simply clicking on the NavigationLink. Can you help me separate this two functionalities (checkbox Button and NavigationLink)?
struct ContentView: View {
#ObservedObject var spendingList: SpendingList
var body: some View {
NavigationView {
List {
ForEach($spendingList.spendings) { $spending in
HStack{
Button(action: {
spending.Bought = !spending.Bought
}, label: {
if spending.Bought == false {
Image(systemName: "square")
.foregroundColor(.accentColor)
} else {
Image(systemName: "checkmark.square")
.foregroundColor(.accentColor)
}
})
NavigationLink(destination: DetailView(spending: $spending)
.navigationTitle(Text(spending.Name)),
label: {
Text(spending.Name).frame(maxWidth: .infinity, alignment: .leading)
if spending.Price != 0 {
Text(String(spending.Price)).frame(maxWidth: .infinity, alignment: .trailing)
} else {
Text("empty").foregroundColor(.gray)
}
})
}
}
.navigationTitle(Text("Spending Priority"))
}
}
}
}
The default Style of Button & NavigationLink makes the whole row click as one. However, using PlainButtonStyle() fixes the issue by making the button clickable & not the cell:
.buttonStyle(.plain)//to your button & NavigationLink
Unfortunately, the parent-view list item becomes the Navigation link. So the button press will never be recorded.
In iOS 16 you can solve this by replacing the NavigationLink with a Button and pushing an item onto the navigation stack with the Button.
Documentation: https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
Something like this, for example:
struct ContentView: View {
#State var path: [View] = []
var body: some View {
NavigationStack(path: $path) {
List {
ForEach($spendingList.spendings) { $spending in
HStack {
Button(action: {
spending.Bought = !spending.Bought
}, label: {
Image(...)
})
Button(action: {
path.append(DetailView(spending: $spending))
}, label: {
Text(...)
})
Text(spending.Name)
})
}
}
}
.navigationTitle(Text("Spending Priority"))
.navigationDestination(for: View.self) { view in
view
}
}
}
}

SwitUI - Two navigationLink in a list

I have two NavigationLink in a cell of my List
I want to go to destination1 when I tap once,and go to destination2 when I tap twice.
So I added two tap gesture to control the navigation.
But when I tap,there are two questions:
1 The tap gesture block won't be called.
2 The two navigation link will be both activated automatically even if they are behind a TextView.
The real effect is: Tap the cell -> go to Destination1-> back to home -> go to Destination2 -> back to home
Here is my code :
struct MultiNavLink: View {
#State var mb_isActive1 = false;
#State var mb_isActive2 = false;
var body: some View {
return
NavigationView {
List {
ZStack {
NavigationLink("", destination: Text("Destination1"), isActive: $mb_isActive1)
NavigationLink("", destination: Text("Destination2"), isActive: $mb_isActive2)
Text("Single tap::go to destination1\nDouble tap,go to destination2")
}
.onTapGesture(count: 2, perform: {()->Void in
NSLog("Double tap::to destination2")
self.mb_isActive2 = true
}).onTapGesture(count: 1, perform: {()->Void in
NSLog("Single tap::to destination1")
self.mb_isActive1 = true
})
}.navigationBarTitle("MultiNavLink",displayMode: .inline)
}
}
}
I have tried remove the List element,then everything goes as I expected.
It seems to be the List element that makes everything strange.
I found this question:SwiftUI - Two buttons in a List,but the situation is different from me.
I am expecting for your answer,thank you very much...
Try the following approach - the idea is to hide links in background of visible content and make them inactive for UI, but activated programmatically.
Tested with Xcode 12 / iOS 14.
struct MultiNavLink: View {
var body: some View {
return
NavigationView {
List {
OneRowView()
}.navigationBarTitle("MultiNavLink", displayMode: .inline)
}
}
}
struct OneRowView: View {
#State var mb_isActive1 = false
#State var mb_isActive2 = false
var body: some View {
ZStack {
Text("Single tap::go to destination1\nDouble tap,go to destination2")
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.contentShape(Rectangle())
.background(Group {
NavigationLink(destination: Text("Destination1"), isActive: $mb_isActive1) {
EmptyView() }
.buttonStyle(PlainButtonStyle())
NavigationLink(destination: Text("Destination2"), isActive: $mb_isActive2) {
EmptyView() }
.buttonStyle(PlainButtonStyle())
}.disabled(true))
.highPriorityGesture(TapGesture(count: 2).onEnded {
self.mb_isActive2 = true
})
.onTapGesture(count: 1) {
self.mb_isActive1 = true
}
}
}
Navigation link has a initializer that takes a binding selection and whenever that selection is set to the value of the NavigationLink tag, the navigation link will trigger.
As a tip, if the app can't differentiate and identify your taps, and even with two taps, still the action for one-tap will be triggered, then you can use a simultaneous gesture(.simultaneousGesture()) modifier instead of a normal gesture(.gesture()) modifier.
struct someViewName: View {
#State var navigationLinkTriggererForTheFirstOne: Bool? = nil
#State var navigationLinkTriggererForTheSecondOne: Bool? = nil
var body: some View {
VStack {
NavigationLink(destination: SomeDestinationView(),
tag: true,
selection: $navigationLinkTriggererForTheFirstOne) {
EmptyView()
}
NavigationLink(destination: AnotherDestinationView(),
tag: true,
selection: $navigationLinkTriggererForTheSecondOne) {
EmptyView()
}
NavigationView {
Button("tap once to trigger the first navigation link.\ntap twice to trigger the second navigation link.") {
// tap once
self.navigationLinkTriggererForTheFirstOne = true
}
.simultaneousGesture(
TapGesture(count: 2)
.onEnded { _ in
self.navigationLinkTriggererForTheSecondOne = true
}
)
}
}
}
}

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

Handle Tab Selection SwiftUI

Following tutorials, I have the following code to show a tab view with 3 tab items all with an icon on them, When pressed they navigate between the three different views. This all works fine, however, I want to be able to handle the selection and only show views 2 or 3 if certain criteria are met.
Is there a way to get the selected value and check it then check my own criteria and then show the view is criteria is met, or show an alert if it is not saying they can't use that view at the moment.
Essentially I want to be able to intercept the selection value before it switches out the view, maybe I need to rewrite all of this but this is the functionality I'm looking for as this is how I had my previous app working using the old framework.
#State private var selection = 1
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
}
}
You can do it by changing the value of selection on tap. You can use .onAppear() method for a particular tab to check your condition:
#State private var selection = 1
var conditionSatisfied = false
var body: some View
{
TabbedView(selection: $selection)
{
View1().tabItemLabel(
VStack
{
Image("icon")
Text("")
})
.tag(1)
View2().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(2)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
View3().tabItemLabel(
VStack
{
Image("icon")
Text("")
}).tag(3)
.onAppear() {
if !conditionSatisfied {
self.selection = 1
}
}
}
}

Resources