Zustandsbesitz in SwiftUI: Views schlank und den Datenfluss vorhersehbar halten
Ein SwiftUI-Bildschirm wirkt reizend, solange er klein ist. Ein paar @State-Properties, eine Liste, ein Button. Dann wächst der Bildschirm, der Zustand verteilt sich auf drei Stellen, eine Änderung zeichnet die falsche View neu, und niemand kann sagen, wem die Daten eigentlich gehören. Die meisten dieser Probleme stammen nicht von SwiftUI selbst, sondern von unklarem Zustandsbesitz.
In diesem Beitrag konzentriere ich mich auf eine einzige Frage: Wem gehört dieser Zustand? Die Antwort entscheidet, welchen Property Wrapper Sie verwenden, wo der Zustand lebt und welche Views aktualisiert werden.
Das Prinzip der einzigen Wahrheitsquelle
In SwiftUI ist eine View eine Funktion des Zustands. Der Zustand ändert sich, die View wird neu berechnet. Dieses Modell funktioniert nur dann gut, wenn jeder Wert genau einen Besitzer hat. Halten Sie dieselbe Information an zwei Stellen, wird es Ihre Aufgabe, beide synchron zu halten, und genau dort beginnen die Fehler.
Eine praktische Regel: Halten Sie einen Wert in der untersten gemeinsamen übergeordneten View oder im Modell, das ihn ändern muss. Untergeordnete Views erhalten den Wert entweder schreibgeschützt oder bekommen über ein Binding das Recht, ihn zu ändern. Nicht kopieren, nicht spiegeln.
@State ist für lokalen, flüchtigen Zustand
@State eignet sich für einfache Werte, die eine View selbst besitzt: den Inhalt eines Textfelds, ob ein Sheet geöffnet ist, den ausgewählten Tab. Diese Werte hängen am Lebenszyklus dieser View, und andere Bildschirme müssen nichts davon wissen.
struct NoteEditor: View {
@State private var draft = ""
@State private var isConfirmingDiscard = false
var body: some View {
TextEditor(text: $draft)
.toolbar {
Button("Verwerfen") { isConfirmingDiscard = true }
}
}
}
@State-Werte als private zu markieren, ist eine gute Gewohnheit. Muss ein Wert von außen geändert werden, ist er nicht wirklich der Zustand dieser View; er sollte in die übergeordnete View wandern.
Befugnisse explizit mit @Binding weitergeben
Muss eine untergeordnete View einen Wert ändern, verwenden Sie @Binding. Ein Binding ist keine Kopie, sondern ein Verweis auf den Besitzer. Die Daten liegen weiterhin an einer Stelle, und die untergeordnete View erhält nur Zugriffsrechte.
struct FavoriteButton: View {
@Binding var isFavorite: Bool
var body: some View {
Button {
isFavorite.toggle()
} label: {
Image(systemName: isFavorite ? "star.fill" : "star")
}
}
}
Reicht Lesezugriff, übergeben Sie statt eines Bindings einen einfachen Wert. Weniger Befugnisse bedeuten weniger Überraschungen. Eine View, die mit einem einfachen Wert arbeitet, lässt sich außerdem in Vorschauen und Tests viel leichter aufsetzen.
Ein beobachtbares Modell für geteilten Zustand
Zustand, den mehrere Bildschirme teilen oder der Geschäftslogik enthält, braucht eine Modellklasse. In modernem SwiftUI können Sie dafür das Makro @Observable verwenden. Ein wichtiger Vorteil: Die View bindet sich nur an die Properties, die sie in ihrem Body tatsächlich liest. Ändert sich eine andere Eigenschaft des Modells, werden unbeteiligte Views nicht neu berechnet.
@Observable
final class CartModel {
var items: [CartItem] = []
var couponCode = ""
var total: Decimal {
items.reduce(0) { $0 + $1.price * Decimal($1.quantity) }
}
func add(_ product: Product) {
// Geschäftsregeln leben hier, nicht in der View
}
}
Hier gilt ein zweites Prinzip: Keine Geschäftsregeln in der View. Summen berechnen, Gutscheine prüfen und Netzwerkaufrufe gehören ins Modell. Die View zeigt nur den Zustand an und leitet die Absicht des Nutzers an das Modell weiter. Diese Trennung erleichtert auch das Testen, denn das Modell lässt sich ohne UI prüfen.
Speichern Sie das Modell mit @State in der View, die es erzeugt, und reichen Sie es als Parameter nach unten. Haben Sie älteren Code auf Basis von ObservableObject, behalten Sie die Unterscheidung zwischen @StateObject und @ObservedObject bei: Der Besitzer, der das Objekt erzeugt, nutzt @StateObject, eine View, die es nur verwendet, @ObservedObject. Der falsche Besitzer kann dazu führen, dass das Objekt bei jeder Neuerzeugung der View zurückgesetzt wird.
Umgebungsobjekte: mit Bedacht einsetzen
Ein Modell mit @Environment tief in den View-Baum zu tragen, ist bequem, macht die Abhängigkeit aber unsichtbar. Eine View verrät in ihrer Signatur nicht, welches Modell sie braucht, und fehlt es in der Umgebung, stürzt die App zur Laufzeit ab.
Reservieren Sie die Umgebung deshalb für Dinge, die wirklich app-weit gelten: Sitzungsinformationen, Theme, Sprache, Einstellungen. Ein bildschirmspezifisches Modell sollte explizit als Parameter übergeben werden. Die Regel: Ist eine Abhängigkeit eine zentrale Eingabe der View, gehört sie in die Signatur; ist sie ein Umgebungskontext, darf sie in der Umgebung leben.
Neuzeichnungen messen, nicht raten
Den Zustand an die richtige Stelle zu legen, soll auch unnötige Arbeit vermeiden. Liest ein großer View-Body alles an einer Stelle, erzwingt eine kleine Änderung die Neuberechnung des ganzen Bildschirms. Die Lösung besteht meist darin, die View in sinnvolle Teile zu zerlegen, die jeweils nur die Daten erhalten, die sie brauchen.
Messen Sie verdächtige Stellen mit den SwiftUI-Werkzeugen in Instruments. Debugging-Hilfen wie let _ = Self._printChanges() zeigen, warum eine View neu berechnet wurde. Erst messen, dann zerlegen; jede View reflexhaft aufzuteilen kann auch die Lesbarkeit verschlechtern.
Eine Checkliste
Stellen Sie beim Schreiben eines neuen Bildschirms diese Fragen der Reihe nach:
- Gehört dieser Wert nur dieser View? Dann
@Stateundprivate. - Ändert eine untergeordnete View ihn? Dann
@Binding; liest sie nur, genügt ein einfacher Wert. - Teilen ihn mehrere Bildschirme oder enthält er Geschäftsregeln? Dann gehört er in ein Modell, mit
@Observable. - Ist die Abhängigkeit wirklich app-weit? Dann in die Umgebung; sonst als Parameter.
- Wird dieselbe Information an zwei Stellen gehalten? Entfernen Sie eine davon.
Fazit
Ein Großteil der Komplexität in SwiftUI entsteht durch Zustand ohne Besitzer. Weisen Sie jedem Wert genau einen Besitzer zu, klären sich Wrapper-Wahl, Codeplatzierung und Neuzeichnung weitgehend von selbst. Denken Sie zuerst an den Besitz und wählen Sie den Wrapper danach; Ihre Views bleiben schlank und Ihr Datenfluss vorhersehbar.