NavigationStack Deep Links in Forty Lines
A value-driven router turns any URL into a navigation path — no coordinators required.
Deep linking used to mean threading flags through half the view hierarchy. With NavigationStack bound to a path, navigation becomes plain data: an array of routes. Parse a URL into that array and the stack rebuilds itself to match.
Start with an enum of every destination the app can push. Making it Hashable is all NavigationStack needs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
enum Route: Hashable {
case recipe(id: Int)
case author(name: String)
case settings
}
@Observable final class Router {
var path: [Route] = []
func open(_ url: URL) {
guard url.scheme == "iosbuffet" else { return }
let parts = url.pathComponents.dropFirst() // drop "/"
switch (url.host, parts.first) {
case ("recipe", let id?):
if let id = Int(id) { path = [.recipe(id: id)] }
case ("author", let name?):
path = [.author(name: name)]
case ("settings", _):
path = [.settings]
default:
break
}
}
}
struct RootView: View {
@State private var router = Router()
var body: some View {
NavigationStack(path: $router.path) {
HomeView()
.navigationDestination(for: Route.self) { route in
switch route {
case .recipe(let id): RecipeView(id: id)
case .author(let name): AuthorView(name: name)
case .settings: SettingsView()
}
}
}
.onOpenURL { router.open($0) }
.environment(router)
}
}
Replacing path wholesale means a link always lands in a predictable place, regardless of where the user was. If you’d rather push on top of the current stack, append instead.
Why this scales
Because the path is just an array, it’s trivially testable — feed open(_:) a URL and assert on path without rendering a single view. It’s also Codable if your routes are, which gives you state restoration almost for free.
Universal links follow the same route: handle NSUserActivity with .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) and pass its webpageURL through the same parser.
— 30 —
Surviving Swift 6's Sendable Warnings
Strict concurrency lit up 300 warnings overnight. A triage order that clears most of them in a day.
Instruments Finds the Leak Your Closures Hid
A view controller that never died. The Memory Graph Debugger found the cycle in minutes.