Hope this helps. Working with NavigationView
s and NavigationLink
s, we've found that having State on a parent that can be mutated by several levels of children views, which have conditional NavigationLink
s 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 NavigationLink
s, I would recommend to:
- Avoid having
NavigationLink
s inside conditions - Not using
NavigationLink
s 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
NavigationLink
s, 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 Button
s 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()
//...
}
}