Change NavigationSplitView to NavigationStack

164 Views Asked by At

In the Food Truck app Apple Food Truck App the contentView has a NavigationSplitView to target iPadOS.

        NavigationSplitView {
            Sidebar(selection: $selection)
        } detail: {
            NavigationStack(path: $path) {
                DetailColumn(selection: $selection, model: model)
            }
        }
...

I would like to change this to remove the navigationSplitView so that it targets iOS only. Something like this:

        NavigationStack(path: $path) {
            Sidebar(selection: $selection)
                .navigationDestination(for: Panel.self) { panel in
                    DetailColumn(selection: $selection, model: model)
            }
        }


...

There are a couple of problems with this - the first problem is no navigation occurs. This is fixed by removing the $selection in Sidecar(). The second problem is that when the app does navigate, it only navigates to ".truck". To fix this, I have to go into DetailColumn and change selection to @State and change the above code to:

        NavigationStack(path: $path) {
            Sidebar(selection: $selection)
                .navigationDestination(for: Panel.self) { panel in
                    DetailColumn(selection: panel, model: model)
            }
        }
...

Even though this works, Xcode does complain that "A navigationDestination for “Food_Truck.Panel” was declared earlier on the stack. Only the destination declared closest to the root view of the stack will be used."

I think I am missing some understanding of how this should be approached, or the design of the app was done so that it works with NavigationSplitView and not NavigationStackView directly.

Thanks in advance.

1

There are 1 best solutions below

4
On BEST ANSWER

I don't quite follow the changes you made, but I've made similar changes and the app works.

First, remove the selection in Sidebar. Sidebar should no longer have a selection property. You should just use the parameterless List initialiser.

struct Sidebar: View {
    var body: some View {
        List {
            NavigationLink(value: Panel.truck) {
                Label("Truck", systemImage: "box.truck")
            }
            
            NavigationLink(value: Panel.orders) {
                Label("Orders", systemImage: "shippingbox")
            }
            ...

Then you can even get rid of the selection state in ContentView.

Next, selection in DetailColumn need not be a Binding anymore. It is merely used to determine which detail view to show, and DetailColumn doesn't change it at all, so it can be a let.

struct DetailColumn: View {
    let selection: Panel?
    @ObservedObject var model: FoodTruckModel
    @State var timeframe: Timeframe = .today
    var body: some View {
        switch selection ?? .truck {
        case .truck:
            TruckView(model: model) // TruckView no longer needs to take the selection
         ...

Note that TruckView doesn't need to take a $selection binding. TruckView only uses it in the cardNavigation computed property. This property just decides how navigation should be done. Notice that on iPhones, this always returns .navigationLink. So you can just change it to:

var cardNavigation: TruckCardHeaderNavigation {
    .navigationLink
}

and remove @Binding var navigationSelection: Panel?.

Finally, the code in ContentView looks like this:

NavigationStack(path: $path) {
    Sidebar()
        .navigationDestination(for: Panel.self) { panel in
            DetailColumn(selection: panel, model: model)
    }
}