Bijgewerkt: 3 september 2026
NavigationStack is sinds iOS 16 de moderne, type-veilige manier om navigatie in SwiftUI te bouwen, en in iOS 26 is het het enige door Apple ondersteunde pad. NavigationView is definitief afgekeurd. In deze gids laat ik zien hoe je een NavigationStack opzet, programmatische navigatie met NavigationPath beheert, deep links vanaf een universele URL afhandelt, en waarom toegankelijkheid vanaf de eerste regel code hoort. Elk codefragment werkt op Xcode 26 en iOS 26. (Ja, ik heb het net vanmorgen nog geverifieerd op een verse installatie.)
Wat is het verschil tussen NavigationStack en NavigationView?
Het korte antwoord: NavigationView is een verouderde container waarin bestemmingen aan elke NavigationLink vastzitten, terwijl NavigationStack een expliciet, type-veilig pad beheert dat je kunt lezen, wijzigen en herstellen. Dat verschil klinkt bureaucratisch, maar het bepaalt hoe je hele app is opgebouwd. In NavigationView gaf je de bestemming mee aan de link, waardoor elke bestemming eager werd gebouwd, je niet programmatisch terug naar de root kon springen, en deep links een onhandige puzzel van geneste bindings vormden.
Met NavigationStack verhuis je de bestemming naar de root van de stack via .navigationDestination(for:). Je koppelt een waardetype (bijvoorbeeld Recipe) aan een view. Elke NavigationLink(value:) ergens dieper in de hiërarchie duwt eenvoudigweg een waarde op het pad. De view wordt lazy gebouwd, precies op het moment dat je hem nodig hebt.
Dit is ook waarom ik in de vergaderzaal zo koppig ben over navigatie: als je pad een value type is, wordt je hele navigatiestatus Codable, testbaar en gemakkelijk te snapshotten voor state restoration. Vergelijk het met de UIKit-wereld. Een UINavigationController-stack was altijd een array van UIViewController-instanties, dus moeilijk te serialiseren zonder complexe coder-logica. Een SwiftUI-pad is gewoon een array van hashable waarden. Zie de officiële Apple NavigationStack-documentatie voor de volledige API.
| Aspect | NavigationView (verouderd) | NavigationStack (iOS 16+) |
|---|---|---|
| Type-safety van pad | Impliciet, gebonden aan links | Expliciet via Hashable-waarden |
| Lazy destination-opbouw | Nee, eager evaluation | Ja, alleen bij push |
| Pop-to-root | Onmogelijk zonder truc | path.removeAll() |
| Deep linking | Complexe geneste bindings | Eén NavigationPath vullen |
| State restoration | Handmatig, foutgevoelig | Codable-pad via SceneStorage |
| iPad-splitview | Ingebakken maar rigide | Aparte NavigationSplitView |
| iOS 26-status | Deprecation-warning | Aanbevolen |
Basisimplementatie van NavigationStack in iOS 26
Goed, laten we beginnen met het simpelste realistische voorbeeld: een lijst met recepten die naar een detailweergave navigeert. Het patroon dat ik hieronder toon is de aanbeveling voor 95% van alle NavigationStack-code die je schrijft. Eén root-view, één .navigationDestination(for:) per waardetype, en NavigationLink(value:) voor elke navigatie-actie.
import SwiftUI
struct Recipe: Hashable, Identifiable {
let id: UUID
let name: String
let prepMinutes: Int
}
struct RecipeListView: View {
let recipes: [Recipe]
var body: some View {
NavigationStack {
List(recipes) { recipe in
NavigationLink(value: recipe) {
HStack {
Text(recipe.name)
Spacer()
Text("\(recipe.prepMinutes) min")
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("Recepten")
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetailView(recipe: recipe)
}
}
}
}
Twee dingen om op te merken. Ten eerste: Recipe moet Hashable zijn. Als je bestaande model dat nog niet is, kun je meestal volstaan met een : Hashable conformance, want Swift synthetiseert de implementatie automatisch als alle stored properties hashable zijn. Ten tweede: de .navigationDestination-modifier staat direct op de List, niet op de root van de NavigationStack. Dat maakt in dit geval geen verschil, maar zodra je meerdere stacks in TabView plaatst, moet elke stack zijn eigen destinations registreren.
Programmatische navigatie met NavigationPath
Zodra je meer wilt dan simpele tap-navigatie (denk aan een "Ga terug naar begin"-knop, een wizard die drie stappen vooruit springt na een netwerkcall, of een deep link vanaf een pushmelding), heb je programmatische controle nodig. Dat doe je met een gebonden pad. Je hebt twee opties: een array van één specifiek type, of het type-erased NavigationPath.
import SwiftUI
@Observable
final class NavigationModel {
var path = NavigationPath()
func openRecipe(_ recipe: Recipe) {
path.append(recipe)
}
func openSettings() {
path.append(SettingsRoute.main)
}
func popToRoot() {
path.removeLast(path.count)
}
}
enum SettingsRoute: Hashable {
case main, notifications, privacy
}
struct RootView: View {
@State private var model = NavigationModel()
var body: some View {
NavigationStack(path: $model.path) {
RecipeListView(recipes: sampleRecipes)
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetailView(recipe: recipe)
}
.navigationDestination(for: SettingsRoute.self) { route in
SettingsView(route: route)
}
.environment(model)
}
}
}
Het gebruik van @Observable (uit de nieuwe Swift 6.2 Observation-API) betekent dat elke view die model.path leest automatisch wordt hertekend zodra het pad muteert, precies zoals je wilt. Belangrijk: als je een array van één type gebruikt ([Recipe]) in plaats van NavigationPath, krijg je meer type-veiligheid maar verlies je de flexibiliteit om verschillende typen te pushen. Voor het merendeel van productie-apps kies ik NavigationPath, omdat scenario's als deep linking bijna altijd meerdere routetypen mengen.
De methode path.removeLast(path.count) is de canonieke "pop to root". Sinds iOS 17 kun je ook direct path.removeAll() gebruiken op een reguliere array-binding, maar voor NavigationPath blijft removeLast(_:) de weg.
Hoe implementeer je deep linking met NavigationStack?
Deep linking is waar NavigationStack echt schittert. Het idee: je vangt een universele link of custom URL-scheme op, parseert deze naar een reeks routewaarden, en zet die in één keer op het pad. Omdat het pad code is en niet een reeks UIKit push-animaties, kun je zeven schermen diep springen in één frame. (Ik hit deze bug bij het lanceren van een reisapp, waar een pushmelding drie schermen diep moest openen; met NavigationView voelde het als plakband, met NavigationStack is het één regel.)
import SwiftUI
extension NavigationModel {
func handle(url: URL) {
// Voorbeeld: myapp://recipe/123/step/2
guard url.scheme == "myapp" else { return }
var newPath = NavigationPath()
let components = url.pathComponents.filter { $0 != "/" }
var i = 0
while i < components.count {
switch components[i] {
case "recipe":
if i + 1 < components.count,
let id = UUID(uuidString: components[i + 1]),
let recipe = RecipeStore.shared.find(id: id) {
newPath.append(recipe)
i += 2
} else { i += 1 }
case "step":
if i + 1 < components.count,
let step = Int(components[i + 1]) {
newPath.append(RecipeStep(index: step))
i += 2
} else { i += 1 }
default:
i += 1
}
}
withAnimation(.smooth(duration: 0.35)) {
self.path = newPath
}
}
}
struct RootView: View {
@State private var model = NavigationModel()
var body: some View {
NavigationStack(path: $model.path) {
RecipeListView(recipes: sampleRecipes)
.navigationDestination(for: Recipe.self) { RecipeDetailView(recipe: $0) }
.navigationDestination(for: RecipeStep.self) { StepView(step: $0) }
}
.onOpenURL { url in
model.handle(url: url)
}
}
}
Merk op dat ik de pad-mutatie in een withAnimation(.smooth(duration: 0.35))-block wikkel. Voor deep links die meerdere niveaus diep springen, geeft de standaard .easeInOut soms een korte "double bounce" op iOS 26. De .smooth-curve (nieuw sinds iOS 17 en verfijnd in iOS 26) is een kritisch gedempte spring die één vloeiende beweging garandeert. Voor mij is dat één van die kleine timing-details die het verschil maakt tussen "goede app" en "app die aanvoelt als Apple's eigen".
NavigationSplitView voor iPad, Mac en Vision Pro
Voor iPhone en Apple Watch blijft NavigationStack je gereedschap. Voor iPad, macOS en visionOS wil je bijna altijd NavigationSplitView, dat een twee- of driekoloms layout beheert. In iOS 26 zijn er duidelijke API-verbeteringen: kolomzichtbaarheid kan nu worden gepersisteerd via SceneStorage, en met de nieuwe Liquid Glass-materialen krijgen de kolom-dividers automatisch de juiste vibrancy.
struct RecipeSplitView: View {
@State private var selectedCategory: Category?
@State private var selectedRecipe: Recipe?
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.name, value: category)
}
.navigationTitle("Categorieën")
} content: {
if let category = selectedCategory {
List(recipes(in: category), selection: $selectedRecipe) { recipe in
NavigationLink(recipe.name, value: recipe)
}
.navigationTitle(category.name)
} else {
ContentUnavailableView("Kies een categorie", systemImage: "list.bullet")
}
} detail: {
if let recipe = selectedRecipe {
RecipeDetailView(recipe: recipe)
} else {
ContentUnavailableView("Kies een recept", systemImage: "fork.knife")
}
}
}
}
Belangrijke tip: NavigationSplitView kan een NavigationStack in de detail-kolom nesten voor secundaire push-navigatie. Dit is het patroon dat Apple's eigen Mail-app gebruikt. Zet NavigationStack nooit rondom een NavigationSplitView, want dat leidt tot dubbele back-knoppen en gebroken kolomrestoration.
Toolbars en back-button aanpassen
Toolbars in NavigationStack werken via de .toolbar-modifier op de gepushte view, niet op de stack zelf. De ToolbarItem-plaatsingen zoals .topBarLeading, .topBarTrailing en .principal geven je fijnmazige controle.
struct RecipeDetailView: View {
let recipe: Recipe
@State private var isFavourite = false
var body: some View {
ScrollView {
RecipeContent(recipe: recipe)
}
.navigationTitle(recipe.name)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
withAnimation(.spring(response: 0.3)) {
isFavourite.toggle()
}
} label: {
Image(systemName: isFavourite ? "heart.fill" : "heart")
.symbolEffect(.bounce, value: isFavourite)
}
.accessibilityLabel(isFavourite ? "Verwijder uit favorieten" : "Voeg toe aan favorieten")
}
}
}
}
De .symbolEffect(.bounce, value:)-modifier is één van de mooie iOS 26-toevoegingen. SF Symbols animeren automatisch met een organische bounce wanneer value verandert. Combineer het met een spring(response:)-animatie op de state-mutatie en je krijgt gratis dat "Apple-gevoel". Voor het verbergen van de back-knop gebruik je .navigationBarBackButtonHidden(true), maar doe dit alleen als je een expliciete vervanging biedt. Anders raken VoiceOver-gebruikers verdwaald.
Toegankelijkheid en VoiceOver in NavigationStack
Dit is het onderdeel dat de meeste tutorials overslaan, en waar ik me eerlijk gezegd kwaad om maak. NavigationStack heeft heel goede standaard-toegankelijkheid: VoiceOver kondigt push-transities aan met "Terug naar <vorige titel>", scrollt automatisch naar het eerste focusseerbare element van de nieuwe view, en respecteert de navigationTitle als het "screen name" voor rotors.
Maar er zijn valkuilen. Als je de back-knop vervangt (bijvoorbeeld met een "Klaar"-knop bij een modaal flow), verlies je die automatische aankondiging. De oplossing:
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button("Klaar") { dismiss() }
.accessibilityLabel("Klaar, sluit dit scherm")
.accessibilityHint("Sluit het huidige scherm en gaat terug naar de lijst")
}
}
Verder: als je custom animaties gebruikt voor push-transities, respecteer dan @Environment(\.accessibilityReduceMotion). Voor gebruikers met vestibulaire aandoeningen zijn snelle horizontale slides misselijkmakend. Gebruik in dat geval een cross-fade:
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var transition: AnyTransition {
reduceMotion ? .opacity : .slide
}
De Apple Accessibility-documentatie heeft een volledig hoofdstuk over navigatie-patronen dat elke SwiftUI-engineer een keer per jaar moet herlezen. En schakel VoiceOver in tijdens QA, niet alleen aan het eind, maar terwijl je bouwt. Toegankelijkheid moet geweven zijn, niet toegevoegd.
Veelgemaakte fouten en oplossingen
Na drie jaar productie-NavigationStack-code zijn dit de fouten die ik keer op keer zie voorbijkomen:
- "NavigationLink werkt niet in mijn TabView-tab." Elke tab moet zijn eigen
NavigationStackhebben. Wikkel de tab-inhoud, niet deTabView. - "Mijn destination wordt twee keer aangeroepen." Je hebt
.navigationDestination(for:)zowel in de root als in een child geregistreerd. Verwijder de child-registratie. - "Path wordt niet gerestored na crash." Sla
NavigationPath.CodableRepresentationop via@SceneStorage, niet het pad zelf; en zorg dat elk pushed typeCodableconformeert. - "NavigationLink push toont geen back-knop." Je view zit niet in een
NavigationStack. Controleer of je root-view daadwerkelijk in een stack zit, niet in een sheet zonder eigen stack. - "Sheet-inhoud verliest zijn navigation destinations." Een sheet is een aparte view-hiërarchie. Wikkel de sheet-inhoud in een eigen
NavigationStack. - "VoiceOver leest de titel niet uit." Je gebruikt een custom-view als
toolbar principalzonderaccessibilityLabel. GebruiknavigationTitlewaar mogelijk.
Voor de foutafhandeling van navigatie-acties zelf, bijvoorbeeld een deep-link die niet parsebaar is, is Swift 6.2's typed throws-patroon ideaal. Definieer een NavigationError enum, laat je URL-parser die throwen, en presenteer een ContentUnavailableView met een duidelijke boodschap.
Veelgestelde vragen
Wat is NavigationPath in SwiftUI?
NavigationPath is een type-erased container die een lijst van Hashable-waarden vasthoudt die het huidige navigatie-pad in een NavigationStack vertegenwoordigt. Je kunt waarden appenden, verwijderen of het hele pad vervangen om programmatische navigatie of deep linking te implementeren.
Kan ik NavigationView nog gebruiken in iOS 26?
Technisch wel, want NavigationView compileert nog met een deprecation-warning, maar Apple zal het in een toekomstige release verwijderen. Nieuwe code moet altijd NavigationStack of NavigationSplitView gebruiken; bestaande code migreer je best zo snel mogelijk.
Hoe navigeer je terug naar de root in NavigationStack?
Roep path.removeLast(path.count) aan op een NavigationPath of path.removeAll() op een array-binding. Beide operaties zijn animeerbaar en werken zowel voor gebruikers-acties als voor deep links.
Waarom werkt mijn NavigationStack niet in een sheet?
Sheets zijn afzonderlijke view-hiërarchieën die geen NavigationStack of .navigationDestination-modifiers van de presentator erven. Wikkel de sheet-inhoud in een eigen NavigationStack en registreer daarin de benodigde destinations.
Wat is het verschil tussen NavigationStack en NavigationSplitView?
NavigationStack beheert een pushed navigatie-hiërarchie (één kolom), ideaal voor iPhone en Watch. NavigationSplitView beheert een twee- of driekoloms layout voor iPad, Mac en visionOS. Ze kunnen genest worden: een NavigationStack in de detail-kolom van een NavigationSplitView is een standaardpatroon.




