I created a NavigationView and it works, but everytime I click on a NavigationLink, SwiftUI recreates the destination view and resets every property in it. See this example:
struct NavView: View {
#State var selectionIndex: Int? = nil
let views = [
DestinationView(viewNumber: 1),
DestinationView(viewNumber: 2),
DestinationView(viewNumber: 3)
]
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: self.views[0], tag: 0, selection: $selectionIndex) {
Text("View 1")
}
NavigationLink(destination: self.views[1], tag: 1, selection: $selectionIndex) {
Text("View 2")
}
NavigationLink(destination: self.views[2], tag: 0, selection: $selectionIndex) {
Text("View 3")
}
}
self.views[0]
}
}
}
struct DestinationView: View {
#State var viewNumber: Int
#State var count = 0
var body: some View {
VStack {
Text("View \(self.viewNumber)")
.font(.largeTitle)
Text("Count: \(self.count)")
Button(action: {
self.count += 1
}) {
Text("Increase counter")
}
}
}
}
In this example clicking for example on the View1 link opens the View 1. Pressing the button increases the count property. If you then click on the View2 button and then back the View1 button, the count property will be zero again.
EDIT
I also tried not to store the views and create them directly in body, as suggested in the comments. Doesn't work either :(
struct NavView: View {
#State var selectionIndex: Int? = nil
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DestinationView(viewNumber: 1), tag: 0, selection: $selectionIndex) {
Text("View 1")
}
NavigationLink(destination: DestinationView(viewNumber: 2), tag: 1, selection: $selectionIndex) {
Text("View 2")
}
NavigationLink(destination: DestinationView(viewNumber: 3), tag: 2, selection: $selectionIndex) {
Text("View 3")
}
}
}
}
}
EDIT 2
I also tried to store the views globally, I also tried with and without tag: and selection:
Issue in your code is that every time you are by default initializing count to 0 whenever we are navigating to DestinationView so trick is we have to update count value and save it some where in NavView.
First of all you have to create DestinationViewModel to keep track of your updated count values like below
class DestinationViewModel: ObservableObject {
#Published var viewNumber: Int
#Published var count: Int
init(_ viewNumber: Int, count: Int) {
self.viewNumber = viewNumber
self.count = count
}
}
Then you can access your DestinationViewModel in your DestinationView
struct DestinationView: View {
#EnvironmentObject var viewModel: DestinationViewModel
var body: some View {
VStack {
Text("View \(viewModel.viewNumber)")
.font(.largeTitle)
Text("Count: \(viewModel.count)")
Button(action: {
self.viewModel.count += 1
}) {
Text("Increase counter")
}
}
}
}
Below is your updated NavView
class DestinationViewModelContainer: ObservableObject {
#Published var viewModels: [DestinationViewModel]
init(_ viewModels: [DestinationViewModel]) {
self.viewModels = viewModels
}
}
struct NavView: View {
#EnvironmentObject var viewModelContainer: DestinationViewModelContainer
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: DestinationView().environmentObject(viewModelContainer.viewModels[0])) {
Text("View 1")
}
NavigationLink(destination: DestinationView().environmentObject(viewModelContainer.viewModels[1])) {
Text("View 2")
}
NavigationLink(destination: DestinationView().environmentObject(viewModelContainer.viewModels[2])) {
Text("View 3")
}
}
}
}
}
and Here you can call your NavView from below code
let contentView = NavView().environmentObject(DestinationViewModelContainer([DestinationViewModel(1, count: 0), DestinationViewModel(2, count: 0),DestinationViewModel(3, count: 0)]))
Hope it will help you.
Related
In the app, if I follow the navigations from MainView->Tab1->Link1->Sub Link1, put the app to background, then bring it back, then it shows back to Tab1 again, does anyone know why NavigationView cannot keep the last view? it works fine in iOS 14.7
struct LocalNotificationDemoView: View {
#StateObject var localNotification = LocalNotification()
#ObservedObject var notificationCenter: NotificationCenter
var body: some View {
NavigationView {
VStack {
MainView()
}
}
.navigationViewStyle(.stack)
}
}
struct MainView: View {
var body: some View {
TabView {
Tab1()
.tabItem {
Text("Tab1")
}
Tab2()
.tabItem {
Text("Tab2")
}
}
}
}
struct Tab1: View {
#State var selection: Int? = nil
var body: some View {
NavigationLink(destination: View1(), tag: 1, selection: $selection) {
Text("Link 1")
}
}
}
struct Tab2: View {
#State var selection: Int? = nil
var body: some View {
NavigationLink(destination: Text("Link 2").navigationTitle("").navigationBarHidden(true), tag: 1, selection: $selection) {
Text("Link 2")
.onAppear {
print("Link2 shows")
Thread.callStackSymbols.forEach{print($0)}
}
}
}
}
struct View1: View {
#State var selection: Int? = nil
var body: some View {
NavigationLink(destination: Text("Sub Link 1"), tag: 1, selection: $selection) {
Text("Sub Link 1")
}
}
}
For the purpose of this question, I have provided minimum sample code to re-create the bug. Just copy/paste it to run.
import SwiftUI
final class Popper: ObservableObject {
#Published var shouldProceed: String? = nil
var id: String
init(id: String) { self.id = id }
}
struct ContentView: View {
#StateObject var popper = Popper(id: "ROOT")
var body: some View {
NavigationView {
VStack(spacing: 40) {
Text("You are now in ROOT").font(.largeTitle)
Text("Tap Here to goto V1").onTapGesture {
popper.shouldProceed = "y"
}.foregroundColor(.blue)
NavigationLink(destination: V1().environmentObject(popper),
tag: "y",
selection: $popper.shouldProceed) {
EmptyView()
}
}
}
}
}
struct V1: View {
#EnvironmentObject var rootPopper: Popper
#StateObject var v1Popper = Popper(id: "V1")
var body: some View {
VStack(spacing: 40) {
Text("You are now in V1").font(.largeTitle)
Text("Tap Here to pop to root").onTapGesture {
print("popping to: \(rootPopper.id)")
rootPopper.shouldProceed = nil
}.foregroundColor(.red)
Text("Or tap here to go to V2").onTapGesture {
v1Popper.shouldProceed = "y"
}.foregroundColor(.blue)
NavigationLink(destination: V2().environmentObject(v1Popper),
tag: "y",
selection: $v1Popper.shouldProceed) {
EmptyView()
}
}
}
}
struct V2: View {
#EnvironmentObject var v1Popper: Popper
#State var shouldProceed: String? = nil
var body: some View {
VStack(spacing: 40) {
Text("You are now in V2").font(.largeTitle)
Text("Tap Here to pop to V1").onTapGesture {
print("popping to: \(v1Popper.id)")
v1Popper.shouldProceed = nil
}.foregroundColor(.red)
Text("Or Tap here to gotoV3").onTapGesture {
shouldProceed = "y"
}.foregroundColor(.blue)
NavigationLink(destination: V3().environmentObject(v1Popper),
tag: "y", selection: $shouldProceed) {
EmptyView()
}
}
}
}
struct V3: View {
#EnvironmentObject var v1Popper: Popper
var body: some View {
VStack(spacing: 40) {
Text("You are now in V3").font(.largeTitle)
Text("Tap Here to pop to V1").onTapGesture {
print("popping to: \(v1Popper.id)")
v1Popper.shouldProceed = nil
}.foregroundColor(.red)
}
}
}
Question When you get to screen V3, why does it not pop to the V1 screen? All the other pop functions work. It just fails for some reason when it gets to screen V3.
Please help.
Try to use .isDetailLink(false) for your links, like
NavigationLink(destination: V1().environmentObject(popper),
tag: "y",
selection: $popper.shouldProceed) {
EmptyView()
}.isDetailLink(false) // << here !!
For the purpose of this question, I have provided minimum sample code to re-create the bug. Just copy/paste it to run.
import SwiftUI
final class Popper: ObservableObject {
#Published var shouldProceed: String? = nil
var id: String
init(id: String) { self.id = id }
}
struct ContentView: View {
#StateObject var popper = Popper(id: "ROOT")
var body: some View {
NavigationView {
VStack(spacing: 40) {
Text("You are now in ROOT").font(.largeTitle)
Text("Tap Here to goto V1").onTapGesture {
popper.shouldProceed = "y"
}.foregroundColor(.blue)
NavigationLink(destination: V1().environmentObject(popper),
tag: "y",
selection: $popper.shouldProceed) {
EmptyView()
}
}
}
}
}
struct V1: View {
#EnvironmentObject var rootPopper: Popper
#StateObject var v1Popper = Popper(id: "V1")
var body: some View {
VStack(spacing: 40) {
Text("You are now in V1").font(.largeTitle)
Text("Tap Here to pop to root").onTapGesture {
print("popping to: \(rootPopper.id)")
rootPopper.shouldProceed = nil
}.foregroundColor(.red)
Text("Or tap here to go to V2").onTapGesture {
v1Popper.shouldProceed = "y"
}.foregroundColor(.blue)
NavigationLink(destination: V2().environmentObject(v1Popper),
tag: "y",
selection: $v1Popper.shouldProceed) {
EmptyView()
}
}
}
}
struct V2: View {
#EnvironmentObject var v1Popper: Popper
#State var shouldProceed: String? = nil
var body: some View {
VStack(spacing: 40) {
Text("You are now in V2").font(.largeTitle)
Text("Tap Here to pop to V1").onTapGesture {
print("popping to: \(v1Popper.id)")
v1Popper.shouldProceed = nil
}.foregroundColor(.red)
Text("Or Tap here to gotoV3").onTapGesture {
shouldProceed = "y"
}.foregroundColor(.blue)
NavigationLink(destination: V3().environmentObject(v1Popper),
tag: "y", selection: $shouldProceed) {
EmptyView()
}
}
}
}
struct V3: View {
#EnvironmentObject var v1Popper: Popper
var body: some View {
VStack(spacing: 40) {
Text("You are now in V3").font(.largeTitle)
Text("Tap Here to pop to V1").onTapGesture {
print("popping to: \(v1Popper.id)")
v1Popper.shouldProceed = nil
}.foregroundColor(.red)
}
}
}
Question When you get to screen V3, why does it not pop to the V1 screen? All the other pop functions work. It just fails for some reason when it gets to screen V3.
Please help.
Try to use .isDetailLink(false) for your links, like
NavigationLink(destination: V1().environmentObject(popper),
tag: "y",
selection: $popper.shouldProceed) {
EmptyView()
}.isDetailLink(false) // << here !!
I create a List to show my player information. but my List column didn't fit my cell view. The textfield that disappear in some column. This is strange because most of they has "-1" number. Is somebody know any reason may cause it happen? Demo video: https://youtu.be/vUo9zZZ5olo
struct SelectPlayerCellView: View {
#ObservedObject var player: PlayerGameData
#State var newuniformNumber: Int = 0
var body: some View {
HStack{
Button(action: {
self.player.isPlayer.toggle()
}){
if self.player.isPlayer{
Image(systemName: "checkmark.rectangle")
}
else{
Image(systemName: "rectangle")
}
}
TextField("\(player.uniformNumber)", value: $newuniformNumber, formatter: NumberFormatter())
.font(.system(size: 40))
.frame(width:55)
.padding(5)
Text(player.name)
}
.padding(.horizontal,8)
.cornerRadius(20)
}
}
I put my cell view in the list
struct SelectPlayerView: View {
#EnvironmentObject var teamResult : TeamResult
#ObservedObject var allPlayerList : PlayersGameData = PlayersGameData()
#State var goToNextPage : Bool = false
var selectedPlayerList : PlayersGameData = PlayersGameData()
var body: some View {
VStack {
List{
ForEach(allPlayerList.playersGameDataArray, id: \.userId) { (player) in
SelectPlayerCellView(player: player)
}.onMove(perform: move)
}
NavigationLink(destination: SelectStartingLineupView(selectedPlayerList: self.selectedPlayerList).environmentObject(teamResult),isActive: $goToNextPage){
EmptyView()
}
VStack (spacing: 10){
Button(action: {
self.createPlayersList()
self.goToNextPage.toggle()
}){
Image(systemName: "arrowshape.turn.up.right.circle")
}
}
}.onAppear(perform: getTeamMemberResults)
.onDisappear(perform: clearTeamMemberResults)
.navigationBarItems(trailing: EditButton())
}
Why I am putting TabView into a NavigationView is because I need to hide the bottom tab bar when user goes into 2nd level 'detail' views which have their own bottom action bar.
But doing this leads to another issue: all the 1st level 'list' views hosted by TabView no longer display their titles. Below is a sample code:
import SwiftUI
enum Gender: String {
case female, male
}
let members: [Gender: [String]] = [
Gender.female: ["Emma", "Olivia", "Ava"], Gender.male: ["Liam", "Noah", "William"]
]
struct TabItem: View {
let image: String
let label: String
var body: some View {
VStack {
Image(systemName: image).imageScale(.large)
Text(label)
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
TabView {
ListView(gender: .female).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
ListView(gender: .male).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
}
}
}
struct ListView: View {
let gender: Gender
var body: some View {
let names = members[gender]!
return List {
ForEach(0..<names.count, id: \.self) { index in
NavigationLink(destination: DetailView(name: names[index])) {
Text(names[index])
}
}
}.navigationBarTitle(Text(gender.rawValue), displayMode: .inline)
}
}
struct DetailView: View {
let name: String
var body: some View {
ZStack {
VStack {
Text("profile views")
}
VStack {
Spacer()
HStack {
Spacer()
TabItem(image: "pencil.circle", label: "Edit")
Spacer()
TabItem(image: "minus.circle", label: "Delete")
Spacer()
}
}
}
.navigationBarTitle(Text(name), displayMode: .inline)
}
}
What I could do is to have a #State var title in the root view and pass the binding to all the list views, then have those list views to set their title back to root view on appear. But I just don't feel so right about it, is there any better way of doing this? Thanks for any help.
The idea is to join TabView selection with NavigationView content dynamically.
Demo:
Here is simplified code depicting approach (with using your views). The NavigationView and TabView just position independently in ZStack, but content of NavigationView depends on the selection of TabView (which content is just stub), thus they don't bother each other. Also in such case it becomes possible to hide/unhide TabView depending on some condition - in this case, for simplicity, presence of root list view.
struct TestTabsOverNavigation: View {
#State private var tabVisible = true
#State private var selectedTab: Int = 0
var body: some View {
ZStack(alignment: .bottom) {
contentView
tabBar
}
}
var contentView: some View {
NavigationView {
ListView(gender: selectedTab == 0 ? .female : .male)
.onAppear {
withAnimation {
self.tabVisible = true
}
}
.onDisappear {
withAnimation {
self.tabVisible = false
}
}
}
}
var tabBar: some View {
TabView(selection: $selectedTab) {
Rectangle().fill(Color.clear).tag(0).tabItem {
TabItem(image: "person.crop.circle", label: Gender.female.rawValue)
}
Rectangle().fill(Color.clear).tag(1).tabItem {
TabItem(image: "person.crop.circle.fill", label: Gender.male.rawValue)
}
}
.frame(height: 50) // << !! might be platform dependent
.opacity(tabVisible ? 1.0 : 0.0)
}
}
This maybe a late answer, but the TabView items need to be assigned tag number else binding selection parameter won't happen. Here is how I do the same thing on my project:
#State private var selectedTab:Int = 0
private var pageTitles = ["Home", "Customers","Sales", "More"]
var body: some View {
NavigationView{
TabView(selection: $selectedTab, content:{
HomeView()
.tabItem {
Image(systemName: "house.fill")
Text(pageTitles[0])
}.tag(0)
CustomerListView()
.tabItem {
Image(systemName: "rectangle.stack.person.crop.fill")
Text(pageTitles[1])
}.tag(1)
SaleView()
.tabItem {
Image(systemName: "tag.fill")
Text(pageTitles[2])
}.tag(2)
MoreView()
.tabItem {
Image(systemName: "ellipsis.circle.fill")
Text(pageTitles[3])
}.tag(3)
})
.navigationBarTitle(Text(pageTitles[selectedTab]),displayMode:.inline)
.font(.headline)
}
}