Lernziel
Nach dieser Lektion kannst du mit NavigationStack Bildschirme aufeinanderstapeln, mit NavigationLink von einer Liste in eine Detailansicht wechseln, Navigationstitel setzen und die wertbasierte Navigation mit navigationDestination einsetzen.
Das Problem: Eine App hat mehr als einen Bildschirm
Bisher passte alles auf einen Bildschirm. Echte Apps sind aber hierarchisch aufgebaut: In der Mail-App tippst du auf ein Postfach und siehst die Nachrichten. Du tippst auf eine Nachricht und siehst ihren Inhalt. Oben links bringt dich ein Pfeil zurück. Und ein Wisch vom linken Rand tut dasselbe.
Dieses Muster heißt Stapelnavigation: Jeder neue Bildschirm wird auf die vorherigen gelegt, wie Karten auf einen Stapel. Zurück heißt: die oberste Karte wieder wegnehmen.
NavigationStack
struct ContentView: View {
var body: some View {
NavigationStack {
List {
Text("Erste Zeile")
Text("Zweite Zeile")
}
.navigationTitle("Start")
}
}
}NavigationStackist der Rahmen für die Navigation. Er verwaltet den Stapel und zeigt oben eine Navigationsleiste..navigationTitle("Start")setzt den Titel. Wichtig: Der Modifier steht an der View im Stack, hier an derList, nicht amNavigationStackselbst.
Erwartetes Ergebnis
Oben ein großer Titel „Start“, darunter die Liste. Scrollst du nach unten, schrumpft der Titel und wandert in die Leiste. Genau wie in den Systemapps.
NavigationLink: Zum nächsten Bildschirm
NavigationStack {
List {
NavigationLink("Einstellungen") {
Text("Hier sind die Einstellungen")
.navigationTitle("Einstellungen")
}
NavigationLink("Über die App") {
Text("Version 1.0")
.navigationTitle("Über")
}
}
.navigationTitle("Start")
}Ein NavigationLink ist eine Zeile, die beim Antippen eine neue View auf den Stapel legt. Der Titel steht in den Klammern, das Ziel in der Closure.
In einer Liste bekommt jede Link-Zeile automatisch einen kleinen Pfeil › am rechten Rand. Und auf dem Zielbildschirm erscheint oben links automatisch ein Zurück-Knopf mit dem Titel des vorherigen Bildschirms.
Tipp
Du musst für den Zurück-Weg nichts programmieren. Zurück-Knopf, Wischgeste vom Rand und Animation bringt NavigationStack mit.
Eigene Zeilen als Link
Wie bei Button (Lektion 2.11) kannst du das Aussehen frei gestalten:
NavigationLink {
ContactDetail(contact: contact)
} label: {
HStack {
Image(systemName: "person.circle.fill")
Text(contact.name)
}
}Liste und Detailansicht
Das häufigste Muster überhaupt: eine Liste von Elementen, und ein Tipp öffnet die Details. Mit allem aus den letzten Lektionen:
struct Book: Identifiable {
let id = UUID()
let title: String
let author: String
let pages: Int
}
extension Book {
static let examples = [
Book(title: "Momo", author: "Michael Ende", pages: 304),
Book(title: "Der kleine Prinz", author: "Antoine de Saint-Exupéry", pages: 96),
Book(title: "Krabat", author: "Otfried Preußler", pages: 320)
]
}
struct BookList: View {
let books = Book.examples
var body: some View {
NavigationStack {
List(books) { book in
NavigationLink {
BookDetail(book: book)
} label: {
VStack(alignment: .leading) {
Text(book.title)
.font(.headline)
Text(book.author)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("Bücher")
}
}
}
struct BookDetail: View {
let book: Book
var body: some View {
VStack(spacing: 12) {
Image(systemName: "book.closed.fill")
.font(.system(size: 80))
.foregroundStyle(.brown)
Text(book.title)
.font(.largeTitle)
.bold()
Text("von \(book.author)")
Text("\(book.pages) Seiten")
.foregroundStyle(.secondary)
}
.padding()
.navigationTitle(book.title)
.navigationBarTitleDisplayMode(.inline)
}
}Erwartetes Ergebnis
Eine Liste „Bücher“ mit drei Einträgen. Ein Tipp auf „Momo“ schiebt die Detailansicht von rechts herein, mit einem Buch-Symbol, dem Titel, dem Autor und der Seitenzahl. Oben links steht „‹ Bücher“.
Ein paar Details:
BookDetailbekommt das Buch als ganz normale Eigenschaft (Lektion 2.2). Sie zeigt es nur an, also reichtlet..navigationBarTitleDisplayMode(.inline)zeigt den Titel klein in der Leiste statt groß darunter. Für Detailseiten ist das oft passender.
Nur ein NavigationStack
Der NavigationStack gehört an die oberste View, die navigiert. Die Detailansicht bekommt keinen eigenen NavigationStack. Sie ist ja schon Teil des Stapels. Ein zweiter, verschachtelter Stack führt zu doppelten Leisten und seltsamem Verhalten.
Wertbasierte Navigation
Die Schreibweise oben, mit dem Ziel direkt im Link, funktioniert gut für einfache Fälle. Sie hat aber eine Schwäche: SwiftUI muss für jede Zeile schon die Zielview beschreiben, auch wenn der Nutzer sie nie öffnet.
Moderner und flexibler ist die wertbasierte Navigation: Der Link transportiert nur einen Wert, und an einer zentralen Stelle steht, welche View für welchen Werttyp gezeigt wird:
struct BookList: View {
let books = Book.examples
var body: some View {
NavigationStack {
List(books) { book in
NavigationLink(book.title, value: book)
}
.navigationTitle("Bücher")
.navigationDestination(for: Book.self) { book in
BookDetail(book: book)
}
}
}
}NavigationLink(book.title, value: book): „Wenn getippt wird, leg diesen Wert auf den Stapel.“.navigationDestination(for: Book.self) { ... }: „Liegt einBookauf dem Stapel, zeig dafür diese View.“
Damit das funktioniert, muss Book Hashable sein (Lektion 1.15):
struct Book: Identifiable, Hashable {
let id = UUID()
let title: String
let author: String
let pages: Int
}Swift erzeugt Hashable automatisch, weil alle Eigenschaften selbst Hashable sind.
Welche Variante nehmen?
Für den Anfang sind beide in Ordnung. Die wertbasierte Variante hat zwei große Vorteile, die später wichtig werden: Die Ziele werden erst erzeugt, wenn sie wirklich gebraucht werden, und du kannst Navigation per Code steuern, etwa direkt zu einem bestimmten Buch springen. Das lernst du in Lektion 3.20. Gewöhn dir deshalb gleich die wertbasierte Variante an.
Mehrere Ziele
Ein Stapel kann verschiedene Werttypen enthalten. Für jeden Typ gibt es dann eine eigene navigationDestination:
NavigationStack {
List {
Section("Bücher") {
ForEach(books) { book in
NavigationLink(book.title, value: book)
}
}
Section("Autoren") {
ForEach(authors) { author in
NavigationLink(author.name, value: author)
}
}
}
.navigationDestination(for: Book.self) { book in
BookDetail(book: book)
}
.navigationDestination(for: Author.self) { author in
AuthorDetail(author: author)
}
}Und auch in der Detailansicht darf es wieder Links geben, etwa vom Buch zum Autor. Der Stapel wächst dann einfach weiter.
Daten zurückgeben
Was, wenn die Detailansicht ein Buch bearbeiten soll und die Liste die Änderung sehen muss? Dafür hast du schon das Werkzeug: ein Binding (Lektion 2.10).
struct BookList: View {
@State private var books = Book.examples
var body: some View {
NavigationStack {
List($books) { $book in
NavigationLink(book.title) {
BookEditor(book: $book)
}
}
.navigationTitle("Bücher")
}
}
}
struct BookEditor: View {
@Binding var book: Book
var body: some View {
Form {
TextField("Titel", text: $book.title)
Stepper("Seiten: \(book.pages)", value: $book.pages, in: 1...2000)
}
.navigationTitle("Bearbeiten")
}
}List($books) { $book in ... } funktioniert wie ForEach($books) aus Lektion 2.15. Damit Titel und Seiten änderbar sind, müssen sie in Book mit var statt let deklariert sein. Änderst du im Editor den Titel und gehst zurück, steht der neue Titel schon in der Liste.
Typische Fehler
Titel erscheint nicht
NavigationStack {
List { ... }
}
.navigationTitle("Start")Der Titel steht am NavigationStack statt an der View darin. Verschieb ihn an die List.
Link ist ausgegraut
Ein NavigationLink funktioniert nur innerhalb eines NavigationStack. Ohne Stack ist er deaktiviert. Häufige Ursache: Die Preview zeigt nur die Liste, nicht den Stack drumherum. Pack sie in der Preview in einen NavigationStack.
Nichts passiert beim Tippen
Bei der wertbasierten Variante fehlt die passende .navigationDestination(for:). In der Konsole steht dann ein Hinweis, dass für den Typ kein Ziel gefunden wurde. Prüf außerdem, ob der Typ in for: genau mit dem Typ des Werts übereinstimmt.
Hashable fehlt
Fehlermeldung
Initializer 'init(_:value:)' requires that 'Book' conform to 'Hashable'
Für NavigationLink(value:) muss der Typ Hashable sein. Ergänze : Hashable.
navigationDestination in einer Liste
Steht .navigationDestination innerhalb eines ForEach oder einer Zeile, warnt SwiftUI und findet das Ziel womöglich nicht zuverlässig. Setz es an die Liste oder eine andere View, die immer sichtbar ist.
FAQ
Anfänger
Was bedeutet das überhaupt?Wie ändere ich den Text des Zurück-Knopfs? Er zeigt automatisch den Titel des vorherigen Bildschirms. Ist dieser sehr lang, kürzt iOS ihn zu „Zurück“. Das ist so gewollt und sieht in allen Apps gleich aus.
Kann ich den Zurück-Knopf ausblenden?
Ja, mit .navigationBarBackButtonHidden(). Dann musst du aber selbst für einen Weg zurück sorgen, etwa mit @Environment(\.dismiss) (Lektion 2.19). Mach das nur, wenn es einen guten Grund gibt.
Wo gehört der NavigationStack hin, in die App oder in die View?
In die oberste View, die navigiert, meist die Start-View. Bei Apps mit Tabs bekommt jeder Tab einen eigenen NavigationStack (Lektion 3.21).
Fortgeschritten
Warum verhält es sich so?Was ist der Unterschied zu NavigationSplitView?
NavigationSplitView zeigt Liste und Details nebeneinander in Spalten, wie in der Mail-App auf dem iPad. Auf dem iPhone fällt sie automatisch auf eine Stapelnavigation zurück. Für reine iPhone-Apps ist NavigationStack die einfachere Wahl.
Warum sollte das Ziel einer wertbasierten Navigation ein Wert und keine View sein? Werte lassen sich speichern, vergleichen und per Code erzeugen. Dadurch kannst du den ganzen Navigationszustand in einem Array festhalten, beim Neustart wiederherstellen oder per Deep Link direkt zu einem Bildschirm springen (Lektionen 3.20 und 3.22).
Profi
Was passiert unter der Haube?Wie verwaltet NavigationStack den Stapel?
Intern hält er einen Pfad aus Werten. Jeder NavigationLink(value:) hängt einen Wert an, Zurück entfernt den letzten. Für jeden Wert sucht SwiftUI die passende navigationDestination nach dessen Typ. Mit NavigationStack(path: $path) kannst du diesen Pfad selbst besitzen und steuern.
Warum werden Ziele bei NavigationLink { Ziel } sofort erzeugt?
Weil die Closure Teil der Beschreibung der Zeile ist und SwiftUI die Zeile beim Aufbau der Liste auswertet. Die Zielview als Struct zu erzeugen ist zwar günstig, ihr body wird aber erst beim Anzeigen berechnet. Teure Arbeit im init einer Zielview kann trotzdem auffallen, ein weiterer Grund für die wertbasierte Variante.
Übungen
Übung 1: Einstellungsmenü
Baue eine Liste „Einstellungen“ mit drei NavigationLinks: „Profil“, „Benachrichtigungen“ und „Datenschutz“. Jeder Link führt zu einer einfachen View mit passendem Titel und einem Text.
Übung 2: Rezeptbuch
Baue eine Struct Recipe (Identifiable, Hashable) mit Name, Dauer in Minuten und einem Array von Zutaten. Zeig die Rezepte in einer Liste mit Name und Dauer. Ein Tipp öffnet über wertbasierte Navigation eine Detailansicht, die die Zutaten als Liste zeigt.
Lösung Übung 2Erst selbst probieren!
struct Recipe: Identifiable, Hashable {
let id = UUID()
let name: String
let minutes: Int
let ingredients: [String]
}
struct RecipeList: View {
let recipes = [
Recipe(name: "Palatschinken", minutes: 20, ingredients: ["Mehl", "Milch", "Eier", "Salz"]),
Recipe(name: "Kaiserschmarrn", minutes: 30, ingredients: ["Mehl", "Milch", "Eier", "Zucker", "Rosinen"])
]
var body: some View {
NavigationStack {
List(recipes) { recipe in
NavigationLink(value: recipe) {
LabeledContent(recipe.name, value: "\(recipe.minutes) Min.")
}
}
.navigationTitle("Rezepte")
.navigationDestination(for: Recipe.self) { recipe in
List(recipe.ingredients, id: \.self) { ingredient in
Text(ingredient)
}
.navigationTitle(recipe.name)
}
}
}
}NavigationLink(value:) { label } ist die Variante mit eigenem Aussehen.
Übung 3: Bearbeitbare Kontakte
Baue eine Kontaktliste mit einer Contact-Struct (Name, Telefon, Favorit). Ein Tipp öffnet einen Editor mit einem Form, in dem alle drei Werte geändert werden können. Änderungen sollen sofort in der Liste sichtbar sein. Favoriten bekommen in der Liste einen Stern.
Zusammenfassung
NavigationStackstapelt Bildschirme und zeigt eine Navigationsleiste..navigationTitlesteht an der View im Stack..navigationBarTitleDisplayMode(.inline)macht den Titel klein.NavigationLink("Titel") { Ziel }legt eine View auf den Stapel. Zurück funktioniert automatisch.- Wertbasiert:
NavigationLink(value:)und.navigationDestination(for:). Der Typ mussHashablesein. - Nur ein
NavigationStackganz oben, nicht in Detailansichten. - Mit Bindings kann eine Detailansicht Daten der Liste ändern.
Spickzettel
NavigationStack {
List(books) { book in
NavigationLink(book.title, value: book)
}
.navigationTitle("Bücher")
.navigationDestination(for: Book.self) { book in
BookDetail(book: book)
}
}
NavigationLink("Einstellungen") { SettingsView() }
.navigationBarTitleDisplayMode(.inline)
struct Book: Identifiable, Hashable { ... }