Hi! Working with SwiftUI that same issue happened to us when having lists (using ForEach) with the same id. Imagine that you have a ForEach to present a list of people, if you use as id the name of the person, can happen that several people have John as id.
Be careful if you are mocking those items, that was what happened to us. We were mocking Models with the same id.
Hope it helps!
Post
Replies
Boosts
Views
Activity
Hope this helps. Working with NavigationViews and NavigationLinks, we've found that having State on a parent that can be mutated by several levels of children views, which have conditional NavigationLinks create autopop and autopush issues. This seems to be releated on how SwiftUI tries to uniquely identify those NavigationLink components.
Here's an example:
The app contains a shared State
struct TestNavigationApp: App {
@StateObject var viewModel = DummyViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
}
}
}
The ContentView has a NavigationLink which leads to a loop of navigations
struct ContentView: View {
@EnvironmentObject var viewModel: DummyViewModel
@State var autopopActive = false
var body: some View {
NavigationView {
VStack {
NavigationLink(destination: AutopopLevel, isActive: $autopopActive) {
Button {
viewModel.level = 1
autopopActive = true
} label: {
Text("Autopop level")
}
.padding()
}
}
.navigationTitle("Main")
}
.navigationViewStyle(StackNavigationViewStyle())
}
var AutopopLevel: some View {
AutopopLevelView()
.environmentObject(viewModel)
}
}
struct AutopopLevelView: View {
@EnvironmentObject var viewModel: DummyViewModel
@State var isPresented = false
@State var isActive = false
var body: some View {
if viewModel.level == 3 {
Button {
isPresented = true
} label: {
Text("Fullscreen cover")
}
.fullScreenCover(isPresented: $isPresented) {
VStack {
HStack {
Button {
isPresented = false
} label: {
Text("Close")
}
.padding()
Spacer()
}
Spacer()
Text("Fullscreen")
Spacer()
}
}
} else {
NavigationLink(destination: Level, isActive: $isActive) {
Button {
viewModel.level += 1
isActive = true
} label: {
Text(viewModel.level == 3 ? "Fullscreen cover" : "Level \(viewModel.level)")
}
}
}
}
var Level: some View {
AutopopLevelView()
.environmentObject(viewModel)
}
}
As you can see, AutopopLevelView has a NavigationLink conditional to the shared viewModel, which leads to an autopop.
To fix that, we did the following:
struct CorrectLevelView: View {
@EnvironmentObject var viewModel: DummyViewModel
@State var isPresented = false
@State var isActive = false
var body: some View {
NavigationLink(destination: Level, isActive: $isActive) {
Button {
if viewModel.level == 3 {
isPresented = true
} else {
viewModel.level += 1
isActive = true
}
} label: {
Text(viewModel.level == 3 ? "Fullscreen cover" : "Level \(viewModel.level)")
}
}
.navigationTitle("Level \(viewModel.level)")
.fullScreenCover(isPresented: $isPresented) {
VStack {
HStack {
Button {
isPresented = false
} label: {
Text("Close")
}
.padding()
Spacer()
}
Spacer()
Text("Fullscreen")
Spacer()
}
}
}
var Level: some View {
CorrectLevelView()
.environmentObject(viewModel)
}
}
Must say that this is only one of the issues that we found. To fix how to work with NavigationLinks, I would recommend to:
Avoid having NavigationLinks inside conditions
Not using NavigationLinks as buttons, and using isActive to specifically have more control over it
Apple is showcasing simple projects when you usually don't have more than 1 level deepness on NavigationLinks, so they can use the NavigationLink(destination: <>, label: <>) initializer, but we've found that is not a good idea.
Avoid using NavigationLink(destination: <>, tag: <>, selection: <>, label: <>) if possible, we got some weird behaviours with it.
Not using more than 1 NavigationLink per body view and just setting the destination conditionally, that way you have full control over it and you can decide which View you want as destination. Example: if you have a list of 6 map views, create 6 Buttons inside the List / VStack / whatever list UI component and just put 1 NavigationLink at the body level, which will have a different destination View based on some conditions, example:
var body: some View {
VStack {
ForEach(maps) { map in
Button {
// Here you want to change the conditional binding
// and the condition that choses the view
modifyConditionals()
} label: {
Text(map.name)
}
.padding()
}
}
.navigationTitle("Maps")
NavigationLink(destination: destinationMapView, isActive: $someConditionalBinding) {
EmptyView()
}
}
@ViewBuilder
var destinationMapView: some View {
switch someCondition {
case .someCase:
return MapView()
//...
}
}