You moved your SwiftUI app to a router pattern with a top-level NavigationPath, added a NavigationSplitView for iPad and Mac, and now pushing a view from the router replaces the entire content column instead of stacking inside the sidebar. You reach for .isDetailLink(false) — the old escape hatch — and find it does nothing once NavigationPath is involved. This is one of the most common SwiftUI architecture traps in 2026, and Apple Developer Technical Support has now answered it definitively.
The short version: a centralized NavigationPath at the top of the view hierarchy and a NavigationSplitView are not compatible. The fix is to scope each NavigationStack to the column that owns it.
Why a top-level NavigationPath fights NavigationSplitView
NavigationSplitView is a layout with columns where each column shows one view at a time. NavigationPath is a stack that drives a single NavigationStack. When you wrap your whole app in a NavigationStack(path: $router.path) and put a NavigationSplitView inside it, every push replaces the entire split view.
.isDetailLink(false) was the iOS 15 fix for NavigationView. It does not apply to NavigationStack or NavigationPath.
The DTS-recommended pattern
Stop driving everything from a top-level NavigationPath. Put a NavigationStack inside the sidebar column.
import SwiftUI
enum SidebarDestination: Hashable {
case folder(Folder)
case tag(Tag)
}
struct RootView: View {
var body: some View {
NavigationSplitView {
SidebarView()
} detail: {
ContentUnavailableView("Pick something", systemImage: "sidebar.left")
}
}
}
struct SidebarView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
List {
NavigationLink("Inbox", value: SidebarDestination.folder(.inbox))
NavigationLink("Archive", value: SidebarDestination.folder(.archive))
}
.navigationDestination(for: SidebarDestination.self) { dest in
switch dest {
case .folder(let folder): FolderView(folder: folder)
case .tag(let tag): TagView(tag: tag)
}
}
}
}
}
Keeping a router pattern that scales
The router pattern is still valid. Give each scoped stack its own router:
@MainActor
final class SidebarRouter: ObservableObject {
@Published var path = NavigationPath()
func open(_ destination: SidebarDestination) {
path.append(destination)
}
func popToRoot() {
path = NavigationPath()
}
}
@MainActor
final class AppRouter: ObservableObject {
let sidebar = SidebarRouter()
let detail = DetailRouter()
}
struct RootView: View {
@StateObject private var router = AppRouter()
var body: some View {
NavigationSplitView {
SidebarView().environmentObject(router.sidebar)
} detail: {
DetailColumn().environmentObject(router.detail)
}
}
}
When you genuinely need cross-column navigation
Use selection binding, not a shared NavigationPath:
struct RootView: View {
@State private var selectedFolder: Folder?
var body: some View {
NavigationSplitView {
List(Folder.allCases, selection: $selectedFolder) { folder in
NavigationLink(folder.name, value: folder)
}
} detail: {
if let folder = selectedFolder {
NavigationStack { FolderView(folder: folder) }
} else {
ContentUnavailableView("Pick a folder", systemImage: "folder")
}
}
}
}
Deep linking still works
Route URLs through the right child router:
extension AppRouter {
func handle(url: URL) {
if let sidebarDest = SidebarDestination(url: url) {
sidebar.path = NavigationPath()
sidebar.path.append(sidebarDest)
}
if let detailDest = DetailDestination(url: url) {
detail.path = NavigationPath()
detail.path.append(detailDest)
}
}
}
The general rule
A short checklist for any non-trivial SwiftUI app:
- One
NavigationStackper column. Never wrap aNavigationSplitViewin aNavigationStack. - One router per stack. Don’t share paths across columns.
- Use
selection:bindings for sidebar-to-detail. - Route deep links through the app router, which fans out to child routers.
- If you find yourself reaching for
.isDetailLink, you’re on the iOS 15 path. Restructure.
0 thoughts on “How to structure SwiftUI navigation with NavigationSplitView and a router”