Lektion 2.17 · Stufe 2 – SwiftUI-Einstieg

Navigation mit NavigationStack

ca. 15 Minuten Lesezeit

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.

Swift
struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                Text("Erste Zeile")
                Text("Zweite Zeile")
            }
            .navigationTitle("Start")
        }
    }
}
  • NavigationStack ist 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 der List, nicht am NavigationStack selbst.

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.

Swift
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.

Wie bei Button (Lektion 2.11) kannst du das Aussehen frei gestalten:

Swift
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:

Swift
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“.

Screenshot folgt: Bücherliste und Detailansicht nebeneinander

Ein paar Details:

  • BookDetail bekommt das Buch als ganz normale Eigenschaft (Lektion 2.2). Sie zeigt es nur an, also reicht let.
  • .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:

Swift
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 ein Book auf dem Stapel, zeig dafür diese View.“

Damit das funktioniert, muss Book Hashable sein (Lektion 1.15):

Swift
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:

Swift
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).

Swift
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

Swift
NavigationStack {
    List { ... }
}
.navigationTitle("Start")

Der Titel steht am NavigationStack statt an der View darin. Verschieb ihn an die List.

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.

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

Anfänger

Ü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.

Fortgeschritten

Ü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!
Swift
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.

Profi

Ü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

  • NavigationStack stapelt Bildschirme und zeigt eine Navigationsleiste.
  • .navigationTitle steht 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 muss Hashable sein.
  • Nur ein NavigationStack ganz oben, nicht in Detailansichten.
  • Mit Bindings kann eine Detailansicht Daten der Liste ändern.

Spickzettel

Swift
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 { ... }

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.