Does anybody have a working NavigationSplitView with NavigationStack in Details View?

642 Views Asked by At

No matter how it is set up, a complex 3 Columns NavigationSplitView with NavigationStack crashes or has inconsistent workflow.

  • Sidebar selection determines which content (or feature) is in Content.

  • Content (or feature) who is live (or selected) can control what the Details.

  • Details has to be Navigable, and navigationDestinations depend on each feature (selected Content), so they cannot be registered all under one root.

Options I have tried: (many more as well)

@State var sideBar: SideBar?
@State var content: Content?
NavigationSplitView(
    sidebar: {
        List(sidebar) { ... }
    },
    content: {
        switch sidebar { ... }
    },
    detail: {
        XXX what to put here?
    }
).navigationSplitViewStyle(.balanced)

Each sidebar brings a set of routes, that depends on it. So in details I have tried multiple options

// this does not work as NavigationStack is not simple
// enough to be completely removed or recreated
// somehow it ties on the Scene and crashes when
// switching between sidebars
detail: {
    switch sidebar {
    case .a:
        NavigationStack {
            View()
            .navigationDestination(...
        }
    }
}
// clearing the navigationPath just before switching
// sidebars, but still controlling a NavigationStack
// inside details does not work, nothing happens.
detail: {
    switch sidebar {
    case .a:
        NavigationStack(navigationPath) {
            View()
            .navigationDestination(...
        }
    }
}
// Having an empty NavigationStack in details
// but registering the destinations inside the content.
// This works however, when switching between sidebar
// it also crashes sometimes or it shows the Warning triangle
// on the navigation stack, as destinations become unavailable.
content: {
    switch sidebar {
    case .a:
        RootView()
            .navigationDestination(...)
    }
},
detail: {
    NavigationStack { }
}
// Trying to clear out the path before switching
// does not work either.
content: {
    switch sidebar {
    case .a:
        RootView()
            .navigationDestination(...)
    }
},
detail: {
    NavigationStack(navigationPath) { }
}
1

There are 1 best solutions below

1
On

After much debugging, I found a way that works.

Placing a single NavigationStack in the Details view and maintaining the NavigationPath somewhere. When switching between SideBar, I had to clear the path before allowing the NavigationSplitView to refresh it self and its Content.

I also had to set a specific .id() to the content view that is the sidebar, somehow, even when sidebar changes, the content is not being refreshed.

I have tested this on 16.4 iPhone, iPad and MacOs.

// in StateObject, Model or wherever
var selectedSidebar: SomeSidebarType? {
    willSet {
        if navigationPath.isEmpty {
            // if the stack is empty, we do not need to clear it
            objectWillChange.send()
        } else {
            // otherwise clear it, and do not send an update
            // as the stack will update the view and itself
            stack.removeLast(stack.count)
        }
    }
}

@ViewBuilder
var detail: some View {
    NavigationStack(path: $storage.navigationPath) {
        switch storage.selectedSidebar {
        case .a:
            SomeView
        case .b:
            SomeView
        default:
            Text("No Selection")
        }
    }
}