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 { |
| |
| |
| 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() |
| //... |
| } |
| } |