Lektion 3.2 · Stufe 3 – Fortgeschritten

Wem gehören die Daten? @State, Weitergabe, @Bindable

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du für jeden Zustand entscheiden, wo er hingehört und wie er weitergegeben wird. Du kennst @Bindable, mit dem du Bindings auf Eigenschaften einer @Observable-Klasse erzeugst, und hast eine klare Entscheidungshilfe für alle Property Wrapper aus den Stufen 2 und 3.

Das Problem: Ein Textfeld für eine Klasse

In der letzten Lektion hast du einen HabitStore an eine Kind-View als let übergeben. Lesen funktioniert, Methoden aufrufen auch. Jetzt willst du in einer Kind-View aber den Namen einer Einstellung mit einem TextField bearbeiten:

Swift
@Observable
class Settings {
    var userName = ""
    var dailyGoal = 3
}

struct SettingsForm: View {
    let settings: Settings

    var body: some View {
        Form {
            TextField("Name", text: $settings.userName)   // ❌
        }
    }
}

Fehlermeldung

Cannot find '$settings' in scope

TextField braucht ein Binding (Lektion 2.10). Mit $ bekommst du Bindings aber nur von Property Wrappern wie @State. Eine let-Eigenschaft hat kein $.

Die Lösung: @Bindable

Swift
struct SettingsForm: View {
    @Bindable var settings: Settings

    var body: some View {
        Form {
            TextField("Name", text: $settings.userName)
            Stepper("Tagesziel: \(settings.dailyGoal)", value: $settings.dailyGoal, in: 1...10)
        }
    }
}

@Bindable macht aus einem @Observable-Objekt eine Quelle für Bindings. Mit $settings.userName bekommst du ein Binding<String> direkt auf die Eigenschaft. Das Textfeld schreibt bei jeder Eingabe in das Objekt, und alle Views, die userName lesen, aktualisieren sich.

Der Aufruf von außen bleibt gleich, ohne $:

Swift
SettingsForm(settings: settings)

Tipp

@Bindable ändert nichts daran, wem das Objekt gehört. Es ist immer noch dasselbe geteilte Objekt. @Bindable fügt nur die Möglichkeit hinzu, mit $ Bindings auf seine Eigenschaften zu erzeugen.

Und in der View, die das Objekt besitzt?

Dort brauchst du kein @Bindable. @State liefert schon ein $:

Swift
struct RootView: View {
    @State private var settings = Settings()

    var body: some View {
        TextField("Name", text: $settings.userName)   // ✅ funktioniert
    }
}

@Bindable lokal in body

Manchmal hast du ein Objekt nur kurz in der Hand, etwa als Element einer Liste. Dann kannst du @Bindable auch direkt in body verwenden:

Swift
ForEach(store.profiles) { profile in
    @Bindable var profile = profile
    TextField("Name", text: $profile.name)
}

Das setzt voraus, dass die Elemente selbst @Observable-Klassen sind. Bei Structs in einem Array verwendest du stattdessen ForEach($store.items), wie in Lektion 2.15.

Wem gehören die Daten?

Jetzt kennst du alle Werkzeuge für den Datenfluss. Die wichtigste Frage bleibt dieselbe wie in Lektion 2.10: Wem gehört der Wert? Davon hängt ab, wie er angelegt und weitergegeben wird.

Schritt 1: Was ist es für ein Wert?

  • Ein einfacher Wert oder eine Struct, der nur für die Oberfläche einer View zählt: Ist ein Sheet offen? Was steht im Suchfeld? Welcher Tab ist gewählt?
  • Ein Objekt mit Daten und Logik, das mehrere Views teilen: alle Gewohnheiten, der Warenkorb, der angemeldete Benutzer.

Schritt 2: Wo wird er gebraucht?

Leg ihn in der höchsten View an, die ihn braucht, und nicht höher. Ein Sheet-Schalter gehört in die View mit dem Sheet. Der Warenkorb gehört in die App oder die oberste View.

Schritt 3: Wie geben Kind-Views ihn weiter?

Hier ist die vollständige Übersicht:

WasBesitzerKind will lesenKind will ändern
Einfacher Wert / Struct@State private varlet@Binding var
@Observable-Klasse@State private varletlet (Methoden aufrufen) oder @Bindable var (für Bindings)
Viele Views brauchen es.environment(...)@Environment@Environment + @Bindable lokal

Die letzte Zeile ist das Thema der nächsten Lektion.

Merksatz

@State besitzt. let liest. @Binding und @Bindable verbinden. Wenn du dir unsicher bist, fang mit let an und wechsle nur, wenn der Compiler ein $ verlangt.

Ein vollständiges Beispiel

Ein kleiner Lesezeichen-Manager mit allen Varianten:

Swift
struct Bookmark: Identifiable {
    let id = UUID()
    var title: String
    var url: String
}

@Observable
class BookmarkStore {
    var bookmarks: [Bookmark] = []
    var showOnlyFavorites = false

    func add(title: String, url: String) {
        bookmarks.append(Bookmark(title: title, url: url))
    }
}

struct BookmarkApp: View {
    @State private var store = BookmarkStore()         // besitzt das Objekt
    @State private var showAddSheet = false             // besitzt UI-Zustand

    var body: some View {
        NavigationStack {
            List {
                FilterToggle(store: store)               // ändert per Binding
                ForEach($store.bookmarks) { $bookmark in
                    BookmarkRow(bookmark: $bookmark)     // ändert ein Struct-Element
                }
            }
            .navigationTitle("Lesezeichen")
            .toolbar {
                Button("Neu", systemImage: "plus") { showAddSheet = true }
            }
            .sheet(isPresented: $showAddSheet) {
                AddBookmarkSheet(store: store)           // ruft Methoden auf
            }
        }
    }
}

struct FilterToggle: View {
    @Bindable var store: BookmarkStore

    var body: some View {
        Toggle("Nur Favoriten", isOn: $store.showOnlyFavorites)
    }
}

struct BookmarkRow: View {
    @Binding var bookmark: Bookmark

    var body: some View {
        TextField("Titel", text: $bookmark.title)
    }
}

struct AddBookmarkSheet: View {
    let store: BookmarkStore
    @Environment(\.dismiss) private var dismiss
    @State private var title = ""                        // Entwurf, gehört nur dem Sheet

    var body: some View {
        NavigationStack {
            Form {
                TextField("Titel", text: $title)
            }
            .toolbar {
                Button("Sichern") {
                    store.add(title: title, url: "")
                    dismiss()
                }
            }
        }
    }
}

Schau dir jede Zeile mit Kommentar an und überleg, warum dort genau dieser Wrapper steht. Wenn du für jede eine Begründung findest, hast du den Datenfluss in SwiftUI verstanden.

Entwürfe: Wann eine Kopie besser ist

Ein wichtiger Sonderfall: Bearbeitungsformulare mit „Abbrechen“. Bindest du ein Formular direkt an die echten Daten, sind Änderungen sofort gespeichert. „Abbrechen“ kann sie nicht mehr rückgängig machen.

Die Lösung ist ein Entwurf:

Swift
struct EditBookmarkSheet: View {
    let original: Bookmark
    let onSave: (Bookmark) -> Void
    @Environment(\.dismiss) private var dismiss
    @State private var draft: Bookmark

    init(original: Bookmark, onSave: @escaping (Bookmark) -> Void) {
        self.original = original
        self.onSave = onSave
        _draft = State(initialValue: original)
    }

    var body: some View {
        NavigationStack {
            Form {
                TextField("Titel", text: $draft.title)
                TextField("Adresse", text: $draft.url)
            }
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Abbrechen") { dismiss() }
                }
                ToolbarItem(placement: .confirmationAction) {
                    Button("Sichern") {
                        onSave(draft)
                        dismiss()
                    }
                }
            }
        }
    }
}
  • Der Entwurf ist eine Kopie der Struct im eigenen @State des Sheets.
  • Bearbeitet wird nur die Kopie.
  • Erst „Sichern“ gibt sie über onSave zurück.
  • _draft = State(initialValue: original) belegt den @State im Initializer vor. Das _ greift auf den Property Wrapper selbst zu.
  • @escaping ist nötig, weil die Closure gespeichert und später aufgerufen wird (Lektion 1.11, 🔴-FAQ).

Weil Structs kopiert werden (Lektion 1.13), ist das hier ganz natürlich. Bei einer Klasse müsstest du die Werte einzeln kopieren.

Tipp

Faustregel: Sofort übernehmen (direktes Binding) für Einstellungen und Schalter, wie in den iOS-Einstellungen. Entwurf mit Sichern/Abbrechen für Formulare, die einen Datensatz anlegen oder bearbeiten.

Typische Fehler

$ bei einer let-Eigenschaft

Fehlermeldung

Cannot find '$store' in scope

Die Kind-View braucht Bindings, bekommt das Objekt aber als let. Mach daraus @Bindable var.

@Bindable bei einer Struct

Swift
@Bindable var bookmark: Bookmark

Fehlermeldung

'init(wrappedValue:)' is unavailable: The wrapped value must be an object that conforms to Observable

@Bindable gibt es nur für @Observable-Klassen. Für Structs nimmst du @Binding.

@Binding bei einer Klasse

@Binding var store: BookmarkStore funktioniert technisch, ist aber unnötig umständlich. Du würdest eine Verbindung auf einen Verweis übergeben. Bei Klassen reicht let oder @Bindable.

Abbrechen ändert trotzdem

Das Formular ist direkt an die echten Daten gebunden. Arbeite mit einem Entwurf.

FAQ

Anfänger

Was bedeutet das überhaupt?

Muss ich mir die Tabelle auswendig merken? Nein. Mit der Zeit entwickelst du ein Gefühl dafür. Bis dahin hilft der Merksatz und der Compiler: Er meldet sich, wenn du ein $ brauchst, das es nicht gibt.

Ist es schlimm, wenn ein Zustand zu hoch angelegt ist? Nicht dramatisch, aber unordentlich. Ein Sheet-Schalter in der App-Struct würde funktionieren, wäre aber schwer zu finden, und jede Änderung beträfe mehr Views als nötig. Halte Zustand so nah wie möglich an der Stelle, wo er gebraucht wird.

Fortgeschritten

Warum verhält es sich so?

Warum gibt es @Bindable überhaupt, wenn Klassen ohnehin geteilt werden? Weil SwiftUI-Elemente wie TextField und Toggle mit Bindings arbeiten und nicht mit Objekten. @Bindable ist die Brücke: Es erzeugt aus einem Objekt und einem Key Path ein Binding, dessen Getter und Setter auf die Eigenschaft des Objekts zugreifen.

Wie bearbeite ich ein Element einer Liste, wenn die Liste in einer Klasse liegt? Mit ForEach($store.items) { $item in ... } in der View, die das Objekt mit @State oder @Bindable hält. Daraus bekommst du Bindings auf die einzelnen Struct-Elemente.

Profi

Was passiert unter der Haube?

Was passiert bei _draft = State(initialValue:), wenn sich original später ändert? Nichts. @State übernimmt den Startwert nur beim ersten Erzeugen der View-Identität (Lektion 2.9, 🔴-FAQ). Für ein Sheet ist das genau richtig: Der Entwurf wird beim Öffnen einmal angelegt. Soll sich der Entwurf an spätere Änderungen anpassen, bräuchtest du .onChange(of: original) oder eine neue Identität mit .id(original.id).

Wann @Bindable und wann einzelne @Bindings? Braucht eine Kind-View nur ein oder zwei Werte, sind einzelne @Bindings oft besser: Die View bleibt unabhängig von der konkreten Klasse und lässt sich leichter wiederverwenden und testen. @Bindable lohnt sich, wenn die View eng zu diesem Objekt gehört und viele seiner Eigenschaften bearbeitet.

Übungen

Anfänger

Übung 1: Profil-Editor

Baue eine @Observable-Klasse UserProfile mit Name, Bio und einem Bool für ein öffentliches Profil. Eine ProfileView besitzt das Objekt und zeigt die Werte an. Eine ProfileEditor-View bekommt es mit @Bindable und ändert alles in einem Form.

Lösung Übung 1Erst selbst probieren!
Swift
@Observable
class UserProfile {
    var name = "Anna"
    var bio = ""
    var isPublic = true
}

struct ProfileView: View {
    @State private var profile = UserProfile()

    var body: some View {
        NavigationStack {
            VStack(spacing: 8) {
                Text(profile.name).font(.title)
                Text(profile.bio.isEmpty ? "Keine Bio" : profile.bio)
                Text(profile.isPublic ? "Öffentlich" : "Privat")
                    .foregroundStyle(.secondary)
                NavigationLink("Bearbeiten") {
                    ProfileEditor(profile: profile)
                }
            }
        }
    }
}

struct ProfileEditor: View {
    @Bindable var profile: UserProfile

    var body: some View {
        Form {
            TextField("Name", text: $profile.name)
            TextField("Bio", text: $profile.bio, axis: .vertical)
            Toggle("Öffentlich", isOn: $profile.isPublic)
        }
        .navigationTitle("Profil")
    }
}
Fortgeschritten

Übung 2: Wrapper bestimmen

Bestimme für jede Situation den passenden Wrapper und begründe:

  1. Eine View zeigt die Anzahl der Artikel aus einem Cart-Objekt an.
  2. Eine Zeile soll das isDone einer Struct-Aufgabe umschalten.
  3. Eine View merkt sich, ob ein Alert sichtbar ist.
  4. Ein Einstellungsformular bearbeitet direkt Eigenschaften eines @Observable-Objekts.
  5. Die oberste View erzeugt den HabitStore.
Lösung Übung 2Erst selbst probieren!
  1. let cart: Cart: nur lesen.
  2. @Binding var todo: Todo bzw. @Binding var isDone: Bool: Struct ändern.
  3. @State private var showAlert = false: gehört nur dieser View.
  4. @Bindable var settings: Settings: Bindings auf ein Objekt.
  5. @State private var store = HabitStore(): besitzt das Objekt.
Profi

Übung 3: Bearbeiten mit Entwurf

Nimm den Lesezeichen-Manager und ergänze für jedes Lesezeichen ein Bearbeiten-Sheet mit Entwurf, Sichern und Abbrechen. Beim Sichern ersetzt eine Methode update(_:) im Store das passende Element anhand seiner id.

Zusammenfassung

  • @Bindable erzeugt Bindings auf Eigenschaften eines @Observable-Objekts: $store.name.
  • Die besitzende View mit @State braucht kein @Bindable.
  • @State besitzt, let liest, @Binding und @Bindable verbinden.
  • Zustand so nah wie möglich an der Stelle anlegen, wo er gebraucht wird.
  • Für Formulare mit „Abbrechen“: mit einem Entwurf arbeiten, einer Kopie im eigenen @State.

Spickzettel

Swift
// Besitzer
@State private var store = Store()
TextField("Name", text: $store.name)

// Kind, das Bindings braucht
struct Editor: View {
    @Bindable var store: Store
    var body: some View { TextField("Name", text: $store.name) }
}

// Lokal in body
@Bindable var item = item

// Entwurf
@State private var draft: Item
init(item: Item) { _draft = State(initialValue: item) }

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.