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:
@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
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 $:
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 $:
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:
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:
| Was | Besitzer | Kind will lesen | Kind will ändern |
|---|---|---|---|
| Einfacher Wert / Struct | @State private var | let | @Binding var |
@Observable-Klasse | @State private var | let | let (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:
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:
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
@Statedes Sheets. - Bearbeitet wird nur die Kopie.
- Erst „Sichern“ gibt sie über
onSavezurück. _draft = State(initialValue: original)belegt den@Stateim Initializer vor. Das_greift auf den Property Wrapper selbst zu.@escapingist 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
@Bindable var bookmark: BookmarkFehlermeldung
'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
Ü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!
@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")
}
}Übung 2: Wrapper bestimmen
Bestimme für jede Situation den passenden Wrapper und begründe:
- Eine View zeigt die Anzahl der Artikel aus einem
Cart-Objekt an. - Eine Zeile soll das
isDoneeiner Struct-Aufgabe umschalten. - Eine View merkt sich, ob ein Alert sichtbar ist.
- Ein Einstellungsformular bearbeitet direkt Eigenschaften eines
@Observable-Objekts. - Die oberste View erzeugt den
HabitStore.
Lösung Übung 2Erst selbst probieren!
let cart: Cart: nur lesen.@Binding var todo: Todobzw.@Binding var isDone: Bool: Struct ändern.@State private var showAlert = false: gehört nur dieser View.@Bindable var settings: Settings: Bindings auf ein Objekt.@State private var store = HabitStore(): besitzt das Objekt.
Ü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
@Bindableerzeugt Bindings auf Eigenschaften eines@Observable-Objekts:$store.name.- Die besitzende View mit
@Statebraucht kein@Bindable. @Statebesitzt,letliest,@Bindingund@Bindableverbinden.- 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
// 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) }