SwiftUI NavigationStack‑Muster für vorhersehbare mehrstufige Flows
SwiftUI NavigationStack‑Muster für mehrstufige Flows: klare Routen, sicheres Zurück‑Verhalten und praktische Beispiele für Onboarding und Genehmigungs‑Wizards.

Was bei mehrstufigen Flows schiefgeht
Ein mehrstufiger Flow ist jede Abfolge, bei der Schritt 1 passieren muss, damit Schritt 2 Sinn ergibt. Häufige Beispiele sind Onboarding, eine Genehmigungsanfrage (Überprüfen, Bestätigen, Absenden) und Wizard-ähnliche Dateneingaben, bei denen ein Entwurf über mehrere Bildschirme aufgebaut wird.
Diese Flows wirken nur dann einfach, wenn Zurück sich so verhält, wie Nutzer es erwarten. Wenn Zurück sie an einen überraschenden Ort bringt, verlieren Nutzer das Vertrauen in die App. Das zeigt sich in falschen Einreichungen, abgebrochenem Onboarding und Supportanfragen wie „Ich komme nicht mehr zu dem Bildschirm, auf dem ich war.“
Unordentliche Navigation sieht oft so aus:
- Die App springt zum falschen Bildschirm oder verlässt den Flow zu früh.
- Derselbe Bildschirm erscheint zweimal, weil er zweimal gepusht wurde.
- Ein Schritt setzt sich beim Zurückgehen zurück und der Nutzer verliert seinen Entwurf.
- Der Nutzer erreicht Schritt 3, ohne Schritt 1 abgeschlossen zu haben, und es entsteht ein ungültiger Zustand.
- Nach einem Deep Link oder App-Neustart zeigt die App zwar den richtigen Bildschirm, aber mit falschen Daten.
Ein nützliches Denkmodell: Ein mehrstufiger Flow sind zwei Dinge, die zusammenlaufen.
Erstens ein Stack von Bildschirmen (wohin der Nutzer zurückgehen kann). Zweitens gemeinsamer Flow-State (Entwurfsdaten und Fortschritt), der nicht einfach verschwindet, weil ein Bildschirm verschwindet.
Viele NavigationStack-Setups gehen schief, wenn sich der Bildschirm-Stack und der Flow-State auseinander bewegen. Zum Beispiel könnte ein Onboarding-Flow „Profil erstellen“ zweimal pushen (duplizierte Routen), während der Entwurfsprofilzustand in der View liegt und bei jedem Re-Render neu erstellt wird. Der Nutzer geht zurück, sieht eine andere Version des Formulars und hält die App für unzuverlässig.
Vorhersehbares Verhalten beginnt damit, den Flow zu benennen, festzulegen, was Zurück an jedem Schritt tun sollte, und dem Flow-State ein klares Zuhause zu geben.
NavigationStack-Grundlagen, die du wirklich brauchst
Für mehrstufige Flows verwende NavigationStack statt des älteren NavigationView. NavigationView kann sich je nach iOS-Version unterschiedlich verhalten und ist schwerer zu durchschauen, wenn du Bildschirme pushst, popst oder wiederherstellst. NavigationStack ist die moderne API, die Navigation wie einen echten Stack behandelt.
Ein NavigationStack speichert eine Historie, wo der Nutzer gewesen ist. Jeder Push fügt ein Ziel zum Stack hinzu. Jede Zurück-Aktion entfernt ein Ziel. Diese einfache Regel macht einen Flow stabil: die UI sollte eine klare Abfolge von Schritten widerspiegeln.
Was der Stack wirklich hält
SwiftUI speichert nicht deine View-Objekte. Es speichert die Daten, die du zum Navigieren benutzt hast (deinen Routenwert), und baut damit bei Bedarf die Ziel-View neu auf. Das hat praktische Konsequenzen:
- Verlasse dich nicht darauf, dass eine View am Leben bleibt, um wichtige Daten zu halten.
- Wenn ein Bildschirm Zustand braucht, lege ihn in ein Modell (z. B. ein
ObservableObject), das außerhalb der gepushten View lebt. - Wenn du dasselbe Ziel zweimal mit unterschiedlichen Daten pushst, behandelt SwiftUI sie als verschiedene Stack-Einträge.
NavigationPath ist das richtige Werkzeug, wenn dein Flow nicht nur aus ein oder zwei festen Pushes besteht. Denk daran wie an eine editierbare Liste von „wohin wir gehen“-Werten. Du kannst Routen anhängen, um vorwärts zu gehen, das letzte Element entfernen, um zurückzugehen, oder den gesamten Pfad ersetzen, um zu einem späteren Schritt zu springen.
Es passt gut, wenn du Wizard-artige Schritte brauchst, den Flow nach Abschluss zurücksetzen willst oder einen teilweise abgeschlossenen Flow aus gespeichertem Zustand wiederherstellen möchtest.
Vorhersehbar schlägt clever. Weniger versteckte Regeln (automatische Sprünge, implizite Pops, view-getriebene Nebeneffekte) bedeutet später weniger seltsame Back-Stack-Bugs.
Modelliere den Flow mit einem kleinen Route-Enum
Vorhersehbare Navigation beginnt mit einer Entscheidung: Halte das Routing an einer Stelle und mach jeden Bildschirm des Flows zu einem kleinen, klaren Wert.
Erstelle eine einzige Quelle der Wahrheit, z. B. einen FlowRouter (ein ObservableObject), der die NavigationPath besitzt. Das hält jedes Push/Pop konsistent, statt Navigation über Views zu verstreuen.
Eine einfache Router-Struktur
Verwende ein Enum, um Schritte zu repräsentieren. Füge assoziierte Werte nur für leichte Identifizierer (wie IDs) hinzu, nicht ganze Modelle.
enum Step: Hashable {
case welcome
case profile
case verifyCode(phoneID: UUID)
case review(applicationID: UUID)
case done
}
final class FlowRouter: ObservableObject {
@Published var path = NavigationPath()
func go(_ step: Step) { path.append(step) }
func back() { if !path.isEmpty { path.removeLast() } }
func reset() { path = NavigationPath() }
}
Trenne Flow-State vom Navigations-State
Behandle Navigation als „wo der Nutzer ist“ und Flow-State als „was er bisher eingegeben hat“. Lege Flow-Daten in einem eigenen Store ab (z. B. OnboardingState mit Name, E‑Mail, hochgeladenen Dokumenten) und halte sie stabil, während Views kommen und gehen.
Eine einfache Faustregel:
FlowRouter.pathenthält nurStep-Werte.OnboardingStateenthält die Eingaben und Entwurfsdaten des Nutzers.- Schritte tragen IDs, um Daten nachzuschlagen, nicht die Daten selbst.
Das vermeidet fragiles Hashing, riesige Pfade und überraschende Resets, wenn SwiftUI Views neu aufbaut.
Schritt für Schritt: Baue einen Wizard mit NavigationPath
Für Wizard-ähnliche Bildschirme ist der einfachste Ansatz, den Stack selbst zu kontrollieren. Ziel: eine Quelle der Wahrheit für „wo bin ich im Flow?“ und einen einzigen Weg vorwärts oder zurück.
Beginne mit einem NavigationStack(path:), gebunden an eine NavigationPath. Jeder gepushte Bildschirm wird durch einen Wert repräsentiert (oft ein Enum-Fall), und du registrierst navigationDestination einmal.
import SwiftUI
enum WizardRoute: Hashable {
case profile
case verifyEmail
case permissions
case review
}
struct OnboardingWizard: View {
@State private var path = NavigationPath()
@State private var currentIndex = 0
private let steps: [WizardRoute] = [.profile, .verifyEmail, .permissions, .review]
var body: some View {
NavigationStack(path: $path) {
StartScreen {
goToStep(0) // push first step
}
.navigationDestination(for: WizardRoute.self) { route in
switch route {
case .profile:
ProfileStep(onNext: { goToStep(1) })
case .verifyEmail:
VerifyEmailStep(onNext: { goToStep(2) })
case .permissions:
PermissionsStep(onNext: { goToStep(3) })
case .review:
ReviewStep(onEditProfile: { popToStep(0) })
}
}
}
}
private func goToStep(_ index: Int) {
currentIndex = index
path.append(steps[index])
}
private func popToStep(_ index: Int) {
let toRemove = max(0, currentIndex - index)
if toRemove > 0 { path.removeLast(toRemove) }
currentIndex = index
}
}
Um Zurück vorhersehbar zu halten, halte dich an ein paar Gewohnheiten. Hänge genau eine Route an, um voranzukommen, lass „Weiter“ linear sein (nur den nächsten Schritt pushen), und wenn du zurückspringen musst (z. B. „Profil bearbeiten“ von Review), trimme den Stack auf einen bekannten Index.
Das vermeidet versehentliche Duplikate und sorgt dafür, dass Zurück dem Nutzererwarteten entspricht: ein Tap = ein Schritt.
Halte Daten stabil, während Views kommen und gehen
Ein mehrstufiger Flow wirkt unzuverlässig, wenn jeder Bildschirm seinen eigenen Zustand besitzt. Du gibst einen Namen ein, gehst vorwärts, gehst zurück, und das Feld ist leer, weil die View neu erstellt wurde.
Die Lösung ist einfach: Behandle den Flow als ein Entwurfsobjekt, und lass jeden Schritt daran arbeiten.
In SwiftUI bedeutet das meist, ein geteiltes ObservableObject einmal beim Start des Flows zu erzeugen und an jeden Schritt weiterzureichen. Speichere Entwurfswerte nicht in jedem View-@State, es sei denn, sie gehören wirklich nur zu diesem Bildschirm.
final class OnboardingDraft: ObservableObject {
@Published var fullName = ""
@Published var email = ""
@Published var wantsNotifications = false
var canGoNextFromProfile: Bool {
!fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
&& email.contains("@")
}
}
Erzeuge das Objekt am Einstiegspunkt und teile es dann mit @StateObject und @EnvironmentObject (oder gib es explizit weiter). Nun kann sich der Stack ändern, ohne dass Daten verloren gehen.
Entscheide, was Back überlebt
Nicht alles sollte für immer bestehen bleiben. Lege Regeln vorher fest, damit der Flow konsistent bleibt.
Behalte Nutzereingaben (Textfelder, Umschalter, Auswahlen), außer sie werden bewusst zurückgesetzt. Setze schritt-spezifische UI-Zustände zurück (Ladespinner, temporäre Alerts, kurze Animationen). Leere sensible Felder (wie Einmal-Codes), wenn du den Schritt verlässt. Wenn eine Wahl nachfolgende Schritte beeinflusst, lösche nur die abhängigen Felder.
Validierung passt hier natürlich rein. Anstatt den Nutzer vorwärtsschreiten zu lassen und auf dem nächsten Bildschirm einen Fehler zu zeigen, halte ihn auf dem aktuellen Schritt, bis er gültig ist. Das Deaktivieren des Buttons basierend auf einer berechneten Eigenschaft wie canGoNextFromProfile reicht oft aus.
Checkpoints speichern, aber nicht übertreiben
Manche Entwürfe können nur im Speicher leben. Andere sollten App-Neustarts oder Abstürze überstehen. Ein praktischer Default:
- Halte Daten im Speicher, während der Nutzer aktiv durch die Schritte geht.
- Persistiere lokal bei klaren Meilensteinen (Konto erstellt, Genehmigung abgeschickt, Zahlung gestartet).
- Speichere früher, wenn der Flow lang ist oder die Dateneingabe länger als eine Minute dauert.
So können Bildschirme kommen und gehen, und der Fortschritt des Nutzers bleibt stabil und respektiert seine Zeit.
Deep Links und das Wiederherstellen eines teilweise abgeschlossenen Flows
Deep Links sind wichtig, weil reale Flows selten bei Schritt 1 beginnen. Jemand tippt in einer E‑Mail, einer Push‑Benachrichtigung oder einem geteilten Link und erwartet, am richtigen Bildschirm zu landen, etwa Schritt 3 des Onboardings oder die finale Genehmigungsseite.
Mit NavigationStack behandle einen Deep Link als Anweisung, einen gültigen Pfad aufzubauen, nicht als Befehl, zu einer View zu springen. Starte am Anfang des Flows und hänge nur die Schritte an, die für diesen Nutzer und diese Session zutreffen.
Verwandle einen externen Link in eine sichere Reihenfolge von Routen
Ein gutes Muster ist: parse die externe ID, lade die minimal nötigen Daten und konvertiere sie dann in eine Sequenz von Routen.
enum Route: Hashable {
case start
case profile
case verifyEmail
case approve(requestID: String)
}
func pathForDeepLink(requestID: String, hasProfile: Bool, emailVerified: Bool) -> [Route] {
var routes: [Route] = [.start]
if !hasProfile { routes.append(.profile) }
if !emailVerified { routes.append(.verifyEmail) }
routes.append(.approve(requestID: requestID))
return routes
}
Diese Prüfungen sind deine Leitplanken. Wenn Voraussetzungen fehlen, wirf den Nutzer nicht auf Schritt 3 mit einem Fehler und ohne Weg vorwärts. Schicke ihn zum ersten fehlenden Schritt und sorge dafür, dass der Back-Stack weiterhin eine kohärente Geschichte erzählt.
Wiederherstellen eines teilweise abgeschlossenen Flows
Um nach einem Neustart wiederherzustellen, speichere zwei Dinge: den zuletzt bekannten Routenstatus und die vom Nutzer eingegebenen Entwurfsdaten. Entscheide dann, wie du weitermachst, ohne den Nutzer zu überraschen.
Wenn der Entwurf frisch ist (Minuten oder Stunden), biete eine klare „Fortsetzen“-Option an. Ist er alt, starte von vorne, fülle Felder aber mit dem Entwurf vor. Wenn sich Anforderungen geändert haben, baue den Pfad mit denselben Leitplanken neu auf.
Push vs Modal: mach den Flow leicht verlassbar
Ein Flow wirkt vorhersehbar, wenn es einen Hauptweg nach vorn gibt: Screens auf einem einzigen Stack pushen. Verwende Sheets und full‑screen covers für Nebentasks, nicht für den Hauptpfad.
Push (NavigationStack) passt, wenn der Nutzer erwartet, dass Zurück seine Schritte rückverfolgt. Modals (sheet oder fullScreenCover) passen, wenn der Nutzer eine Nebenaufgabe erledigt, schnell etwas auswählt oder eine riskante Aktion bestätigt.
Eine einfache Regel verhindert die meisten Navigationsprobleme:
- Push für den Hauptpfad (Schritt 1, Schritt 2, Schritt 3).
- Nutze ein Sheet für kleine optionale Aufgaben (Datum wählen, Land auswählen, Dokument scannen).
- Verwende
fullScreenCoverfür „getrennte Welten“ (Login, Kameramodus, langes rechtliches Dokument). - Nutze ein Modal für Bestätigung (Flow abbrechen, Entwurf löschen, zur Genehmigung abschicken).
Der häufige Fehler ist, Haupt-Flows in Sheets zu legen. Wenn Schritt 2 ein Sheet ist, kann der Nutzer es mit Wischen schließen, Kontext verlieren und in einem Stack landen, der sagt, er sei auf Schritt 1, während die Daten zeigen, dass Schritt 2 abgeschlossen ist.
Bestätigungen sind das Gegenteil: einen „Bist du sicher?“-Screen in den Wizard zu pushen verstopft den Stack und kann Schleifen erzeugen (Schritt 3 -> Bestätigen -> Zurück -> Schritt 3 -> Zurück -> Bestätigen).
Wie man nach „Fertig“ alles sauber schließt
Entscheide zuerst, was „Fertig“ bedeutet: Zurück zum Home, zurück zur Liste oder Erfolg anzeigen.
Wenn der Flow gepusht ist, setze deine NavigationPath auf leer, um zum Start zurückzupoppen. Wenn der Flow modal präsentiert wurde, rufe dismiss() aus der Umgebung auf. Wenn beides kombiniert ist (ein Modal mit NavigationStack), schließe das Modal, nicht jeden gepushten Bildschirm einzeln. Nach erfolgreichem Absenden lösche auch Entwurfszustand, damit ein neu geöffneter Flow frisch startet.
Back-Button-Verhalten und „Bist du sicher?“-Momente
In den meisten mehrstufigen Flows ist das Beste: nichts tun — lass System-Back-Button (und Swipe‑Back‑Gesten) arbeiten. Das entspricht den Erwartungen der Nutzer und vermeidet Bugs, bei denen die UI etwas anderes sagt als der Navigationszustand.
Abfangen lohnt sich nur, wenn Zurück echten Schaden anrichten würde, z. B. Verlust eines langen ungespeicherten Formulars oder das Abbrechen einer nicht umkehrbaren Aktion. Wenn der Nutzer sicher zurückkehren und weitermachen kann, füge keinen Reibungsverlust hinzu.
Praktisch ist es, die Systemnavigation zuzulassen, aber nur dann eine Bestätigung einzubauen, wenn der Bildschirm „dirty“ ist. Das bedeutet, eine eigene Back‑Aktion bereitzustellen und einmal zu fragen, mit einer klaren Möglichkeit zum Verlassen.
@Environment(\.dismiss) private var dismiss
@State private var showLeaveConfirm = false
let hasUnsavedChanges: Bool
var body: some View {
Form { /* fields */ }
.navigationBarBackButtonHidden(hasUnsavedChanges)
.toolbar {
if hasUnsavedChanges {
ToolbarItem(placement: .navigationBarLeading) {
Button("Back") { showLeaveConfirm = true }
}
}
}
.confirmationDialog("Discard changes?", isPresented: $showLeaveConfirm) {
Button("Discard", role: .destructive) { dismiss() }
Button("Keep Editing", role: .cancel) {}
}
}
Vermeide, dass das zur Falle wird:
- Frage nur, wenn du die Konsequenz in einem kurzen Satz erklären kannst.
- Biete eine sichere Option (Abbrechen, Weiter bearbeiten) plus einen klaren Ausweg (Verwerfen, Verlassen).
- Verstecke Back‑Buttons nicht, es sei denn, du ersetzt sie durch ein offensichtlich ersichtliches Back oder Close.
- Bestätige besser die irreversible Aktion (z. B. „Genehmigen“) statt überall Navigation zu blockieren.
Wenn du ständig gegen die Back‑Geste kämpfen musst, ist das meist ein Zeichen dafür, dass der Flow Autosave, gespeicherte Entwürfe oder kleinere Schritte braucht.
Häufige Fehler, die seltsame Back-Stacks erzeugen
Die meisten „Warum ist es dorthin zurückgegangen?“-Bugs sind nicht SwiftUI, das zufällig ist. Sie entstehen meist durch Muster, die den Navigationszustand instabil machen. Für vorhersehbares Verhalten behandle den Back-Stack wie App‑Daten: stabil, testbar und an einem Ort besessen.
Aus Versehen mehrere Stacks
Eine häufige Falle ist, mehr als einen NavigationStack zu haben, ohne es zu merken. Zum Beispiel hat jeder Tab seinen eigenen Root‑Stack, und dann fügt eine Child‑View nochmal einen Stack innerhalb des Flows hinzu. Das Ergebnis ist verwirrendes Zurück‑Verhalten, fehlende Navigation Bars oder Screens, die nicht wie erwartet poppen.
Ein weiteres häufiges Problem ist, die NavigationPath zu oft neu zu initialisieren. Wenn der Pfad in einer View erzeugt wird, die neu rendert, kann er bei State‑Änderungen zurückgesetzt werden und den Nutzer nach Eingabe in ein Feld zurück zu Schritt 1 springen.
Die Fehler hinter den meisten seltsamen Stacks sind einfach:
NavigationStackinnerhalb eines anderen Stacks verschachteln (oft in Tabs oder Sheet‑Inhalten)NavigationPath()bei View‑Updates neu initialisieren statt in langlebigem State zu halten- Nicht‑stabile Werte in deiner Route (z. B. ein sich änderndes Modell) verwenden, was
Hashablebricht und zu mismatched destinations führt - Navigationsentscheidungen über Button-Handler verstreuen, bis niemand mehr erklären kann, was „Weiter“ bedeutet
- Den Flow von mehreren Stellen gleichzeitig steuern lassen (z. B. ViewModel und View verändern beide den Pfad)
Wenn du Daten zwischen Schritten übergeben musst, verwende stabile Identifikatoren in der Route (IDs, Step‑Enums) und halte die eigentlichen Formdaten im geteilten State.
Ein konkretes Beispiel: Wenn deine Route .profile(User) ist und User sich beim Tippen ändert, kann SwiftUI das als andere Route behandeln und den Stack umschreiben. Mach die Route .profile und speichere das Entwurfsprofil in geteiltem State.
Kurze Checkliste für vorhersehbare Navigation
Wenn sich ein Flow seltsam anfühlt, liegt es meist daran, dass der Back‑Stack nicht dieselbe Geschichte wie der Nutzer erzählt. Vor dem UI‑Polish: überprüfe deine Navigationsregeln.
Teste auf einem echten Gerät, nicht nur in Previews, und probiere langsame und schnelle Taps. Schnelle Taps decken oft doppelte Pushes und fehlenden State auf.
- Gehe einen Schritt nach dem anderen vom letzten Bildschirm zum ersten zurück. Bestätige, dass jeder Bildschirm dieselben eingegebenen Daten zeigt.
- Löse Abbrechen von jedem Schritt aus (inkl. erster und letzter). Bestätige, dass es immer an einen sinnvollen Ort zurückkehrt, nicht zu einem zufälligen früheren Bildschirm.
- Beende die App mitten im Flow und starte neu. Stelle sicher, dass du sicher fortsetzen kannst, entweder durch Wiederherstellung des Pfads oder durch Neustart an einem bekannten Schritt mit gespeicherten Daten.
- Öffne den Flow per Deep Link oder App‑Shortcut. Verifiziere, dass der Zielschritt gültig ist; fehlen benötigte Daten, leite zum frühesten Schritt, der sie sammeln kann.
- Schließe mit Fertig ab und bestätige, dass der Flow sauber entfernt ist. Der Nutzer sollte nicht Zurück drücken können, um einen abgeschlossenen Wizard wieder zu betreten.
Ein einfacher Test: Stell dir ein Onboarding‑Wizard mit drei Bildschirmen vor (Profil, Berechtigungen, Bestätigung). Gib einen Namen ein, gehe vorwärts, gehe zurück, ändere ihn, springe dann per Deep Link zur Bestätigung. Wenn die Bestätigung den alten Namen zeigt oder Zurück zu einem duplizierten Profilbildschirm führt, sind deine Pfad‑Updates inkonsistent.
Wenn du die Checkliste ohne Überraschungen bestehst, wirkt dein Flow ruhig und vorhersehbar, selbst wenn Nutzer später zurückkehren.
Ein realistisches Beispiel und nächste Schritte
Stell dir einen Genehmigungs‑Flow für Auslagen vor. Er hat vier Schritte: Review, Edit, Confirm und Receipt. Der Nutzer erwartet eins: Zurück geht immer zum vorherigen Schritt, nicht zu einem zufälligen Bildschirm, den er früher besucht hat.
Ein einfaches Route‑Enum hält das vorhersehbar. Deine NavigationPath sollte nur die Route und kleine Identifikatoren speichern, um Zustand neu zu laden, z. B. expenseID und einen mode (review vs edit). Vermeide es, große, veränderliche Modelle in den Pfad zu pushen, weil das Wiederherstellung und Deep Links fragil macht.
Halte den Arbeitsentwurf in einer einzigen Quelle der Wahrheit außerhalb der Views, z. B. einem @StateObject Flow‑Modell (oder einem Store). Jeder Schritt liest und schreibt dieses Modell, so dass Screens erscheinen und verschwinden können, ohne Eingaben zu verlieren.
Mindestens verfolgst du drei Dinge:
- Routen (z. B.:
review(expenseID),edit(expenseID),confirm(expenseID),receipt(expenseID)) - Daten (ein Entwurfsobjekt mit Positionen und Notizen sowie ein Status wie
pending,approved,rejected) - Ort (Entwurf im Flow‑Modell, kanonischer Datensatz auf dem Server und ein kleines Restore‑Token lokal: expenseID + letzter Schritt)
Edge‑Cases sind die Stellen, an denen Flows Vertrauen gewinnen oder verlieren. Wenn der Manager in Confirm ablehnt, entscheide, ob Zurück nach Edit führt (zum Korrigieren) oder den Flow verlässt. Bei späterer Rückkehr stelle den letzten Schritt vom gespeicherten Token wieder her und lade den Entwurf. Wechseln Nutzer das Gerät, betrachte den Server als Wahrheit: Rekonstruiere den Pfad aus dem Server‑Status und sende sie zum passenden Schritt.
Nächste Schritte: Dokumentiere dein Route‑Enum (was jeder Fall bedeutet und wann er verwendet wird), füge ein paar einfache Tests für Pfad‑Aufbau und Wiederherstellung hinzu und halte dich an eine Regel: Views besitzen keine Navigationsentscheidungen.
Wenn du ähnliche mehrstufige Flows baust, ohne alles von Grund auf neu zu schreiben, wenden Plattformen wie AppMaster (appmaster.io) dasselbe Prinzip an: Trenne Schritt‑Navigation und Business‑Daten, damit Screens wechseln können, ohne den Fortschritt der Nutzer zu zerstören.
FAQ
Verwende NavigationStack mit einem einzigen NavigationPath, den du kontrollierst. Füge pro „Weiter“-Aktion genau eine Route hinzu und entferne pro Zurück-Aktion genau eine Route. Wenn du springen musst (z. B. „Profil bearbeiten“ von Review aus), schneide den Pfad auf einen bekannten Schritt zurück statt weitere Bildschirme zu pushen.
Weil SwiftUI Zielansichten aus dem Routenwert neu aufbaut, nicht aus einer erhaltenen View-Instanz. Wenn deine Formdaten in @State der View liegen, können sie beim Wiederaufbau zurückgesetzt werden. Lege Entwurfsdaten in ein geteiltes Modell (z. B. ein ObservableObject) außerhalb der gepushten Views.
Das passiert meist, wenn dieselbe Route mehr als einmal angehängt wurde (oft durch schnelle Taps oder mehrere Codepfade). Deaktiviere den Weiter-Button während der Navigation oder während Validierung/Laden läuft, und zentralisiere Navigation, damit pro Schritt nur ein append ausgeführt wird.
Bewahre kleine, stabile Werte wie Enum-Fälle plus leichte IDs auf. Mutable Daten (der Entwurf) gehören in ein separates geteiltes Objekt und werden bei Bedarf per ID nachgeschlagen. Große, sich ändernde Modelle im Pfad können Hashable-Erwartungen brechen und zu fehlerhaften Zielen führen.
Navigation ist „wo der Benutzer ist“, Flow-State ist „was er eingegeben hat“. Besitze den Navigationspfad in einem Router (oder einem obersten State) und den Entwurf in einem separaten ObservableObject. Jede View bearbeitet den Entwurf; der Router ändert nur die Schritte.
Behandle einen Deep Link als Anleitung, einen gültigen Schrittfolge-Pfad zu bauen, nicht als Teleportation zu einer einzelnen View. Hänge nötige Voraussetzungsschritte zuerst an (abhängig davon, was der Nutzer bereits hat), und dann den Zielschritt. So bleibt der Back-Stack kohärent.
Speichere zwei Dinge: die zuletzt bedeutsame Route (oder Schritt-ID) und die Entwurfsdaten. Beim Neustart baue den Pfad mit den gleichen Voraussetzungsprüfungen wie bei Deep Links wieder auf und lade den Entwurf. Ist der Entwurf alt, ist es oft weniger überraschend, den Flow neu zu starten, aber Felder vorzufüllen.
Push für den Hauptpfad, damit Back die Schritte natürlicherweise zurückverfolgt. Verwende Sheets für optionale Nebentasks und fullScreenCover für separate Erlebnisse wie Login oder Kamera. Vermeide Hauptschritte in Modals, weil Dismiss-Wischen UI und Flow-State entkoppeln kann.
Nicht standardmäßig; lass das System-Back-Verhalten arbeiten. Bestätige nur, wenn das Verlassen echten Schaden anrichten würde (z. B. Verlust langer ungesicherter Eingaben), und nur wenn der Bildschirm tatsächlich „dirty“ ist. Bevorzuge Autosave oder Entwurfs-Persistenz, wenn du oft Bestätigungen brauchst.
Typische Ursachen sind das Verschachteln mehrerer NavigationStacks, das Neuerzeugen von NavigationPath bei View-Updates und mehrere Besitzer, die den Pfad mutieren. Halte einen Stack pro Flow, lege den Pfad in langlebigen State (@StateObject oder Router) und zentralisiere Push/Pop-Logik.


