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
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 Werttrueist.- In der Closure stehen die Buttons. Ein Tipp auf einen Button schließt den Alert automatisch und setzt
showAlertzurück auffalse.
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
.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: .destructivefärbt den Button rot (Lektion 2.11).role: .cancelmarkiert 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:
@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::
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.")
}
}
}itemToDeletemerkt sich, um welchen Eintrag es geht.presenting: itemToDeleteübergibt ihn an die Closures. Dort steht er alsitemzur 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:
@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: .visiblezeigt 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:
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 …
}
}LocalizedErrorist ein Protokoll für Fehler mit verständlicher Beschreibung (Lektion 1.17, 🟡-FAQ). MiterrorDescriptionliefert jeder Fall einen Text.error.localizedDescriptionliest diesen Text aus.- Der Alert ist sichtbar, solange
errorMessagenichtnilist.
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.
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
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
Ü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!
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.")
}
}
}
}Ü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.
Ü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: .destructiveundrole: .cancelwerden automatisch passend gestaltet und angeordnet. presenting:verbindet einen Alert mit einem bestimmten Element..confirmationDialogfür die Auswahl zwischen mehreren Aktionen, von unten.- Fehler mit
LocalizedErrorundlocalizedDescriptionverständlich anzeigen. .contextMenubietet Nebenaktionen per langem Drücken.- Dialoge sparsam einsetzen, Buttons klar beschriften.
Spickzettel
.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") { }
}