NavigationStack programatically appending childviews with different types

321 Views Asked by At

I have a NavigationStack with parent view

struct MainDatabaseView: View {
@State private var path = NavigationPath()

var body: some View {
    NavigationStack(path: $path) {
        List {
            Section {
                NavigationLink(destination: MainTabPilotDatabaseView(path: $path)) {
                    Label("Pilots", systemImage: "person.2")
                        .foregroundStyle(.primary)
                }

                NavigationLink(destination: MainTabAircraftDatabaseView()) {
                    Label("Aircraft", systemImage: "airplane.circle")
                        .foregroundStyle(.primary)
                }
                
            } header: {
                Text("Database")
            }
        }
    }
}

And a child view

struct MainTabPilotDatabaseView: View {
    @EnvironmentObject var pilotVM: PilotVM

    @Binding var path : NavigationPath

    var body: some View {
        List {
            ForEach(pilotVM.filteredPilots, id: \.id) { pilot in
                NavigationLink {
                    PilotDatabaseDetailView(pilot: pilot)
                } label: {
                    PilotCell(pilot: pilot, isInDetailView: false)
                        .contentShape(Rectangle())
                        .swipeActions(edge: .trailing) { delete(for: pilot) }
                        .swipeActions(edge: .leading) { markFavorite(for: pilot) }
                }
            }
        }
    
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) { addPilot() }
        }
    }

    func addPilot() -> some View {
        Button {
            let pilotModelInput = PilotModelInput()
            let new = pilotVM.addPilotToModel(input: pilotModelInput)
            path.append(new)
        } label: {
            Image(systemName: "plus.circle.fill")
        }
      
    }
}

When I have path as @State var path: [PilotEntity] = [] the code works as expected - i.e. when plus.circle.fill button in child view is tapped, it pushes a sub-child view in the NavigationStack. However since I will also be using a different entity (AircraftEntity) for the other view in the NavigationStack, I presume that I have to go with generic NavigationPath.

I have the code mock as above and cannot seem to pass the data to the sub-child view. What is the best way of going about it?

EDIT:

I have been playing with the solution from @son and found that this also works:

import SwiftUI

enum DatabaseTabOption {
    case pilot, aircraft
}

class NavigationStackManager: ObservableObject {
    @Published var path = NavigationPath()
}

struct MainDatabaseView: View {
    @StateObject var pathManager = NavigationStackManager()

    var body: some View {
        NavigationStack(path: $pathManager.path) {
            List {
                Section {
                    NavigationLink(value: DatabaseTabOption.pilot) {
                        Button {
                            pathManager.path.append(DatabaseTabOption.pilot)
                        } label: {
                            Label("Pilots", systemImage: "person.2")
                        }
                    }

                    NavigationLink(value: DatabaseTabOption.aircraft) {
                        Button {
                            pathManager.path.append(DatabaseTabOption.aircraft)
                        } label: {
                            Label("Aircraft", systemImage: "airplane.circle")
                        }
                    }
                } header: {
                    Text("Database")
                }
            }
            .navigationDestination(for: DatabaseTabOption.self, destination: { value in
                switch value {
                case .pilot:
                    MainTabPilotDatabaseView()
                        .environmentObject(pathManager)
                case .aircraft:
                    MainTabAircraftDatabaseView()
                        .environmentObject(pathManager)
                }
            })
        }
    }
}

I am passing enum on the parent level. I am wrapping the Buttons in NavigationLink in order to get the navigation prompts (chevrons) back. In child view I can then append any other type to the path. Thanks to @son for steering me onto the right path.

1

There are 1 best solutions below

2
On BEST ANSWER

NavigationPath can append generic type which conforms Hashable. So you can totally add AircraftEntity since this model conforms to the Hashable protocol. And change .navigationDestination for this type.

List {
    ForEach(pilotVM.filteredPilots, id: \.id) { pilot in
        Button {
            path.append(pilot)
        } label: {
            PilotCell(pilot: pilot, isInDetailView: false)
                .contentShape(Rectangle())
                .swipeActions(edge: .trailing) { delete(for: pilot) }
                .swipeActions(edge: .leading) { markFavorite(for: pilot) }
        }
        ...
    }
}
.navigationDestination(for: PilotEntity.self) { pilot in
    PilotDatabaseDetailView(pilot: pilot)
}

Also, multiple types in the same context:

NavigationStack(path: $path) {
    List {
        Section {
            Button {
                path.append("Pilot") //<- put something to recognize Pilot
            } label: {
                Label("Pilots", systemImage: "person.2")
                    .foregroundStyle(.primary)
            }
            
            Button {
                path.append(111) //<- put something to recognize Aircraft
            } label: {
                Label("Aircraft", systemImage: "airplane.circle")
                    .foregroundStyle(.primary)
            }
        } header: {
            Text("Database")
        }
    }
    .navigationDestination(for: String.self) { text in
        //Pilot
    }
    .navigationDestination(for: Int.self) { num in
        //Aircraft
    }
}