Lektion 2.20 · Stufe 2 – SwiftUI-Einstieg

Alerts und Dialoge

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du mit .alert Hinweise und Rückfragen zeigen, mit .confirmationDialog Auswahlmöglichkeiten anbieten, Dialoge mit Daten zu einem bestimmten Element verbinden, Fehler verständlich anzeigen und mit .contextMenu Aktionen per langem Drücken anbieten.

Das Problem: Manche Aktionen brauchen eine Rückfrage

In Lektion 2.16 hast du das Löschen per Wischgeste gebaut. Ein Wisch, und der Eintrag ist weg. Bei einer Einkaufsliste ist das in Ordnung. Aber was, wenn der Nutzer alle Einträge löschen will? Oder sein Konto? Ein versehentlicher Tipp, und alles ist verloren.

Für solche Momente gibt es Dialoge: kleine Fenster, die den Nutzer kurz unterbrechen, etwas mitteilen oder eine Entscheidung verlangen.

Sparsam einsetzen

Dialoge unterbrechen. Werden sie zu oft gezeigt, klicken Nutzer sie reflexartig weg, ohne zu lesen. Frag nur nach, wenn eine Aktion nicht rückgängig zu machen ist oder wirklich wichtige Folgen hat.

Der einfachste Alert

Swift
struct ContentView: View {
    @State private var showAlert = false

    var body: some View {
        Button("Info anzeigen") {
            showAlert = true
        }
        .alert("Gespeichert", isPresented: $showAlert) {
            Button("OK") { }
        }
    }
}

Das Muster kennst du von Sheets (Lektion 2.19):

  • Ein @State-Bool bestimmt, ob der Alert sichtbar ist.
  • .alert("Titel", isPresented: $showAlert) zeigt ihn, solange der Wert true ist.
  • In der Closure stehen die Buttons. Ein Tipp auf einen Button schließt den Alert automatisch und setzt showAlert zurück auf false.

Erwartetes Ergebnis

Ein kleines Fenster in der Bildschirmmitte mit dem Titel „Gespeichert“ und einem Button „OK“. Der Hintergrund ist abgedunkelt.

Lässt du die Buttons ganz weg, zeigt SwiftUI automatisch einen „OK“-Button.

Nachricht und mehrere Buttons

Swift
.alert("Alle Einträge löschen?", isPresented: $showDeleteAlert) {
    Button("Löschen", role: .destructive) {
        items.removeAll()
    }
    Button("Abbrechen", role: .cancel) { }
} message: {
    Text("Diese Aktion kann nicht rückgängig gemacht werden.")
}
  • Die zweite Closure message: enthält einen erklärenden Text unter dem Titel.
  • role: .destructive färbt den Button rot (Lektion 2.11).
  • role: .cancel markiert den Abbrechen-Button. SwiftUI setzt ihn an die richtige Stelle und hebt ihn hervor.

Gute Alert-Texte

  • Der Titel ist eine klare Frage oder Aussage: „Alle Einträge löschen?“ statt „Achtung!“.
  • Die Nachricht erklärt die Folgen.
  • Die Buttons sagen, was passiert: „Löschen“ statt „OK“, „Behalten“ statt „Nein“. So muss der Nutzer den Text nicht lesen, um richtig zu entscheiden.

Alerts mit Eingabe

Ein Alert kann sogar ein Textfeld enthalten, etwa zum Umbenennen:

Swift
@State private var showRename = false
@State private var newName = ""

.alert("Liste umbenennen", isPresented: $showRename) {
    TextField("Name", text: $newName)
    Button("Abbrechen", role: .cancel) { }
    Button("Sichern") {
        listName = newName
    }
}

Für mehr als ein, zwei Felder nimm aber besser ein Sheet mit einem Formular.

Alerts für ein bestimmtes Element

Willst du vor dem Löschen eines bestimmten Eintrags nachfragen, musst du dir merken, welcher gemeint ist. Dafür gibt es eine Variante mit presenting::

Swift
struct ShoppingList: View {
    @State private var items = ["Brot", "Milch", "Käse"]
    @State private var itemToDelete: String?
    @State private var showDeleteAlert = false

    var body: some View {
        List(items, id: \.self) { item in
            Text(item)
                .swipeActions {
                    Button("Löschen", role: .destructive) {
                        itemToDelete = item
                        showDeleteAlert = true
                    }
                }
        }
        .alert("Eintrag löschen?", isPresented: $showDeleteAlert, presenting: itemToDelete) { item in
            Button("„\(item)“ löschen", role: .destructive) {
                items.removeAll { $0 == item }
            }
            Button("Abbrechen", role: .cancel) { }
        } message: { item in
            Text("„\(item)“ wird aus deiner Liste entfernt.")
        }
    }
}
  • itemToDelete merkt sich, um welchen Eintrag es geht.
  • presenting: itemToDelete übergibt ihn an die Closures. Dort steht er als item zur Verfügung, schon ausgepackt.
  • So wissen Buttons und Nachricht genau, welcher Eintrag gemeint ist.

Confirmation Dialogs: Auswahl von unten

Ein Confirmation Dialog erscheint auf dem iPhone als Liste von Buttons am unteren Rand. Er eignet sich, wenn der Nutzer zwischen mehreren Aktionen wählt:

Swift
@State private var showOptions = false

Button("Foto ändern") {
    showOptions = true
}
.confirmationDialog("Profilfoto", isPresented: $showOptions, titleVisibility: .visible) {
    Button("Foto aufnehmen") { takePhoto() }
    Button("Aus Mediathek wählen") { pickPhoto() }
    Button("Foto entfernen", role: .destructive) { removePhoto() }
}

Erwartetes Ergebnis

Von unten gleitet ein Feld mit dem Titel „Profilfoto“ und drei Optionen herein. Darunter steht automatisch ein „Abbrechen“-Button.

  • titleVisibility: .visible zeigt den Titel an. Ohne diese Angabe wird er auf dem iPhone oft ausgeblendet.
  • Einen Abbrechen-Button fügt SwiftUI automatisch hinzu.

Alert oder Confirmation Dialog?

  • Alert: eine wichtige Information oder eine Ja/Nein-Entscheidung. Erscheint in der Mitte.
  • Confirmation Dialog: Auswahl zwischen mehreren Aktionen, die der Nutzer selbst ausgelöst hat. Erscheint unten.

Apple verwendet zum Beispiel beim Löschen einer Mail einen Confirmation Dialog, weil der Nutzer die Aktion gerade selbst angestoßen hat.

Fehler anzeigen

In Lektion 1.17 hast du Fehler mit throws und catch behandelt. In einer App muss der Nutzer davon erfahren. Ein bewährtes Muster:

Swift
enum SaveError: LocalizedError {
    case nameEmpty
    case nameTooLong

    var errorDescription: String? {
        switch self {
        case .nameEmpty: "Bitte gib einen Namen ein."
        case .nameTooLong: "Der Name darf höchstens 30 Zeichen haben."
        }
    }
}

struct SaveView: View {
    @State private var name = ""
    @State private var errorMessage: String?

    var body: some View {
        Form {
            TextField("Name", text: $name)
            Button("Sichern") {
                do {
                    try save()
                } catch {
                    errorMessage = error.localizedDescription
                }
            }
        }
        .alert("Fehler", isPresented: .constant(errorMessage != nil)) {
            Button("OK") { errorMessage = nil }
        } message: {
            Text(errorMessage ?? "")
        }
    }

    func save() throws {
        guard !name.isEmpty else { throw SaveError.nameEmpty }
        guard name.count <= 30 else { throw SaveError.nameTooLong }
        // speichern …
    }
}
  • LocalizedError ist ein Protokoll für Fehler mit verständlicher Beschreibung (Lektion 1.17, 🟡-FAQ). Mit errorDescription liefert jeder Fall einen Text.
  • error.localizedDescription liest diesen Text aus.
  • Der Alert ist sichtbar, solange errorMessage nicht nil ist.

Achtung

.constant(errorMessage != nil) ist hier eine Abkürzung. Weil ein konstantes Binding keine Änderungen zurückschreibt, muss der OK-Button errorMessage selbst auf nil setzen. Sonst bleibt der Alert für immer offen. Sauberer ist ein eigenes @State-Bool showError, das du zusammen mit der Nachricht setzt.

Kontextmenüs: langes Drücken

Neben Dialogen gibt es eine weitere Art, Aktionen anzubieten, ohne den Bildschirm zu überladen: das Kontextmenü, das bei langem Drücken erscheint.

Swift
Text(item)
    .contextMenu {
        Button("Kopieren", systemImage: "doc.on.doc") {
            copy(item)
        }
        Button("Favorit", systemImage: "star") {
            favorite(item)
        }
        Divider()
        Button("Löschen", systemImage: "trash", role: .destructive) {
            delete(item)
        }
    }

Drückt der Nutzer länger auf den Eintrag, wird er hervorgehoben, und darunter erscheint das Menü. Kontextmenüs sind ideal für Nebenaktionen, die man nicht ständig braucht.

Typische Fehler

Alert erscheint nicht

Häufige Ursachen: Der Bool wird nie auf true gesetzt, oder der .alert-Modifier hängt an einer View, die gerade nicht sichtbar ist. Wie bei Sheets gilt: Häng ihn an eine View, die immer da ist, etwa an die List statt an eine einzelne Zeile.

Alert in einer Zeile

Swift
ForEach(items, id: \.self) { item in
    Text(item)
        .alert(...)   // ❌ ein Alert pro Zeile
}

Das erzeugt für jede Zeile einen eigenen Alert, und sie kommen sich in die Quere. Nimm einen Alert an der Liste mit presenting:.

Alert schließt nicht

Bei einem .constant(...)-Binding wird der Zustand beim Schließen nicht zurückgesetzt. Setz ihn im Button selbst zurück oder nimm ein echtes @State.

Zu viele Buttons

Mehr als drei Buttons in einem Alert werden unübersichtlich. Für viele Optionen ist ein Confirmation Dialog oder ein Menü besser.

FAQ

Anfänger

Was bedeutet das überhaupt?

Warum schließt sich der Alert automatisch, wenn ich auf einen Button tippe? Das ist Apples Standardverhalten: Jeder Button in einem Alert beendet ihn. Du musst showAlert nicht selbst zurücksetzen.

Kann ich das Aussehen eines Alerts ändern? Nein. Alerts sehen absichtlich überall gleich aus, damit Nutzer sie sofort erkennen. Brauchst du etwas Besonderes, baue ein Sheet mit kleiner Höhe (Lektion 2.19).

Wann frage ich vor dem Löschen nach? Wenn das Löschen nicht rückgängig zu machen ist und Daten verloren gehen, die dem Nutzer wichtig sind. Bei einem einzelnen Eintrag per Wischgeste fragt Apple meist nicht nach. Bei „alles löschen“ immer.

Fortgeschritten

Warum verhält es sich so?

Gibt es eine eingebaute Variante für Fehler? Ja, .alert(isPresented:error:actions:) nimmt direkt einen Fehler, der LocalizedError erfüllt, und zeigt dessen Beschreibung als Titel. Das spart den Umweg über einen String.

Wie biete ich „Rückgängig“ statt einer Rückfrage an? Statt vor dem Löschen zu fragen, kannst du nach dem Löschen kurz einen Hinweis mit „Rückgängig“-Button einblenden und den gelöschten Eintrag so lange aufbewahren. Viele moderne Apps machen das, weil es den Nutzer weniger unterbricht.

Profi

Was passiert unter der Haube?

Warum braucht presenting: zusätzlich isPresented:? Das Datum und die Sichtbarkeit werden getrennt gesteuert, damit der Inhalt beim Ausblenden nicht plötzlich verschwindet: Würde der Alert direkt an itemToDelete hängen und es beim Schließen auf nil gesetzt, wäre der Text während der Ausblend-Animation schon leer. So bleibt der Inhalt bis zum Ende der Animation stabil.

Wie funktioniert die Vorschau im Kontextmenü? Mit .contextMenu { ... } preview: { ... } kannst du statt der hervorgehobenen Zeile eine eigene, größere Vorschau zeigen, wie beim langen Drücken auf einen Link in Safari.

Übungen

Anfänger

Übung 1: Liste leeren

Baue eine Liste mit einem Toolbar-Button „Alle löschen“ (Papierkorb-Symbol). Er öffnet einen Alert mit Titel, Nachricht, einem roten „Alle löschen“-Button und „Abbrechen“. Ist die Liste leer, ist der Toolbar-Button deaktiviert.

Lösung Übung 1Erst selbst probieren!
Swift
struct ClearableList: View {
    @State private var items = ["Brot", "Milch", "Käse"]
    @State private var showClearAlert = false

    var body: some View {
        NavigationStack {
            List(items, id: \.self) { Text($0) }
                .navigationTitle("Einkauf")
                .toolbar {
                    Button("Alle löschen", systemImage: "trash") {
                        showClearAlert = true
                    }
                    .disabled(items.isEmpty)
                }
                .alert("Alle Einträge löschen?", isPresented: $showClearAlert) {
                    Button("Alle löschen", role: .destructive) {
                        items.removeAll()
                    }
                    Button("Abbrechen", role: .cancel) { }
                } message: {
                    Text("Das kann nicht rückgängig gemacht werden.")
                }
        }
    }
}
Fortgeschritten

Übung 2: Teilen-Optionen

Baue eine Fotokarte (ein SF Symbol als Platzhalter) mit einem Button „Optionen“. Er öffnet einen Confirmation Dialog mit „Teilen“, „Als Hintergrundbild“, „Duplizieren“ und „Löschen“ (rot). Zeig unter der Karte, welche Option zuletzt gewählt wurde.

Profi

Übung 3: Validierte Eingabe

Baue ein Formular zum Anlegen eines Benutzernamens. Beim Tippen auf „Sichern“ wird eine Funktion mit throws aufgerufen, die drei Fehler kennt: zu kurz, enthält Leerzeichen, bereits vergeben (wenn der Name „admin“ ist). Zeig jeden Fehler mit einer passenden Meldung in einem Alert. Verwende ein eigenes @State-Bool statt .constant.

Zusammenfassung

  • .alert("Titel", isPresented: $bool) { Buttons } message: { Text } für Hinweise und Rückfragen.
  • Buttons mit role: .destructive und role: .cancel werden automatisch passend gestaltet und angeordnet.
  • presenting: verbindet einen Alert mit einem bestimmten Element.
  • .confirmationDialog für die Auswahl zwischen mehreren Aktionen, von unten.
  • Fehler mit LocalizedError und localizedDescription verständlich anzeigen.
  • .contextMenu bietet Nebenaktionen per langem Drücken.
  • Dialoge sparsam einsetzen, Buttons klar beschriften.

Spickzettel

Swift
.alert("Löschen?", isPresented: $showAlert) {
    Button("Löschen", role: .destructive) { delete() }
    Button("Abbrechen", role: .cancel) { }
} message: {
    Text("Kann nicht rückgängig gemacht werden.")
}

.alert("Löschen?", isPresented: $show, presenting: selected) { item in
    Button("„\(item.name)“ löschen", role: .destructive) { delete(item) }
}

.confirmationDialog("Foto", isPresented: $showOptions, titleVisibility: .visible) {
    Button("Aufnehmen") { }
    Button("Entfernen", role: .destructive) { }
}

.contextMenu {
    Button("Kopieren", systemImage: "doc.on.doc") { }
}

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.