How to structure SwiftUI navigation with NavigationSplitView and a router

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:

  1. One NavigationStack per column. Never wrap a NavigationSplitView in a NavigationStack.
  2. One router per stack. Don’t share paths across columns.
  3. Use selection: bindings for sidebar-to-detail.
  4. Route deep links through the app router, which fans out to child routers.
  5. If you find yourself reaching for .isDetailLink, you’re on the iOS 15 path. Restructure.


Avoid Delays and Rejections when Submitting Your App to The Store!


Follow my FREE cheat sheets to design, develop, or even amend your app to deserve its virtual shelf space in the App Store.

* indicates required

When you subscribe you’ll also get programming tips, business advices, and career rants from the trenches about twice a month. I respect your e-mail privacy.

0 thoughts on “How to structure SwiftUI navigation with NavigationSplitView and a router

Leave a Reply