SwiftUI @State, @Binding und @Observable: Datenfluss erklärt
7 Min. Lesezeit

In SwiftUI ist der Bildschirm eine Funktion Ihrer Daten: Die Daten ändern sich, body wird neu ausgewertet, die Anzeige aktualisiert sich. Die eigentliche Frage lautet deshalb nie „Wie aktualisiere ich die View?“, sondern „Wem gehören diese Daten?“. @State, @Binding, @Observable, @Bindable und @Environment sind fünf verschiedene Antworten darauf. Wählen Sie die falsche, schweigt der Compiler meistens, und der Bildschirm antwortet mit Feldern, die sich selbst zurücksetzen, oder mit Texten, die sich nie ändern.
Dieser Beitrag behandelt zuerst den aktuellen Ansatz (das Observation-Framework), danach die ObservableObject-Familie, die Ihnen in vielen Tutorials und Codebasen weiterhin begegnet, die direkte Zuordnung zwischen beiden und die Fehler, die ich am häufigsten sehe. Wenn Views für Sie noch neu sind, beginnen Sie mit der ersten App mit SwiftUI.
@State: Daten, die der View gehören
@State ist für kleine Werte gedacht, die nur eine einzige View betreffen: ein Zähler, der Zustand eines Schalters, der Entwurfstext eines Feldes. Eine View ist ein struct, das bei jeder Aktualisierung neu erzeugt wird, und kann den Wert daher nicht in sich selbst aufbewahren. @State verlagert den Wert in einen von SwiftUI verwalteten Speicher und hält ihn am Leben, während das View-Struct kommt und geht.
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 12) {
Text("Zähler: \(count)")
Button("Erhöhen") { count += 1 }
}
}
}Machen Sie private zur Gewohnheit. @State bedeutet, dass Sie Eigentümer der Daten sind; eine @State-Eigenschaft, die von außen gesetzt werden kann, ist fast immer ein Zeichen für das falsche Werkzeug.
@Binding: Daten ändern, die jemand anderem gehören
Soll eine Kind-View einen Wert lesen und ändern, der aber der Eltern-View gehört, verwenden Sie @Binding. Ein Binding ist keine Kopie, sondern eine Verbindung in beide Richtungen zum Wert beim Eigentümer. Die Eltern-View erzeugt es mit dem Präfix $.
struct NotificationToggle: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Mitteilungen", isOn: $isOn)
}
}
struct SettingsView: View {
@State private var notificationsEnabled = true
var body: some View {
Form {
NotificationToggle(isOn: $notificationsEnabled)
Text(notificationsEnabled ? "An" : "Aus")
}
}
}Zeigt die Kind-View den Wert nur an, verzichten Sie auf das Binding und übergeben einen einfachen let-Parameter. Betrachten Sie ein Binding als Schreibrecht und vergeben Sie es nur dort, wo es gebraucht wird.
@Observable: ein Modell, das mehrere Bildschirme teilen
Sobald die Daten über einen einzelnen Wert hinauswachsen, etwa ein Warenkorb, eine Sitzung oder eine Liste, wandern sie in eine Klasse. Mit dem Observation-Framework genügt dafür @Observable vor der Klasse. Verfügbar ist es ab iOS 17.
import Observation
struct CartItem: Identifiable {
let id = UUID()
var name: String
var price: Double
var quantity = 1
}
@Observable
final class CartModel {
var items: [CartItem] = []
var note = ""
var total: Double {
items.reduce(0) { $0 + $1.price * Double($1.quantity) }
}
func add(_ item: CartItem) {
items.append(item)
}
}Ein @Published gibt es hier nicht. Das Makro macht jede gespeicherte Eigenschaft beobachtbar, und SwiftUI merkt sich, welche Eigenschaften eine View in body tatsächlich liest. Ändert sich note, werden nur die Views neu gezeichnet, die note lesen. Warum das Modell eine class und kein struct ist, erklärt der Beitrag zu struct und class ausführlich: Mehrere Bildschirme müssen hier dasselbe Objekt teilen, wir wollen also Referenzsemantik.
Die View, die das Modell erzeugt, hält es mit @State. Eine View, die das Modell nur entgegennimmt, braucht gar keinen Wrapper:
struct CartScreen: View {
@State private var cart = CartModel()
var body: some View {
List {
ForEach(cart.items) { item in
Text(item.name)
}
CartNoteField(cart: cart)
CartTotalRow(cart: cart)
}
}
}
struct CartTotalRow: View {
let cart: CartModel
var body: some View {
Text("Summe: \(cart.total, format: .number.precision(.fractionLength(2)))")
}
}@Bindable: Bindings auf Eigenschaften des Modells
TextField und Toggle erwarten ein Binding. Um es mit $ aus einer Eigenschaft eines @Observable-Modells zu erzeugen, kennzeichnen Sie das Modell mit @Bindable:
struct CartNoteField: View {
@Bindable var cart: CartModel
var body: some View {
TextField("Bestellnotiz", text: $cart.note)
}
}@Environment: das Modell durch den Baum reichen
Statt das Modell durch fünf Ebenen von Initialisierern zu schleusen, legen Sie es in die Umgebung. An der Wurzel wird es mit .environment(_:) eingesetzt und dort, wo es gebraucht wird, über den Typ gelesen:
@main
struct ShopApp: App {
@State private var cart = CartModel()
var body: some Scene {
WindowGroup {
CartBadge()
.environment(cart)
}
}
}
struct CartBadge: View {
@Environment(CartModel.self) private var cart
var body: some View {
Text("\(cart.items.count) Artikel im Warenkorb")
}
}Brauchen Sie ein Binding auf ein Modell aus der Umgebung, deklarieren Sie in body ein lokales @Bindable:
struct CheckoutNote: View {
@Environment(CartModel.self) private var cart
var body: some View {
@Bindable var cart = cart
TextField("Bestellnotiz", text: $cart.note)
}
}Der ältere Ansatz: die ObservableObject-Familie
Projekte, die Versionen vor iOS 17 unterstützen, und die meisten älteren Tutorials arbeiten mit dem Combine-basierten Aufbau. Die Idee ist dieselbe, die Namen unterscheiden sich:
final class LegacyCartModel: ObservableObject {
@Published var items: [CartItem] = []
@Published var note = ""
}
struct LegacyCartScreen: View {
@StateObject private var cart = LegacyCartModel() // Eigentümer
var body: some View {
LegacyCartSummary(cart: cart)
.environmentObject(cart)
}
}
struct LegacyCartSummary: View {
@ObservedObject var cart: LegacyCartModel // kein Eigentümer
var body: some View {
Text("\(cart.items.count) Artikel im Warenkorb")
}
}
struct LegacyCartBadge: View {
@EnvironmentObject private var cart: LegacyCartModel
var body: some View {
Text("\(cart.items.count)")
}
}Zwei Unterschiede sind wichtig. Erstens macht bei ObservableObject die Änderung irgendeiner @Published-Eigenschaft jede View ungültig, die das Objekt beobachtet, während Observation pro Eigenschaft verfolgt. Zweitens sind Eigentum und Beobachtung auf der alten Seite getrennte Wrapper (@StateObject und @ObservedObject), und wer sie verwechselt, erzeugt den klassischen Fehler, der weiter unten beschrieben ist.
| Situation | Observation (ab iOS 17) | Älterer Ansatz |
|---|---|---|
| Einfacher Wert, nur für eine View | @State |
@State |
| Kind ändert den Wert der Eltern-View | @Binding |
@Binding |
| View erzeugt das Modell und besitzt es | @State + @Observable-Klasse |
@StateObject |
| View erhält das Modell und liest es | einfaches let / var |
@ObservedObject |
$-Binding auf ein erhaltenes Modell |
@Bindable |
@ObservedObject |
| App-weit geteiltes Modell | .environment(_:) + @Environment(Typ.self) |
.environmentObject(_:) + @EnvironmentObject |
| Beobachtete Eigenschaft im Modell | keine Kennzeichnung nötig | @Published |
Wann verwenden – und wann nicht?
Bei einem neuen Projekt mit iOS 17 oder neuer als Mindestziel beginnen Sie mit Observation: weniger Code und weniger unnötiges Neuzeichnen. Müssen Sie eine ältere Version unterstützen, funktioniert ObservableObject weiterhin einwandfrei, und beide können im selben Projekt nebeneinander bestehen, sodass Sie Bildschirm für Bildschirm umstellen können.
Alles in ein Modell zu verschieben ist ebenfalls ein Fehler. Ob ein Sheet angezeigt wird, der Fokus eines Feldes oder ein Animations-Flag gehören zur View und bleiben @State. Umgekehrt gilt: Zeigen zwei Bildschirme dieselben Daten, halten Sie nicht in jedem ein eigenes @State, sondern verlagern Sie die Daten zu einem einzigen Eigentümer. Reservieren Sie @Environment für Modelle, die wirklich breit genutzt werden. Wer alles dort ablegt, versteckt Abhängigkeiten, und eine View, die ein nie eingesetztes Modell liest, stürzt zur Laufzeit ab.
Häufige Fehler
1. Das Objekt mit @ObservedObject erzeugen
struct BadProfileView: View {
@ObservedObject var model = LegacyCartModel() // falsch
var body: some View { Text(model.note) }
}Symptom: Bei jedem Neuzeichnen der Eltern-View entsteht das Modell von Grund auf neu; eingegebene Daten verschwinden, die Netzwerkanfrage startet erneut. @ObservedObject speichert das Objekt nicht, es beobachtet es nur. Lösung: @StateObject in der View, die das Objekt erzeugt, oder @State, wenn Sie Observation verwenden.
2. Einen Wert der Eltern-View in @State kopieren
struct NameEditorBad: View {
@State private var name: String
init(name: String) {
_name = State(initialValue: name)
}
var body: some View {
TextField("Name", text: $name)
}
}Symptom: Der Name ändert sich in der Eltern-View, das Feld zeigt aber weiter den alten Wert, und Eingaben im Feld erreichen die Eltern-View nie. @State verwendet seinen Anfangswert nur beim ersten Erscheinen der View. Lösung: Gehören die Daten der Eltern-View, deklarieren Sie @Binding var name: String und übergeben $name.
3. State in einer kurzlebigen View halten
Eine View in einem if-Zweig verlässt den Baum, sobald die Bedingung falsch wird, und ihr @State wird mit ihr verworfen. Symptom: Sie klappen einen Detailbereich zu und wieder auf, und die eingetippte Notiz ist weg. Dasselbe passiert bei Views, deren .id(...)-Wert sich ändert. Lösung: Verlagern Sie den Wert, der erhalten bleiben muss, in eine Eltern-View, die auf dem Bildschirm bleibt, und reichen Sie ein Binding nach unten.
4. @Bindable vergessen
struct CartNoteFieldBad: View {
let cart: CartModel
var body: some View {
TextField("Notiz", text: $cart.note) // Fehler
}
}Symptom: Der Compiler meldet cannot find '$cart' in scope. Lösung: @Bindable var cart statt let cart. Ein weiteres Detail: In @State private var model = Model() läuft der Ausdruck Model() bei jeder Erzeugung des View-Structs, SwiftUI behält nur die erste Instanz. Erledigen Sie deshalb keine aufwendige Arbeit im init des Modells, sondern laden Sie Daten in .task. Wie das geht, zeigt der Leitfaden zu async/await.
Mini-Szenario: eine Aufgabenliste
Sehen wir uns alle fünf Werkzeuge auf einem Bildschirm an. Die Aufgaben liegen in einem app-weit geteilten TodoStore; der Listenbildschirm liest ihn aus der Umgebung, und das Sheet zum Hinzufügen hält seinen eigenen Entwurf in @State.
struct TodoItem: Identifiable {
let id = UUID()
var title: String
var isDone = false
}
@Observable
final class TodoStore {
var items: [TodoItem] = []
var remainingCount: Int {
items.filter { !$0.isDone }.count
}
func add(title: String) {
let trimmed = title.trimmingCharacters(in: .whitespaces)
guard !trimmed.isEmpty else { return }
items.append(TodoItem(title: trimmed))
}
}
struct TodoListScreen: View {
@Environment(TodoStore.self) private var store
@State private var isAdding = false
var body: some View {
@Bindable var store = store
NavigationStack {
List($store.items) { $item in
Toggle(item.title, isOn: $item.isDone)
}
.navigationTitle("Offen: \(store.remainingCount)")
.toolbar {
Button("Hinzufügen") { isAdding = true }
}
.sheet(isPresented: $isAdding) {
AddTodoSheet(isPresented: $isAdding)
}
}
}
}
struct AddTodoSheet: View {
@Environment(TodoStore.self) private var store
@Binding var isPresented: Bool
@State private var draft = ""
var body: some View {
Form {
TextField("Aufgabe", text: $draft)
Button("Sichern") {
store.add(title: draft)
isPresented = false
}
.disabled(draft.isEmpty)
}
}
}Wem gehört was? isAdding gehört dem Listenbildschirm, draft dem Sheet, die Aufgaben dem TodoStore. Dank des Bindings isPresented kann sich das Sheet selbst schließen, ohne Eigentümer dieses Werts zu sein. List($store.items) gibt jeder Zeile ein Binding auf ihr Element, und wenn ein Toggle umschaltet, aktualisiert sich der Titel, der remainingCount liest, von selbst. Falls Ihnen das guard in add fremd vorkommt, hilft der Beitrag zu Optionals und guard let. Setzen Sie an der Wurzel der App eine einzige, in @State gehaltene Instanz ein, statt .environment(TodoStore()) direkt hinzuschreiben.
Häufig gestellte Fragen
Was ist der Unterschied zwischen @State und @Binding?
@State ist Eigentümer der Daten und legt den Wert im Speicher von SwiftUI ab. @Binding speichert nichts; es gewährt Lese- und Schreibzugriff auf einen Wert, der einer anderen View gehört.
Muss ich ObservableObject noch lernen, wenn es @Observable gibt?
Ja, denn ein großer Teil der bestehenden Projekte und Tutorials nutzt es weiterhin, und für Apps, die Versionen vor iOS 17 unterstützen, ist es die einzige Möglichkeit. Bevorzugen Sie in neuem Code Observation und kennen Sie die ältere API gut genug, um sie lesen zu können.
Kann ich @State anstelle von @StateObject verwenden?
Nur wenn das Modell mit dem Makro @Observable gekennzeichnet ist. Halten Sie eine Klasse, die ObservableObject erfüllt, in @State, aktualisieren Änderungen an Eigenschaften die View nicht; solche Klassen brauchen @StateObject.
Warum aktualisiert sich meine View nicht?
Die drei üblichen Ursachen: Das Modell ist nicht mit @Observable bzw. die Eigenschaft nicht mit @Published gekennzeichnet, die View liest aus einer Kopie (ein Wert der Eltern-View wurde in @State kopiert), oder die Änderung betrifft eine Eigenschaft, die body nie liest.
Verwandte Artikel
SwiftUI: Erste App entwickeln – Schritt für Schritt
Die erste SwiftUI-App vom leeren Xcode-Projekt bis zum Simulator: App, View, VStack, @State, List und TextField in einer funktionierenden To-do-Liste.
Swift Closures erklärt: $0, @escaping und [weak self]
Die Closure-Syntax in Swift Schritt für Schritt von der Langform bis $0: Trailing Closures, Werte einfangen, @escaping, Retain Cycles und [weak self].
Swift async/await Leitfaden: Task, Actor und @MainActor
Vom Completion Handler zu async/await: Task, async let, TaskGroup, @MainActor, Actors, Abbruch und ein JSON-API-Aufruf mit URLSession in SwiftUI.