iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

NavigationStack

SwiftUI navigation has settled on NavigationStack (iOS 16+) for typed routes, NavigationSplitView for sidebar/list/detail on iPad and Mac, and tab-based navigation via TabView. The modern path: define routes as values (often an enum), drive the path from state, and use navigationDestination(for:) to map types to screens.

NavigationStack, typed paths, sheets, deep links

EXAMPLE
import SwiftUI

// 1) Define routes as a value type — push them, don't push views
enum Route: Hashable {
    case productDetail(id: String)
    case cart
    case checkout(orderId: String)
}

// 2) Drive the navigation stack from a typed path
struct CatalogView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            List {
                Button("Wool jacket") { path.append(Route.productDetail(id: "sku-1")) }
                Button("Linen shirt") { path.append(Route.productDetail(id: "sku-2")) }
                Button("Open cart")    { path.append(Route.cart) }
            }
            .navigationTitle("Catalog")
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .productDetail(let id):
                    ProductDetail(id: id, onCheckout: { orderId in
                        path.append(Route.checkout(orderId: orderId))
                    })
                case .cart:
                    CartView(onCheckout: { orderId in
                        path.append(Route.checkout(orderId: orderId))
                    })
                case .checkout(let orderId):
                    CheckoutView(orderId: orderId, onDone: {
                        // Pop to root after success
                        path = NavigationPath()
                    })
                }
            }
        }
    }
}

// 3) Programmatic navigation patterns
extension NavigationPath {
    mutating func popToRoot() { self = NavigationPath() }
    mutating func pop(_ n: Int = 1) { self.removeLast(n) }
}

// 4) Sheets and full-screen covers — for modal flows
struct ProductDetail: View {
    let id: String
    let onCheckout: (String) -> Void
    @State private var showShare = false

    var body: some View {
        VStack(spacing: 16) {
            Text("Product \(id)")
            Button("Buy now") { onCheckout("o-\(UUID().uuidString.prefix(8))") }
            Button("Share")    { showShare = true }
        }
        .padding()
        .sheet(isPresented: $showShare) {
            ShareView(item: id)
                .presentationDetents([.medium, .large])
        }
    }
}

struct ShareView: View {
    let item: String
    @Environment(\.dismiss) private var dismiss
    var body: some View {
        VStack {
            Text("Share \(item)?")
            Button("Cancel") { dismiss() }
        }.padding()
    }
}

// 5) NavigationSplitView — multi-column on iPad / Mac
enum CatalogItem: Hashable { case overview, products, orders }

struct SplitDemo: View {
    @State private var selection: CatalogItem? = .overview

    var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                Label("Overview", systemImage: "chart.bar").tag(CatalogItem.overview)
                Label("Products", systemImage: "bag").tag(CatalogItem.products)
                Label("Orders",   systemImage: "shippingbox").tag(CatalogItem.orders)
            }
            .navigationTitle("Shop")
        } detail: {
            switch selection {
            case .overview: Text("Overview")
            case .products: Text("Products")
            case .orders:   Text("Orders")
            case nil:       Text("Pick a section")
            }
        }
    }
}

// 6) Deep links — onOpenURL maps a URL to a path
@main
struct ShopApp: App {
    @State private var rootPath = NavigationPath()
    var body: some Scene {
        WindowGroup {
            CatalogView()
                .onOpenURL { url in
                    // shop://product/sku-1
                    guard url.scheme == "shop" else { return }
                    let parts = url.pathComponents.filter { $0 != "/" }
                    if url.host == "product", let id = parts.first {
                        rootPath.append(Route.productDetail(id: id))
                    }
                }
        }
    }
}

// 7) Stubs
struct CartView: View {
    let onCheckout: (String) -> Void
    var body: some View { Button("Checkout") { onCheckout("o-cart") } }
}
struct CheckoutView: View {
    let orderId: String
    let onDone: () -> Void
    var body: some View {
        VStack {
            Text("Checking out \(orderId)")
            Button("Done") { onDone() }
        }
    }
}

Why it matters

Drive navigation from a typed path (`NavigationPath`) and map routes to screens via `navigationDestination(for:)`. The view tree stops carrying navigation logic, deep links become "append a Route value", and "go back N screens" is a one-line `path.removeLast(n)` — exactly the abstractions SwiftUI was missing in earlier versions.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
NavigationStack {
    List(users) { u in
        NavigationLink(u.name, destination: ProfileView(user: u))
    }
    .navigationTitle("Users")
}
Try it Yourself »

Discussion

Loading…