Search code examples
iosswiftuiswiftui-tabviewswiftui-navigationstackswiftui-navigationpath

SwiftUI: How to correctly navigate in TabView and NavigationStack using NavigationPath?


I'm experiencing a problem using programmatic navigation using TabView and NavigationStack. I have a TabView with a $selection pointing to a NavigationPath in an environmentObject. I also have two views to tab to. When clicking on a navigationLink item, a DataDetailView opens. Then switching to the second tab, navigationPath should be deleted. When programmatically navigation from tab 2 to tab 1 and changing the navigationPath, an empty view is displayed instead of my DataDetailView(someItem):

Expected behavior: SomeView1 -> (Clicking on item) -> DataDetailView -> (changing tab) -> SomeView2 -> (clicking button) -> DataDetailView(someItem)

Experienced behavior: SomeView1 -> (Clicking on item) -> DataDetailView -> (changing tab) -> SomeView2 -> (clicking button) -> empty view

Can someone explain this behavior? I haven't been successful googling the problem. :/ I could probably do this with a binding of NavigationPath, but in my full app, an environmentObject would be beneficial.

Thanks in advance!


import SwiftUI

struct MyData: Identifiable, Hashable {
    let id = UUID().uuidString
    let name: String
}

class AppManager: ObservableObject {
    @Published var tabSelection: Int = 1
    @Published var navigationPath = NavigationPath()
}

class DataManager: ObservableObject {
    static let instance = DataManager()
    
    @Published var dataArray = [MyData(name: "Data1"), MyData(name: "Data2"), MyData(name: "Data3"), MyData(name: "Data4")]
}

struct NavigationTest: View {
    
    @EnvironmentObject var appManager: AppManager
    
    var body: some View {
        TabView(selection: $appManager.tabSelection) {
            SomeView1()
                .tabItem { Text("View 1") }
                .tag(1)
            SomeView2()
                .tabItem { Text("View 2") }
                .tag(2)
        }
    }
}

struct SomeView1: View {
    
    @EnvironmentObject var appManager: AppManager
    @EnvironmentObject var testData: DataManager
    
    var body: some View {
        NavigationStack(path: $appManager.navigationPath) {
            List {
                ForEach(testData.dataArray) { data in
                    NavigationLink("\(data.name)", value: data)
                }
            }
            .navigationDestination(for: MyData.self) { data in
                DataDetailView(data: data)
            }
        }
        .onDisappear {
            if !appManager.navigationPath.isEmpty {
                appManager.navigationPath.removeLast()
            }
        }
    }
}

struct DataDetailView: View {
    let data: MyData
        
    var body: some View {
        Text(data.name).background(.yellow)
    }
}

struct SomeView2: View {
    
    @EnvironmentObject var appManager: AppManager
            
    var body: some View {
        Button("Segue to someItem") {
            let someItem = DataManager.instance.dataArray[2]
            appManager.navigationPath.append(someItem)
            appManager.tabSelection = 1
        }
    }
}

#Preview {
    @StateObject var appManager = AppManager()
    @StateObject var dataManager = DataManager.instance
    
    return NavigationTest()
        .environmentObject(appManager)
        .environmentObject(dataManager)
}


Solution

  • You can fix it by adding a little delay.

    First Change the Tab, then after a fraction of a second change the path.

    Here is the code for SomeView2:

    struct SomeView2: View {
        
        @EnvironmentObject var appManager: AppManager
                
        var body: some View {
            Button("Segue to someItem") {
                let someItem = DataManager.instance.dataArray[2]
                
                // First change the Tab.
                appManager.tabSelection = 1
                
                // Then change the path.
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                    appManager.navigationPath.append(someItem)
                }
            }
        }
    }