SwiftUI Navigation in iOS 16: Stacks and Split Views

With iOS 16, Apple replaced the legacy NavigationView with two purpose-built, data-driven components: NavigationStack and NavigationSplitView. This update fundamentally changes how SwiftUI handles routing by decoupling view hierarchy from navigation logic. By binding view transitions directly to data models, developers gain full programmatic control over the view stack, native multi-column support across Apple platforms, and streamlined deep linking.

The Modern Navigation Model

Prior to iOS 16, SwiftUI relied on NavigationLink(destination:isActive:), which embedded the destination view directly into the UI declaration. This approach caused performance bottlenecks, redundant view instantiations, and unreliable programmatic routing.

iOS 16 solves this by introducing type-driven routing using the .navigationDestination(for:destination:) modifier. Views present destinations dynamically based on the type of data passed to them, keeping the source view decoupled from the destination view.

Using NavigationStack for Linear Hierarchies

NavigationStack is designed for single-column navigation flows, standard on iPhone devices. It maintains a stack of views where the root view is always present, and additional views can be pushed onto or popped off the stack.

Programmatic Routing with State

A NavigationStack can be bound directly to a collection of data representing the current path:

struct ContentView: View {
    @State private var path: [Product] = []

    var body: some View {
        NavigationStack(path: $path) {
            List(products) { product in
                NavigationLink(value: product) {
                    Text(product.name)
                }
            }
            .navigationDestination(for: Product.self) { product in
                ProductDetailView(product: product)
            }
        }
    }
}

Key features of this pattern include:

  • Heterogeneous Stacks: By using NavigationPath instead of a strongly typed array, developers can push different data types onto the same stack while maintaining type safety.
  • Deep Linking and State Restoration: Manipulating the path collection automatically pushes or pops screens. Resetting navigation to the root view requires only clearing the array (path.removeAll()).
  • Deferred Loading: Destination views are only initialized when pushed, eliminating the premature rendering issues present in older SwiftUI versions.

Using NavigationSplitView for Multi-Column Layouts

NavigationSplitView provides native support for multi-pane layouts on iPadOS, macOS, and larger display environments. It replaces the ambiguous split-screen behavior of NavigationView with explicit column structures.

Two-Column and Three-Column Interfaces

NavigationSplitView accepts either two or three columns depending on layout requirements:

struct SplitExampleView: View {
    @State private var selectedCategory: Category?
    @State private var selectedItem: Item?

    var body: some View {
        NavigationSplitView {
            // Sidebar
            List(categories, selection: $selectedCategory) { category in
                Text(category.title).tag(category)
            }
        } content: {
            // Content List
            if let selectedCategory {
                List(selectedCategory.items, selection: $selectedItem) { item in
                    Text(item.name).tag(item)
                }
            } else {
                Text("Select a category")
            }
        } detail: {
            // Detail View
            if let selectedItem {
                ItemDetailView(item: selectedItem)
            } else {
                Text("Select an item")
            }
        }
    }
}

Adaptive Behavior Across Screen Sizes

NavigationSplitView adapts automatically to the available display environment:

  • On regular horizontal size classes (such as iPad landscape or macOS), all columns appear side by side or as an interactive slide-out pane.
  • On compact size classes (such as standard iPhone portrait), the split view collapses into a linear stack hierarchy. The system uses selection state bindings to coordinate between the collapsed single view and expanded multi-pane modes seamlessly.

Key Differences Summary

  • NavigationStack is intended for single-column, drill-down interactions where users navigate deeper into a single branch of content.
  • NavigationSplitView is intended for hierarchical, multi-level navigation across multiple columns that adapt responsively to screen size.
  • Both APIs eliminate fragile view-level state flags in favor of persistent, model-driven data paths.