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…