Swiftlang
Art
Lektion 12 von 12
Niveau
Fortgeschritten
Lesezeit
etwa 5 Minuten
Stand

Deine erste SwiftUI-App

SwiftUI beschreibt Oberflächen deklarativ: Du sagst, wie die Ansicht für den aktuellen Zustand aussehen soll, und SwiftUI aktualisiert sie, wenn sich der Zustand ändert. Den Zustand hält eine View mit @State. Eine Unteransicht, die ihn ändern darf, bekommt mit @Binding eine Verbindung dazu, übergeben mit einem Dollarzeichen. Für SwiftUI-Apps brauchst du einen Mac mit Xcode.

Danach kannst du

  • verstehst, wie eine SwiftUI-View aus body und Zustand entsteht
  • hältst Zustand mit @State und gibst ihn mit @Binding weiter
  • baust eine Liste mit Eingabe, Umschalter und Löschen
  • weißt, welche Teile Swift und welche SwiftUI sind

Umgebung

Swift 6 mit aktueller Xcode-Version
iOS 16 oder neuer (wegen NavigationStack), entwickelt in Xcode auf macOS

Dieser Code wurde nicht ausgeführt. Auf Swiftlang steht derzeit kein Mac mit Xcode für Prüfungen bereit. Wir haben jede verwendete API und ihre Mindestversion in der Apple-Dokumentation nachgeschlagen (Stand 19.10.2026).

Bevor du anfängst

Diese Lektion ist anders als die übrigen auf Swiftlang: Der Code wurde nicht ausgeführt. SwiftUI ist ein Framework von Apple und läuft nur auf Apple-Plattformen, und für diese Prüfung stand uns kein Mac mit Xcode zur Verfügung. Wir haben jede verwendete API mit ihrer Mindestversion in der Apple-Dokumentation nachgeschlagen und den Code sorgfältig gelesen. Wenn bei dir etwas nicht kompiliert, schreib uns bitte über die Kontaktseite.

Du brauchst:

  • einen Mac mit einer aktuellen Version von Xcode,
  • die Grundlagen aus dem Lernpfad, vor allem Structs, Protokolle und Closures.

Ein kostenpflichtiges Entwicklerkonto brauchst du zum Lernen nicht. Die App läuft im Simulator und in der Vorschau von Xcode.

Swift und SwiftUI auseinanderhalten

Alles, was du bisher gelernt hast, ist Swift. SwiftUI ist eine Bibliothek, geschrieben für Swift, die dir Bausteine für Oberflächen gibt: Text, Button, List, TextField und viele mehr. Du erkennst SwiftUI-Code an import SwiftUI und daran, dass Views Structs sind, die das Protokoll View erfüllen. Die Sprachmittel darin kennst du: Structs, berechnete Eigenschaften, Closures, Protokolle mit some.

Das Projekt anlegen

Lege in Xcode ein neues Projekt mit der Vorlage App für iOS an, als Oberfläche SwiftUI und als Sprache Swift. Die genauen Menüs unterscheiden sich zwischen Xcode-Versionen etwas; Apples SwiftUI-Tutorials zeigen den aktuellen Weg mit Bildern. Xcode erzeugt zwei Dateien, die du gleich ersetzt.

Die erste ist der Einstiegspunkt der App:

LernlisteApp.swiftNicht ausgeführt SwiftUI braucht Xcode auf einem Mac. Quelltext gegen die Apple-Dokumentation geprüft.
import SwiftUI

@main
struct LernlisteApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

@main markiert, wo die App startet. WindowGroup ist das Fenster, auf dem iPhone der ganze Bildschirm. Darin steht die erste View.

Die erste View

Ersetze den Inhalt von ContentView.swift durch diese kleine Lernliste:

ContentView.swiftNicht ausgeführt SwiftUI braucht Xcode auf einem Mac. Quelltext gegen die Apple-Dokumentation geprüft.
import SwiftUI

struct Aufgabe: Identifiable {
    let id = UUID()
    var titel: String
    var erledigt = false
}

struct ContentView: View {
    @State private var aufgaben: [Aufgabe] = [
        Aufgabe(titel: "Swift installieren", erledigt: true),
        Aufgabe(titel: "Optionals verstehen"),
    ]
    @State private var neuerTitel = ""

    var body: some View {
        NavigationStack {
            List {
                Section {
                    HStack {
                        TextField("Neue Aufgabe", text: $neuerTitel)
                            .onSubmit(hinzufuegen)
                        Button("Hinzufügen", action: hinzufuegen)
                            .disabled(bereinigterTitel.isEmpty)
                    }
                }
                Section("Offen: \(anzahlOffen)") {
                    ForEach($aufgaben) { $aufgabe in
                        AufgabeZeile(aufgabe: $aufgabe)
                    }
                    .onDelete { aufgaben.remove(atOffsets: $0) }
                }
            }
            .navigationTitle("Lernliste")
        }
    }

    private var bereinigterTitel: String {
        neuerTitel.trimmingCharacters(in: .whitespaces)
    }

    private var anzahlOffen: Int {
        aufgaben.filter { !$0.erledigt }.count
    }

    private func hinzufuegen() {
        guard !bereinigterTitel.isEmpty else { return }
        aufgaben.append(Aufgabe(titel: bereinigterTitel))
        neuerTitel = ""
    }
}

struct AufgabeZeile: View {
    @Binding var aufgabe: Aufgabe

    var body: some View {
        Toggle(aufgabe.titel, isOn: $aufgabe.erledigt)
    }
}

#Preview {
    ContentView()
}

#Preview zeigt die View direkt in Xcode neben dem Code an. Wenn du etwas änderst, aktualisiert sich die Vorschau. In ihr kannst du die Liste schon benutzen: Aufgaben eintippen, abhaken, nach links wischen zum Löschen.

Wie die Daten fließen

Datenfluss zwischen ContentView und AufgabeZeile ContentView besitzt den Zustand aufgaben mit @State. Über $aufgabe bekommt jede AufgabeZeile eine Bindung. Ein Tipp auf den Umschalter ändert erledigt im Zustand von ContentView, worauf SwiftUI beide Views neu zeichnet. ContentView @State aufgaben besitzt die Daten AufgabeZeile @Binding aufgabe liest und schreibt mit $aufgabe Ein Tipp auf den Umschalter ändert den Zustand in ContentView. SwiftUI zeichnet daraufhin alle Views neu, die ihn verwenden, auch „Offen: 1“.
Die Daten gehören einer View. Andere Views bekommen eine Bindung und keine Kopie.

@State macht aufgaben und neuerTitel zu Zustand, den SwiftUI für diese View verwaltet. Ändert sich der Wert, berechnet SwiftUI body neu und aktualisiert die betroffenen Teile des Bildschirms. Apple empfiehlt, @State als private zu deklarieren und in der höchsten View anzulegen, die den Wert braucht (Dokumentation zu State). Laut derselben Seite setzt SwiftUI @State beim Bauen mit Xcode 27 oder neuer als Makro um; an der Schreibweise in deinem Code ändert sich dadurch nichts.

$neuerTitel ist eine Bindung, eine Verbindung zum Zustand. TextField schreibt jeden Tastendruck über sie direkt in neuerTitel.

@Binding in AufgabeZeile heißt: Diese View besitzt die Aufgabe nicht, sie bekommt nur eine Verbindung zu einer Aufgabe, die jemand anderem gehört. Der Toggle schreibt über $aufgabe.erledigt direkt in das Array von ContentView. Deshalb ändert sich die Zahl „Offen“ sofort mit.

ForEach($aufgaben) { $aufgabe in … } läuft über das Array und liefert für jedes Element gleich eine Bindung. Identifiable mit der id sorgt dafür, dass SwiftUI jede Zeile wiedererkennt, auch wenn Elemente hinzukommen oder verschwinden.

Erinnere dich an die Lektion zu Structs und Klassen: Aufgabe ist ein Struct, ein Werttyp. Ohne Bindung bekäme AufgabeZeile eine Kopie, und das Abhaken würde nur die Kopie ändern. Die Bindung ist genau die Brücke, die das verhindert.

Was im Code noch steckt

  • some View ist ein Rückgabetyp aus der Lektion zu Protokollen: „irgendeine bestimmte View“. Der genaue Typ wäre lang und ändert sich mit jeder Zeile, die du in body schreibst.
  • Modifier wie .navigationTitle, .disabled und .onDelete sind Methoden, die eine neue, veränderte View zurückgeben. Die Reihenfolge kann eine Rolle spielen.
  • Berechnete Eigenschaften wie anzahlOffen werden bei jedem neuen body frisch berechnet. Speichere nichts doppelt, was sich aus dem Zustand ableiten lässt.
  • trimmingCharacters(in:) kommt aus Foundation, das SwiftUI mitbringt. Damit lassen sich keine leeren Aufgaben aus Leerzeichen anlegen.

Häufige Fehler

cannot assign to property: 'self' is immutable. Du änderst in einer View eine normale Eigenschaft. Views sind Structs, und body darf sie nicht verändern; ihr Zustand gehört in @State. Diese Meldung kommt vom Swift-Compiler selbst; wir haben sie an einem normalen Struct geprüft (Fehlermeldungen).

Die Anzeige ändert sich nicht. Der Wert liegt nicht in @State oder einer Bindung, sondern in einer normalen Eigenschaft oder einer Kopie. Prüfe, wem die Daten gehören.

Eine Bindung wird nicht gefunden (sinngemäß „cannot find … in scope“). Bei ForEach fehlt das $ vor aufgaben oder vor dem Parameternamen im Closure.

Übungen

  1. Füge unter der Liste eine Schaltfläche „Erledigte entfernen“ hinzu, die alle abgehakten Aufgaben löscht. Tipp: removeAll(where:).
  2. Zeige in AufgabeZeile erledigte Aufgaben durchgestrichen an. Tipp: Text hat den Modifier .strikethrough(_:); du brauchst dann einen eigenen Aufbau statt Toggle(_:isOn:) mit Titel.
  3. Speichere die Aufgaben dauerhaft. Das geht über diese Lektion hinaus. Ein guter Ausgangspunkt ist Apples Dokumentation zu SwiftData.