Preserving navigation state in NavigationSplitView detail view.

I have a simple NavigationSplitView with various detail views and I want to preserve the navigation state in each of these detail views when the user switches detail views. But it seems that the navigation path is always reset when switching views.

//
//  ContentView.swift
//  Test
//
//  Created by Miles Egan on 6/13/24.
//

import SwiftUI

enum SelectedView {
    case first, second
}

enum DetailItem: String, CaseIterable, Hashable {
    case one, two, three
}

struct DetailView: View {
    let label: String

    var body: some View {
        VStack {
            Text(label)

            ForEach(DetailItem.allCases, id: \.self) { item in
                NavigationLink(item.rawValue, value: item)
            }
        }
        .navigationTitle(label)
    }
}

struct ContentView: View {
    @State private var selectedItem = SelectedView.first
    @State private var firstPath = NavigationPath()
    @State private var secondPath = NavigationPath()

    var body: some View {
        NavigationSplitView {
            List(selection: $selectedItem) {
                Text("First")
                    .tag(SelectedView.first)
                Text("Second")
                    .tag(SelectedView.second)
            }
        } detail: {
            ZStack {
                NavigationStack(path: $firstPath) {
                    DetailView(label: "First Start")
                        .opacity(selectedItem == .first ? 1 : 0)
                        .navigationDestination(for: DetailItem.self) { item in
                            DetailView(label: item.rawValue)
                        }
                }
                NavigationStack(path: $secondPath) {
                    DetailView(label: "Second Start")
                        .opacity(selectedItem == .second ? 1 : 0)
                        .navigationDestination(for: DetailItem.self) { item in
                            DetailView(label: item.rawValue)
                        }
                }
            }
        }
    }
}

For example. Try this and navigate a few times when the left sidebar is on the first item. Then switch to the second item. Then back to the first item. The state of the detail view is always reset instead of being preserved. It doesn't work if I let SwiftUI manage the navigation paths instead of providing them myself either.

Answered by DTS Engineer in 790846022

@milesegan You could try using a navigation link to perform navigation based on a presented data value:

 NavigationSplitView {
            List {
                NavigationLink("First", value: SelectedView.first)
                NavigationLink("Second", value: SelectedView.second)
                
            }
        } detail: { 
          ....
}

Keep in mind that, If your List has a selection binding and it is of the same type as the navigation Link; when you activate the link, it will be added to the value of the lists selection and that resets the NavigationStack path.

@milesegan You could try using a navigation link to perform navigation based on a presented data value:

 NavigationSplitView {
            List {
                NavigationLink("First", value: SelectedView.first)
                NavigationLink("Second", value: SelectedView.second)
                
            }
        } detail: { 
          ....
}

Keep in mind that, If your List has a selection binding and it is of the same type as the navigation Link; when you activate the link, it will be added to the value of the lists selection and that resets the NavigationStack path.

Thanks for the suggestion. When I make this change the detail view no longer changes to reflect the selection in the sidebar. Do I need to make changes to the detail view for this to work?

In case anybody else is struggling with this, the solution I finally found that works is like this. Basically not to use a selectable list as suggested above, and use willSet and didSet on the selected item to update the path.

Observable
class NavigationModel {
    private var navigationPaths = [RootSection: NavigationPath]()

    var path = NavigationPath()

    var selectedSection: RootSection? = RootSection.home {
        willSet {
            if let selectedSection {
                navigationPaths[selectedSection] = path
            }
        }
        didSet {
            if let selectedSection, let newPath = navigationPaths[selectedSection] {
                path = newPath
            }
        }
    }

    init(sections: [RootSection]) {
        for section in sections {
            navigationPaths[section] = NavigationPath()
        }
    }

    func selectSection(_ section: RootSection?) {
        if let section, selectedSection == section {
            navigationPaths[section] = NavigationPath()
        } else {
            selectedSection = section
        }
    }
}
Preserving navigation state in NavigationSplitView detail view.
 
 
Q