SwiftUI migrate NavigationView to NavigationStack or something - swiftui

I'm trying to take my app and migrate my opening NavigationView which has been deprecated to the new NavigationStack.
My opening screen looks like:
and the code that presents it is:
import SwiftUI
struct MainAppView: View {
#State var listSelection: Int = 1
#Binding var isLoggedIn: Bool
var body: some View {
NavigationView {
if DBManager.shared.checkDatabaseFileExists()
{
SideListView(listSelection: $listSelection)
HStack {
if listSelection == 1
{
AccountsListView()
}
if listSelection == 2
{
SetAsideBalanceListView()
}
if listSelection == 3
{
BankBalanceListView()
}
if listSelection == 4
{
SetupMenuView()
}
if listSelection == 5
{
ReportsListView()
}
} // END HSTACK
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END NAVIGATION VIEW
} // END BODY VIEW
} // END STRUCT VIEW
I did the simple thing of changing NavigationView to NavigationStack and while it compiles, it looks wrong:
Chris... the solution that I implemented using your suggestion is working and would be acceptable, except that the navigation behavior seems to have changed with NavigationSplitView and NavigationStack. When a selection is made in the left pane the corresponding view appears in the right pane. The view in the right pane has a NavigationLink to a subview. This works and the subview has the back button. What I noticed is that with NavigationView if the user in a subview clicks on the selection in the left pane, the view immediately pops the appropriate selected view to the right pane clearing the subview of some other selection that is showing. But in this model, using NavigationStack on the selected view, if the subview is showing for a given selection, clicking on the left pane selection has no effect until the existing subview is back buttoned to the parent view at which time the selected view is presented.
Like this:
And then selected the sub view looks OK like this:
But when I select in the left pane another selection like this:
Using NavigationView the subview for SetAside would immediately pop but instead only shows after I use the back button on the sub view...
UPDATES:
Here is the code I've implemented for the MainAppView.swift
import SwiftUI
struct MainAppView: View {
#State var listSelection: Int? = 1
#Binding var isLoggedIn: Bool
var body: some View {
if DBManager.shared.checkDatabaseFileExists()
{
NavigationSplitView {
SideListView(listSelection: $listSelection)
} detail: {
NavigationStack {
switch listSelection {
case 1: AccountsListView()
case 2: SetAsideBalanceListView()
case 3: BankBalanceListView()
case 4: SetupMenuView()
case 5: ReportsListView()
default: Text("Select View")
}
} // END NAVIGATION STACK
} // END NAVIGATION SPLIT VIEW DETAIL
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW'
} // END STRUCT VIEW
Here is the view code for the SetAsideBalanceListView you can comment out the DB function calls and get an idea of the code. They are all somewhat clones of each other
import SwiftUI
struct SetAsideBalanceListView: View {
var accounts: [Accounts.AccountRecord] {
var acctRec = [Accounts.AccountRecord]()
acctRec = Accounts.shared.selectAllAccounts()
return acctRec
}
var body: some View {
// NavigationStack {
VStack {
CustomDivider(horizontalpadding: 0, thickness: 1)
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(destination: SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)){
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
} // END VSTACK
.font(.title2)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.top, 20)
.navigationTitle("Managing Your Money - Set Aside")
// } // END NAVIGATION STACK
} // END BODY VIEW
} // END STRUCT VIEW
It looks like this:
And here is the subview that the view above displays.. This is the one that needs a full "back button" to see the selected view.. If you clone these to make the other subviews you'll get the results (I hope:-))
import SwiftUI
struct SetAsideBalanceView: View {
#State var account_code: Int
#State var currentBudgetYear: Int
#State var totalBalances: Double = 0.00
// Array of SetAside Balance records for View List
var setAsideBalances: [SetAsideBalances.SetAsideBalancesRecord]
{
return SetAsideBalances.shared.selectSetAsideBalancesForAccount(withAccountCode: self.account_code)
}
var body: some View {
GeometryReader { gr in
let viewWidth = gr.size.width * 1
let columns = [
GridItem(.fixed(viewWidth * 0.60), alignment: .leading),
GridItem(.fixed(viewWidth * 0.30), alignment: .trailing)
]
VStack {
if setAsideBalances.count > 0 {
SetAsideBalanceHeader(accountName: setAsideBalances[0].account_name!, budgetYear: currentBudgetYear)
ScrollView {
ForEach (setAsideBalances, id: \.self) { setAsideRecord in
SetAsideBalancesRow(accountCode: account_code, setAsideBalanceCode: setAsideRecord.set_aside_code, description: setAsideRecord.description, set_aside_balance: setAsideRecord.set_aside_balance, currentBudgetYear: currentBudgetYear)
.frame(height: 45)
}
}
.frame(height: CGFloat((setAsideBalances.count + 1) * 45))
LazyVGrid(columns: columns, spacing: 0) {
Text("TOTAL BALANCES")
.padding(.leading, 20)
Text("\(NumberFormatter.formatAsCurrency(value: totalBalances))")
.foregroundColor((totalBalances < 0 ) ? Color("negative") : nil)
}
.frame(maxWidth: .infinity)
.frame(height: 55)
.border(Color.primary)
.foregroundColor(.black)
.background(Rectangle().fill(Color("lightBlue")))
.font(.title2)
Spacer()
}
} // END VSTACK
.font(.title2)
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading
)
.padding(.top, 5)
.onAppear {
self.totalBalances = SetAsideBalances.shared.sumSetAsideBalancesForAccount(withAccountCode: self.account_code)
}
.font(.title2)
.navigationTitle("Managing Your Money - Set Aside")
.navigationBarTitleDisplayMode(.inline)
.ignoresSafeArea(edges: .bottom)
}
} // END BODY VIEW
} // END STRUCT VIEW
This is what the subview looks like:

I think I found it. We have to update the NavigationLink in the subview also to the new logic.
In SetAsideBalanceListView replace this:
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(destination: SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)){
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
with this:
ForEach (accounts, id: \.self) { accountRec in
let result = Budget.shared.getBudgetMonthAndYearForView(withAccountCode: accountRec.account_code)
NavigationLink(value: accountRec) { // HERE
HStack {
Image(systemName: "dollarsign.circle")
.foregroundColor(.white)
.background(Color("iconBlue"))
.cornerRadius(25)
.padding(.leading, 15)
Text(accountRec.account_name)
Spacer()
}
}
.foregroundColor(.primary)
Divider().background(Color.primary)
}// END FOR EACH
// HERE
.navigationDestination(for: AccountRecord.self) { accountRec in
SetAsideBalanceView(account_code: accountRec.account_code, currentBudgetYear: result.viewYear)
}

You want to switch to NavigationSplitView(sidebar: content:). As the name implies it has two elements: 1 the sidebar, and 2 the content area.
I also would like to suggest to exchange the many if statements for the view selection with a switch statement on listSelection.
#State var listSelection: Int = 1
#Binding var isLoggedIn: Bool
var body: some View {
if DBManager.shared.checkDatabaseFileExists()
{
NavigationSplitView {
SideListView(listSelection: $listSelection)
} detail: {
switch listSelection {
case 1: AccountsListView()
case 2: SetAsideBalanceListView()
case 3: BankBalanceListView()
case 4: SetupMenuView()
case 5: ReportsListView()
default: Text("Select View")
}
}
} else {
Text("NO DATABASE FOUND!!!!")
.font(.system(size: 60))
} // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW

extended version including detail links.
struct ContentView: View {
#State var listSelection: Int? = 1
// #Binding var isLoggedIn: Bool
var body: some View {
// if DBManager.shared.checkDatabaseFileExists()
// {
NavigationSplitView {
List(selection: $listSelection) {
Label("Accounts", systemImage: "dollarsign.circle").tag(1)
Label("Set Aside", systemImage: "folder").tag(2)
Label("Bank Bal.", systemImage: "line.2.horizontal.decrease.circle").tag(3)
Label("Setup", systemImage: "gear").tag(4)
Label("Reports", systemImage: "gear").tag(5)
}
} detail: {
switch listSelection {
case 1: SubView(title: "Accounts")
case 2: SubView(title: "Set Aside")
case 3: SubView(title: "Bank Balance")
case 4: SubView(title: "Setup")
case 5: SubView(title: "Reports")
default: Text("Select View")
}
}
// } else {
// Text("NO DATABASE FOUND!!!!")
// .font(.system(size: 60))
// } // END OUTTER IF/ELSE DATABASE FOUND
} // END BODY VIEW
} // END STRUCT VIEW
struct SubView: View {
let title: String
var body: some View {
NavigationStack {
List(0..<6, id: \.self) { nr in
NavigationLink("\(title) Subview \(nr)", value: nr) // select
}
.navigationDestination(for: Int.self) { nr in // define the destination (Int.self refers to type of selection)
DetailView(item: nr)
}
}
.navigationBarTitle(title)
}
}
struct DetailView: View {
let item: Int
var body: some View {
Text("This is the detail view for item \(item)")
}
}

Related

SwiftUI: How to pass an argument from one view to the next with dynamically generated buttons?

Problem:
I am unable to force my alpha, beta, or gamma buttons to turn ON when an input parameter is passed from Landing.swift.
I do not understand why when onAppear fires in the stack, the output becomes:
gamma is the title
beta is the title
alpha is the title
gamma is the title
beta is the title
alpha is the title
Confused -> Why is this outputting 2x when the ForEach loop has only 3 elements inside?
Background:
I am trying to pass a parameter from one view (Landing.swift) to another (ContentView.swift) and then based on that parameter force the correct button (in ContentView) to trigger an ON state so it's selected. I have logic shown below in ButtonOnOff.swift that keeps track of what's selected and not.
For instance, there are 3 buttons in ContentView (alpha, beta, and gamma) and based on the selected input button choice from Landing, the respective alpha, beta, or gamma button (in ContentView) should turn ON.
I am dynamically generating these 3 buttons in ContentView and want the flexibility to extend to possibly 10 or more in the future. Hence why I'm using the ForEach in ContentView. I need some help please understanding if I'm incorrectly using EnvironmentObject/ObservedObject or something else.
Maintaining the ON/OFF logic works correctly with the code. That is, if you manually press alpha, it'll turn ON but the other two will turn OFF and so forth.
Thanks for your help in advance! :)
Testing.swift
import SwiftUI
#main
struct Testing: App {
#StateObject var buttonsEnvironmentObject = ButtonOnOff()
var body: some Scene {
WindowGroup {
Landing().environmentObject(buttonsEnvironmentObject)
}
}
}
Landing.swift
import SwiftUI
struct Landing: View {
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(landingChoice:tag ?? ""), tag: tag ?? "", selection: $tag) {
EmptyView()
}
Button(action: {
self.tag = "alpha"
}) {
HStack {
Text("alpha")
}
}
Button(action: {
self.tag = "beta"
}) {
HStack {
Text("beta")
}
}
Button(action: {
self.tag = "gamma"
}) {
HStack {
Text("gamma")
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
var btnName:String
#EnvironmentObject var buttonEnvObj:ButtonOnOff
init(landingChoice:String){
self.btnName = landingChoice
print("\(self.btnName) is the input string")
}
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
ForEach(0..<buttonEnvObj.buttonNames.count) { index in
BubbleButton(label: "\(buttonEnvObj.buttonNames[index])")
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(buttonEnvObj.buttonNames[index]) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var label: String
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: self.label)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(label)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(buttonBrandButtons.buttonBrand[self.label]! ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(buttonBrandButtons.buttonBrand[self.label]! ? Color.blue : .clear)
.cornerRadius(15)
.overlay(buttonBrandButtons.buttonBrand[self.label]! ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
ButtonOnOff.swift
import Foundation
class ButtonOnOff:ObservableObject{
var buttonNames = ["alpha","beta","gamma"]
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}
For a short answer just add
.onAppear(){
buttonEnvObj.changeState(buttonName: self.btnName)
}
to ContentView that will highlight the button that was selected.
As for a solution that can be expanded at will. I would suggest a single source of truth for everything and a little simplifying.
struct Landing: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
#State private var tag:String? = nil
var body: some View {
NavigationView {
ZStack{
HStack{
NavigationLink(destination: ContentView(), tag: tag ?? "", selection: $tag) {
EmptyView()
}
//Put your buttons here
HStack{
//Use the keys of the dictionary to create the buttons
ForEach(buttonEnvObj.buttonBrand.keys.sorted(by: <), id: \.self){ key in
//Have the button set the value when pressed
Button(action: {
self.tag = key
buttonEnvObj.changeState(buttonName: key)
}) {
Text(key)
}
}
}
}
.navigationBarHidden(true)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
}
struct ContentView: View {
#EnvironmentObject var buttonEnvObj:ButtonOnOff
var body: some View {
VStack{
Form{
Section{
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing:10) {
//Change this to use the dictionary
ForEach(buttonEnvObj.buttonBrand.sorted(by: {$0.key < $1.key }), id:\.key) { key, value in
BubbleButton(key: key, value: value)
.padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 0))
.onAppear {
print("\(value) is the title")
}
}
}
}.frame(height: 50)
}
}
}
}
}
struct BubbleButton: View{
#EnvironmentObject var buttonBrandButtons:ButtonOnOff
var key: String
var value: Bool
var body: some View{
HStack{
Button(action: {
print("Button action")
buttonBrandButtons.changeState(buttonName: key)
}) {
ZStack {
VStack{
HStack {
Spacer()
Text(key)
.font(.system(size: 12,weight:.regular, design: .default))
.foregroundColor(value ? Color.white : Color.gray)
Spacer()
}
}
.frame(height:30)
.fixedSize()
}
}
.background(value ? Color.blue : .clear)
.cornerRadius(15)
.overlay(value ?
RoundedRectangle(cornerRadius: 15).stroke(Color.blue,lineWidth:1) : RoundedRectangle(cornerRadius: 15).stroke(Color.gray,lineWidth:1))
.animation(.linear, value: 0.15)
}
}
}
class ButtonOnOff:ObservableObject{
//Get rid of this so you can keep the single source
//var buttonNames = ["alpha","beta","gamma"]
//When you want to add buttons just add them here it will all adjust
#Published var buttonBrand:[String:Bool] = [
"alpha":false,
"beta":false,
"gamma":false
]
func changeState(buttonName:String) -> Void {
for (key,_) in buttonBrand{
if key == buttonName && buttonBrand[buttonName] == true{
buttonBrand[buttonName] = false
} else{
buttonBrand[key] = (key == buttonName) ? true : false
}
}
print(buttonBrand)
}
}

ScrollViewReader not scrolling programmatically

I have the following view and I'm attempting to have it scroll to the bottom on button click as elements are added to the list. I've searched and found that ScrollViewReader is the option to use however my implementation doesn't appear to be working.
My attempts at fixing have included explicitly setting the id of the cell on both the inner views as well as the outer HStack{} I even attempted to set the id to a reference of itself, kind of knowing that's a bad idea, but for brevity. I also removed any extra views inside of the list such as HStack{}, Spacer(), etc.. and just left my ColorsChosenView().id(i) thinking that extra views might cause it, but I digress the issue still persists.
var body: some View {
VStack {
ScrollViewReader { reader in
List {
ForEach(0..<vm.guesses.count, id: \.self) { i in
HStack{
Spacer()
ColorsChosenView(locationCorrect: 1,
locationIncorrect: 3,
color1: vm.guesses[i][0],
color2: vm.guesses[i][1],
color3: vm.guesses[i][2],
color4: vm.guesses[i][3])
Spacer()
}.id(i)
}
}.listStyle(InsetListStyle())
Divider()
.frame(maxWidth: 250)
ColorChoicePicker(vm: vm)
Divider()
.frame(maxWidth: 250)
HStack {
Spacer()
FABButton(text: "SUBMIT")
.onTapGesture {
vm.submit()
reader.scrollTo(vm.guesses.count - 1)
}
}.padding()
}
}
.navigationBarHidden(true)
.navigationBarHidden(true)
.onAppear(perform: {
vm.resetGame()
})
}
To simplify things, I found that this works just fine. Yet my implementation doesn't feel much different.
var body: some View {
ScrollViewReader { proxy in
VStack {
Button("Jump to #50") {
proxy.scrollTo(50)
}
List(0..<100, id: \.self) { i in
Text("Example \(i)")
.id(i)
}
}
}
}
Since you're modifying the array, this should work:
1: call the function in the main thread (DispatchQueue.main.async)
-> this will "kinda" work, it will scroll but not to the current but the previous last item
2: (Workaround) handle scrolling in a change-handler (you could also remove the shouldScroll variable if all changes should make it scroll to the bottom)
class NumbersContainer: ObservableObject {
#Published var numbers: [Int] = Array(0..<25)
func submit() {
self.numbers.append(self.numbers.count)
}
}
struct ContentView: View {
#StateObject var nc = NumbersContainer()
#State var shouldScroll: Bool = false
var body: some View {
VStack {
ScrollViewReader { reader in
Button("Submit", action: {
DispatchQueue.main.async {
nc.submit()
}
self.shouldScroll = true
})
List {
ForEach(0..<nc.numbers.count, id: \.self) { i in
HStack {
Spacer()
Text("Row \(i)")
Spacer()
}.id(i)
}
}
.onChange(of: nc.numbers) { newValue in
if shouldScroll {
reader.scrollTo(newValue.count - 1)
shouldScroll = false
}
}
}
}
}
}
Another Possibility would be to use the ScrollReaderProxy as a parameter of the submit function:
class NumbersContainer: ObservableObject {
#Published var numbers: [Int] = Array(0..<25)
func submit(reader: ScrollViewProxy) {
let dispatchGroup = DispatchGroup()
dispatchGroup.enter() // All leaves must have an enter
DispatchQueue.main.async {
self.numbers.append(self.numbers.count)
dispatchGroup.leave() // Notifies the DispatchGroup
}
dispatchGroup.notify(queue: .main) {
reader.scrollTo(self.numbers.count - 1)
}
}
}
struct ContentView: View {
#StateObject var nc = NumbersContainer()
var body: some View {
VStack {
ScrollViewReader { reader in
Button("Submit", action: {
nc.submit(reader: reader)
})
List {
ForEach(0..<nc.numbers.count, id: \.self) { i in
HStack {
Spacer()
Text("Row \(i)")
Spacer()
}.id(i)
}
}
}
}
}
}

SwiftUI do not push navigation with keyboard with Xcode 12

I made a simple app to illustrate the issue. I have a custom Tab view, however when I click on text field and open the keyboard and my navigation tabs is being pushed up as well. I would like for navigation to be not visible (as one would expect for any app).
This image illustrates the problem:
This issue does not happen if I use native TabView, however I would like to use custom tabs. Does anyone figured out how to deal with it? Providing the code below.
struct ContentView: View {
#State var menu = 0
var body: some View {
VStack {
VStack {
if menu == 0 {
FirstPage()
} else if menu == 1 {
Text("all is good")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
CustomTabs(menu: $menu)
}
}
}
struct CustomTabs: View {
#Binding var menu: Int
var body: some View {
HStack {
Spacer()
Text("Nav1")
.onTapGesture{
self.menu = 0
}
Spacer()
Text("Nav2")
.onTapGesture{
self.menu = 1
}
Spacer()
}
.padding()
.background(Color(.red))
}
}
struct FirstPage: View {
#State var mood: String = ""
var body: some View {
VStack {
Text("How are you?")
TextField("answer textfield", text: $mood)
}
}
}
A possible solution/work-around would be to embed the CustomTabs in a VStack with a Spacer which pushes the tabs down even when the keyboard appears and then put this Stack in front of your view via ZStack.
struct ContentView: View {
#State var menu = 0
var body: some View {
ZStack {
VStack {
if menu == 0 {
FirstPage()
} else if menu == 1 {
Text("all is good")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
VStack {
Spacer(minLength: UIScreen.main.bounds.height - 70)
CustomTabs(menu: $menu)
.frame(height: 70)
}
}
}
}
For simplicity I just decided to set the tabs height to 70. A more beautiful solution would be to use GeometryReader.

How to make SwiftUI to forget internal state

I have a view like following
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
// Common Elements
content
// More Common Elements
}
}
}
}
When I call this from another view like
A(nextInnerView())
two things happen. Firstly, as the size of the content element changes ScrollView animates the transition. Secondly, if you scroll down and then change the content the scrolling position does not reset.
Here is a demo of possible solution. Tested with Xcode 11.4 / iOS 13.4
The origin of this behaviour is in SwiftUI rendering optimisation, that tries to re-render only changed part, so approach is to identify view A (to mark it as completely changed) based on condition that originated in interview changes, alternatively it can be identified just by UUID().
struct TestInnerViewReplacement: View {
#State private var counter = 0
var body: some View {
VStack {
Button("Next") { self.counter += 1 }
Divider()
A(content: nextInnerView())
.id(counter) // << here !!
}
}
private func nextInnerView() -> AnyView {
AnyView(Group {
if counter % 2 == 0 {
Text("Text Demo")
} else {
Image(systemName: "star")
.resizable()
.frame(width: 100, height: 100)
}
})
}
}
struct A: View {
var content: AnyView
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack {
ForEach(0..<5) { _ in // upper content demo
Rectangle().fill(Color.yellow)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
content
ForEach(0..<10) { _ in // lower content demo
Rectangle().fill(Color.blue)
.frame(height: 40)
.frame(maxWidth: .infinity)
.padding()
}
}
}
}
}

Is it possible to have multiple NavigationLinks per row in a List SwiftUI?

I can't use multiple NavigationLinks in the same row of a List.
It looks like the navigation stack is totally messed up, because you tap once and it goes to multiple views and back erratically...
in TestList, I've tried adding the separate NavigationLinks in Sections, and I've tried moving the NavigationLinks two different places in the view hierarchy...
I've tried adding two NavigationViews for each row of the list, but
then the navigationTitleBar don't go away when I need it to..
struct ContentView: View {
var body: some View {
NavigationView {
TestList()
}
}
}
struct TestList: View {
var body: some View {
List {
ListCellView()
}
}
}
struct ListCellView: View {
var body: some View {
VStack {
Spacer()
NavigationLink(destination: TestDestination1()) {
Text("Test Destination 1")
.frame(width: 140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.green, lineWidth: 3.0))
}
Spacer()
NavigationLink(destination: TestDestination2()) {
Text("Test Destination 2")
.frame(width:140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.purple, lineWidth: 3.0))
Spacer()
}
}
}
}
struct TestDestination1: View {
var body: some View {
Text("Test Destination 1")
}
}
struct TestDestination2: View {
var body: some View {
Text("Test Destination 2")
}
}
I expect that when you tap a NavigationLink, it will navigate to the destination view.
What happens is when two NavigationLinks are in the same row of a List and you tap in it, it will:
1. go to one of the views
2. After tapping 'back', it will take you back to the view AND THEN take you to the other destination view.
https://youtu.be/NCTnqjzJ4VE
As the others mentioned, why 2 NavigationLinks in 1 cell. The issue is with multiple buttons and gesture in general for the Cell. I guess it is expected 1 Button/NavigationLink max per cell. As you noticed, on your video, you tap on a NavigationLink but your full Cell got the gesture (highlighted), which in return impact the other Buttons/NavigationLinks.
Anyhow, you can have it working, the 2 NavigationLinks in 1 cell, with a hack. Below I have created SGNavigationLink, which I use for my own app which solve your issue. It simply replace NavigationLink and based on TapGesture, so you will lose the highlight.
NB: I slightly modified your ListCellView, as the Spacer within my SGNavigationLink was creating an internal crash.
struct ListCellView: View {
var body: some View {
VStack {
HStack{
SGNavigationLink(destination: TestDestination1()) {
Text("Test Destination 1")
.frame(width: 140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.green, lineWidth: 3.0))
}
Spacer()
}
HStack{
SGNavigationLink(destination: TestDestination2()) {
Text("Test Destination 2")
.frame(width:140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.purple, lineWidth: 3.0))
}
Spacer()
}
}
}
}
struct SGNavigationLink<Content, Destination>: View where Destination: View, Content: View {
let destination:Destination?
let content: () -> Content
#State private var isLinkActive:Bool = false
init(destination: Destination, title: String = "", #ViewBuilder content: #escaping () -> Content) {
self.content = content
self.destination = destination
}
var body: some View {
return ZStack (alignment: .leading){
if self.isLinkActive{
NavigationLink(destination: destination, isActive: $isLinkActive){Color.clear}.frame(height:0)
}
content()
}
.onTapGesture {
self.pushHiddenNavLink()
}
}
func pushHiddenNavLink(){
self.isLinkActive = true
}
}
I am not sure about why do you need multiple Navigationlinks (duplicate code). You can use a data source that will hold the required properties of the list [title, color, id etc] and based on the id, call the desired View. Reuse the same code. Here is an example.
struct TestList: View {
var body: some View {
List { // <- Use Data source
ForEach(0..<2) { index in
ListCellView(index: index)
}
}
}
}
struct ListCellView: View {
var index: Int
var body: some View {
return NavigationLink(destination: ViewFactory.create(index)) {
Text("Test Destination 1")
.frame(width: 140, height: 50)
.background(RoundedRectangle(cornerRadius: 7.0).strokeBorder(Color.green, lineWidth: 3.0))
}
}
}
class ViewFactory {
static func create(_ index: Int) -> AnyView {
switch index {
case 0:
return AnyView(TestDestination1())
case 1:
return AnyView(TestDestination2())
default:
return AnyView(EmptyView())
}
}
}
struct TestDestination1: View {
var body: some View {
Text("Test Destination 1")
}
}
struct TestDestination2: View {
var body: some View {
Text("Test Destination 2")
}
}