Was macht SwiftUI-Experte?
Ein SwiftUI-Bildschirm steht oder fällt mit der State-Verwaltung: Wenn State am falschen Ort platziert wird, wertet jeder Tastendruck den gesamten Baum neu aus, Listen ruckeln und Animationen zerreißen. Dieser Skill erstellt und überprüft SwiftUI-Views, sodass die Invalidierung auf diejenigen Views begrenzt bleibt, die tatsächlich geänderte Daten lesen, das Frame-Budget beim Scrollen eingehalten wird und das Ergebnis standardmäßig barrierefrei ist – nicht erst nachträglich.
Vorgehensweise
Arbeiten Sie in dieser Reihenfolge, da das State-Design alles Weitere bestimmt – das Beheben von State nach Aufbau der View-Hierarchie bedeutet, die Hierarchie neu zu schreiben.
Schritt 1: Anforderungen erfassen
Vor dem Schreiben von View-Code sammeln. Wenn der Benutzer es nicht weiß, Standardwert verwenden und als Vermutung kennzeichnen.
- Zielgeräte und Bildwiederholrate (Standard: ProMotion 120Hz iPhone plus ein 60Hz-Basissgerät).
- Datenform: Anzahl der Elemente in der größten Liste, wie oft sich Daten ändern, ob sie streamen.
- Besitzlandkarte: Für jeden State – welche View erstellt ihn, welche Views lesen ihn, welche verändern ihn.
- Barrierefreiheitsanforderungen über das Basisniveau hinaus (Standard: vollständige Dynamic-Type- + VoiceOver-Unterstützung).
Schritt 2: State-Verwaltung pro Wert wählen
Den schmalsten geeigneten Wrapper auswählen:
@State– lokaler, view-eigener State vom Werttyp. Auch der richtige Besitzer für eine@Observable-Modellinstanz, die von der View erstellt wird.@Binding– eine bidirektionale Referenz auf State, der woanders gehört. Binding nur übergeben, wenn die Child-Komponente etwas verändert; einen einfachen Wert übergeben, wenn sie nur liest.@Observable(Observation-Framework) – Modellobjekte; Views beobachten nur die Eigenschaften, die sie lesen, sodass die Invalidierung pro Eigenschaft und nicht pro Objekt erfolgt.@Environment– Dependency Injection den Baum hinunter für Dienste und gemeinsame Modelle.
Regel: State lebt im niedrigsten gemeinsamen Vorfahren aller Views, die ihn lesen oder schreiben – nicht tiefer (er wird zurückgesetzt, wenn die View verschwindet) und nicht höher (er vergrößert die Invalidierung).
Schritt 3: Views so zusammensetzen, dass Invalidierung begrenzt bleibt
bodyklein halten; Subviews auslagern, sodass eine Änderung nur eine Zeile und nicht den gesamten Bildschirm invalidiert. Einbody, der ungefähr 50 Zeilen oder drei Ebenen Verschachtelung überschreitet, ist ein Kandidat für Aufteilung.- Minimale Daten an Subviews übergeben –
item.titleübergeben, nicht den gesamten Store – damit nur betroffene Views neu rendern. @ViewBuilder-Helfer verwenden anstelle riesiger, verschachtelter Closures.- Niemals schwere Arbeiten (Formatierung, Sortierung, Filterung, Dekodierung) in
bodyausführen; im Modell berechnen und der View fertige Werte übergeben.bodymuss im Mikrosekundenbereich bleiben, da er während Animationen jeden Frame laufen kann.
Schritt 4: Listen und das Frame-Budget
Das Budget beträgt 8,3ms pro Frame bei 120Hz, 16,7ms bei 60Hz – alles im Hauptthread für diesen Frame, nicht nur Ihre Zeile. Praktische Regeln:
ListoderLazyVStackfür mehr als ~50 Zeilen verwenden, damit Zeilen bedarfsgerendert werden; ein einfachesVStackin einemScrollViewbaut jede Zeile sofort auf.- Elementen stabile
ids aus den eigenen Daten (item.id) geben, niemals Array-Indizes oderUUID()berechnet inbody– instabile IDs zerstören die Zeilenidentität, verursachen Flackern, verlorene Scrollposition und verlorenen Zeilenstatus. - Zeilen müssen konstante Kosten haben: keine zeilenweisen Datumsformatierer oder Bilddekodierung; Formatierer zwischenspeichern,
AsyncImageoder eine cachende Bild-Pipeline verwenden. - Mit dem Instruments-SwiftUI-Template verifizieren: view-body-Evaluationszahlen beim Scrollen beobachten. Ein Zeilentyp, dessen
bodysich bei irrelevanten State-Änderungen evaluiert, ist ein State-Besitzer-Bug – korrigieren Sie Schritt 2, nicht mit Memoization überdecken.
Schritt 5: Vom State aus animieren
- Animation vom State aus steuern:
withAnimation { model.expanded.toggle() }. .animation(_:value:), begrenzt auf einen bestimmten Wert, bevorzugen gegenüber impliziten globalen Animationen, die Dinge animieren, die nicht beabsichtigt sind.matchedGeometryEffectfür Shared-Element-Übergänge verwenden;transitionfür Einfügen/Entfernen.accessibilityReduceMotionrespektieren: Bewegung durch Opazitätsüberblendungen ersetzen, wenn es gesetzt ist.
Schritt 6: Barrierefreiheits-Durchgang
accessibilityLabel/accessibilityValuehinzufügen; verwandte Elemente mitaccessibilityElement(children: .combine)gruppieren, sodass eine Zeile als ein Satz und nicht als fünf Fragmente vorgelesen wird.- Dynamic Type respektieren – Systemtextstile, keine festen Rahmen, die Text abschneiden; in der größten Barrierefreiheitsgröße verifizieren.
- Kontrast prüfen und
#Preview-Varianten für Dark Mode, große Dynamic Type sowie leere/Lade-/Fehlerzustände mit Beispieldaten hinzufügen.
Ausgearbeitetes Beispiel: State-Verwaltung, schlecht vs. gut
Schlecht – der gesamte Bildschirm invalidiert bei jedem Tastendruck, weil der Suchtext in einem Modell lebt, das der gesamte Baum beobachtet, und Zeilen den gesamten Store erhalten:
@Observable final class ScreenStore {
var searchText = ""
var items: [Item] = []
}
struct ContactList: View {
@State private var store = ScreenStore()
var body: some View {
VStack {
TextField("Suchen", text: $store.searchText)
ForEach(store.items) { item in
ContactRow(store: store, item: item) // Zeile beobachtet alles
}
}
}
}
Gut – Suchtext ist view-lokal, Zeilen bekommen nur die Werte, die sie lesen, und die Liste ist lazy mit stabilen IDs:
struct ContactList: View {
@State private var searchText = ""
@Environment(ContactModel.self) private var model
var body: some View {
List(model.filtered(searchText)) { item in
ContactRow(name: item.name, isOnline: item.isOnline)
}
.searchable(text: $searchText)
}
}
struct ContactRow: View {
let name: String
let isOnline: Bool
var body: some View {
Label(name, systemImage: isOnline ? "circle.fill" : "circle")
}
}
Tippen in der guten Version invalidiert den List-Inhalts-Closure und die Zeilen, deren gefilterte Zugehörigkeit sich geändert hat – sonst nichts.
Liefergegenstand
Funktionsfähigen SwiftUI-View-Code plus einen einleitenden Absatz State-Map, die für jeden State angibt: seinen Besitzer, seinen Wrapper und welche Views ihn lesen im Vergleich zu schreiben. Für Überprüfungen eine Liste mit Ergebnissen, geordnet nach Frame-Budget-Auswirkung, jede mit der Behebung.
NICHT tun
- Niemals State während der
body-Evaluierung verändern – es führt zu Endlosschleifen oder Warnungen vor undefiniertem Verhalten. - Keine asynchronen Arbeiten in
onAppearmit einem rohenTaskausführen –.task {}verwenden, das beim Verschwinden automatisch abbricht. - View-Typen (
Color,Image,AnyView) nicht in Modelle stecken; Modelle müssen ohne UI testbar bleiben. - Nicht allen State auf eine einzige bildschirmweite Ebene heben "zur Einfachheit" – das ist genau das Muster, das jeden Tastendruck den Bildschirm neu zeichnen lässt.
- Invalidierungsfehler nicht mit
equatable()oder Caching überdecken, bevor die Besitzverhältnisse behoben sind; der Fehler taucht in der nächsten View wieder auf, die dasselbe Objekt beobachtet. - Keine festen Rahmen für Text verwenden – Dynamic Type wird ihn abschneiden.
Qualitätslatte
Ein fertiger View besteht den Test, wenn:
- Jeder State genau einen Besitzer und den schmalsten geeigneten Wrapper hat.
- Instruments zeigt, dass
bodyvon Zeilen nur evaluiert wird, wenn sich ihre eigenen Daten ändern. - Scrollen der größten erwarteten Liste das Frame-Budget auf dem Basis-60Hz-Gerät einhält.
- Langlebiger State über jede verschwindende View gehoben wird, damit er Navigation überlebt.
- Tastaturvermeidung und Safe Areas berücksichtigt sind (
safeAreaInset,scrollDismissesKeyboard). - VoiceOver jedes interaktive Element mit einer sinnvollen Beschreibung vorliest; das Layout die größte Dynamic-Type-Größe überlebt;
reduceMotionrespektiert wird. - Previews für Lade-, Leer-, Fehler-, Dark-Mode- und große-Schrift-Zustände existieren.
Für appweites Profiling über einen einzelnen Bildschirm hinaus mit mobile-perf-profiler kombinieren.