SwiftUI-Rendering
SwiftUI betrachtet drei Dinge in Ihrem Code: Identität (ist dies dasselbe Element wie
zuvor?), Lebensdauer (wie lange diese Identität besteht – und damit @State), und
Abhängigkeiten (welche Eingaben, wenn sie sich ändern, führen dazu, dass ein body erneut ausgeführt wird). Fast jede
Frage „Warum wurde es neu gerendert / hat es den Zustand verloren / animiert es seltsam / ruckelt es“ fällt in eine dieser Kategorien.
Das Modell
- View-Werte sind flüchtig; Identität ist es nicht. Eine View-Struktur wird erstellt, mit dem
vorherigen Wert verglichen und verworfen. Was bestehen bleibt, ist ihre Identität, und der
@State-/@StateObject-Speicher ist an diese Identität gebunden: Wenn sich die Identität ändert, wird der Zustand ersetzt undonAppear/taskwerden erneut ausgeführt, Bilder werden neu geladen. - Zwei Arten von Identität. Explizit:
ForEach-IDs,Identifiable,.id(_:). Strukturell: der Typ und die Position einer View in der Hierarchie – jeder Zweig einesif/switchist eine andere Identität. - Der Abhängigkeitsgraph. Die Abhängigkeiten einer View sind ihre gespeicherten Eigenschaften (Eingaben von ihrem
Eltern-View),
@State,@Environment,@Bindingund die@Observable-Eigenschaften, die ihrbodyliest. Wenn sich eine ändert, erhalten nur die Views, die davon abhängen, einen neuenbody. SwiftUI vergleicht dann die neuen Kind-Werte mit den alten und stoppt, wo immer sich nichts geändert hat. - Aktualisierungsreihenfolge für eine ungültig gewordene View: neuer Wert → dynamische Eigenschaften aktualisiert →
bodywird ausgeführt → Kinder verglichen → Rekursion.
Regeln
Identität
- Niemals nach Index identifizieren (
ForEach(Array(x.enumerated()), id: \.offset),0..<countüber dynamischen Daten). Alles, was oben eingefügt wird, verschiebt jeden späteren Index: deren Zustand wird zurückgesetzt, ihre Bilder werden neu geladen, Animationen gehen an die falsche Stelle. Verwenden Sie eine stabile, eindeutige ID. - IDs müssen stabil und eindeutig sein. Kein
UUID()in einer berechnetenid(neue Identität bei jedem Rendern). Keine Namen, die sich wiederholen können (zwei „Dog biscuit“-Leckerlis). Wiederholungen eindeutig machen (key,key#2), wenn die Daten es nicht garantieren können – doppelte IDs in einemForEachverhalten sich falsch. - Für generierte oder gestreamte Inhalte Identität von einer Struktur ableiten, die sich nicht
bewegt: wo das Element im Quelltext beginnt (Streaming hängt nur an) oder
Vorlagenposition + iterationsspezifischer Schlüssel für Programmausgabe (Modell von React:
template:key). Nicht schlüsselgebundene Elemente (loser Text, Absätze) nach dem schlüsselgebundenen Element vor ihnen plus einem Zähler benennen (e812+2), sodass neue Inhalte am Ende nie frühere Elemente umbenennen. - Eine View mit wechselnden Modifikatoren gegenüber Verzweigungen bevorzugen.
if expired { v.opacity(0.3) } else { v }sind zwei Identitäten;v.opacity(expired ? 0.3 : 1)ist eine. „Inerte“ Werte (Opazität 1, Padding 0,frameohne Größen,layoutPriority(0),transformEnvironment) kosten so gut wie nichts. Verwenden Sie eine Verzweigung nur für wirklich unterschiedliche Views. - Layouts mit
AnyLayoutumschalten, nichtif wrap { FlowLayout { … } } else { HStack { … } }— Kinder behalten ihre Identität und ihren Zustand. AnyViewvermeiden. Es verbirgt die Typstruktur vor SwiftUI (schlechterer Diff, und inList/Tablewird gezwungen, jede Zeile zu bauen, um sie zu zählen). Verwenden Sie@ViewBuilder-Funktionen,switch, Generics.- Model-Element-Arten als Enum modellieren und darüber switchen in der dispatchierenden View, ohne
default: ein neuer Fall kann nicht stillschweigend unbehandelt bleiben.
Abhängigkeiten
- Views nur das übergeben, was sie verwenden. Eine View, die ein ganzes Modell annimmt, führt
bodyerneut aus, wenn sich ein Feld ändert; übergeben Sie das Bild, nicht den Hund. Unterviews extrahieren, um Abhängigkeiten einzugrenzen — mit Bedacht, nicht für jede Zeile. @Observableverfolgt, wasbodyliest. Das Lesen einer Eigenschaft in einem Action-Closure erzeugt keine Abhängigkeit; das Lesen inbodyschon. Setzen Sie den Zustand, den die View anzeigt, in das Modell und lesen Sie ihn inbodynur dort, wo er benötigt wird.- Niemals einen neuen Closure (oder anderen nicht-vergleichbaren Wert) pro Rendering in die Umgebung setzen
– z. B.
.environment(\.actions, Actions { … })in einembody. Es kann nicht verglichen werden, daher wird jeder Leser bei jeder Elternaktualisierung ungültig. Setzen Sie stattdessen ein stabiles Objekt in die Umgebung (.environment(modell)mit@Observable, lesen als@Environment(Model.self) var modell: Model?), und rufen Sie es aus Aktionen auf. - Nicht benachrichtigen, wenn sich nichts geändert hat. Eine
@Observable-Eigenschaft benachrichtigt bei jedem Setzen. Absicherung:if new != value { value = new }– wichtig, wenn Sie abgeleiteten Zustand bei jedem Block oder Ticken neu aufbauen. Equatable-Views, um unveränderte Teilbäume zu überspringen. Konformität der View (nur möglich, wenn sie keine Property Wrapper hat) und.equatable()anwenden, wenn ein Eltern-View oft mit größtenteils unveränderten Kindern neu rendert (eine Liste von Blöcken während des Streamings). Schreiben Sie ein benutzerdefiniertes==, das nur das vergleicht, was die View anzeigt. ⚠️.equatable()verbirgtlayoutPrioritydes Kindes vor seinem Container. Innerhalb einesHStack/einer Zeile hört einlayoutPriority(1)-Kind, das inEquatableVieweingewickelt ist, auf, freien Platz zu gewinnen, und die Zeile wird anders angelegt. Verwenden Sie.equatable()in vertikalen Listen von Blöcken, nicht bei Kindern von Zeilen/Stapeln, die auf Priorität angewiesen sind.
Billiger body
- Keine Parser-, Decodierungs-, Filterungs-, Sortierungs- oder aufwändige Allokationsarbeit in
body. Er wird bei jeder Ungültigmachung ausgeführt. Vorberechnen im Modell, cachen (einNSCache, abgebildet auf Eingabe) oder in die View verschieben, deren Eingaben sich tatsächlich ändern. - Teure Eingaben von schnell wechselndem Zustand trennen. Wenn eine View Daten decodiert und
@Statehält, das sich oft ändert (Ziehen, Scrubbing, Hover), wird das Decodieren bei jeder Änderung erneut ausgeführt. Decodieren Sie in einem Eltern-View, übergeben Sie das Ergebnis an eine View, die das@Statebesitzt. - Langsame Arbeit in
.task, nicht ininitoderbody. Machen Sie esasync; blockieren Sie niemals den Initialwert einer dynamischen Eigenschaft. - Teure Formatierung cachen —
AttributedString(markdown:), String-Interpolation auf heißen Pfaden, Bundle-Suchen.
Listen
List/Tablesammeln alle IDs im Voraus. Halten Sie die ID-Generierung billig und geben Sie jedemForEach-Element eine konstante Anzahl von Views — keineif-Filter oderAnyViewinnerhalb des Zeilen-Closures (filtern Sie die Daten im Modell, nicht inline inbody). VerschachtelteForEachist in Ordnung für Abschnitte.
SwiftUI in UIKit-Zellen (UIHostingConfiguration)
- Zellen werden wiederverwendet, und der SwiftUI-View-Zustand kann in das nächste Element übergehen, das in dieser
Zelle angezeigt wird. Geben Sie dem Stamm
.id(item.id), damit ein anderes Element einen frischen Baum erhält – und halten Sie den sichtbaren Benutzerzustand (markiert, erweitert, Auswahl) im Modell, abgebildet auf stabile IDs, nicht in@State, sonst geht er bei Wiederverwendung und Scrollen verloren/wird falsch angewendet. - Die Neukonfiguration einer Zelle baut den Stamm-View-Wert neu auf. Wenn der Inhalt durch ein
beobachtetes Modell gesteuert wird, machen Sie den Stamm-View
Equatableauf das, was er anzeigt (nicht auf rohe Eingaben, die nur das Modell füttern), und wenden Sie.equatable()an, sodass die Neukonfiguration pro Aktualisierung nicht den gesamten Baum neu ausführt – die Beobachtung führt die tatsächliche Aktualisierung durch. - Selbst-Größenanpassung misst mit einer offenen Höhe. Views ohne ideale Größe kollabieren dort auf
Null:
Canvas, Formen,GeometryReader..aspectRatio(r, contentMode: .fit)ist nicht genug – geben Sie ihnen eine Größe mit einem kleinen benutzerdefiniertenLayout(Breite aus dem Angebot, Höhe = Breite / Verhältnis; Sondierungshöhen ignorieren). .aspectRatio(nil, contentMode:)sperrt eine View auf die ideale Größe ihres Inhalts – Karten hören auf, ihre Zeile zu füllen. Wenden Sie.aspectRationur an, wenn Sie ein Verhältnis haben.
Überprüfungs-Checkliste
- Jedes
ForEachhat stabile, eindeutige IDs – kein\.offset, kein berechnetesUUID(). - Kein
AnyView; Verzweigungen nur für wirklich unterschiedliche Views;AnyLayoutfür Layout-Umschaltungen. - Views erhalten nur die Daten, die sie rendern.
- Nichts Nicht-Vergleichbares wird pro Rendering in der Umgebung erstellt.
-
bodyführt kein Parsen/Decodieren/Filtern durch; teure Eingaben von schnellem@Stategetrennt. - Observable-Eigenschaften werden nicht auf gleiche Werte zurückgesetzt.
-
.equatable(), wo es Arbeit spart – und nicht bei Kindern, dielayoutPriorityverwenden. - Gehostete Zellen: Stamm
.id(item.id), sichtbarer Zustand im Modell, Equatabler Stamm. - Views ohne ideale Größe erhalten eine, bevor sie in selbst-größenanpassende Zellen gehen.
Überprüfung
let _ = Self._printChanges()ganz oben in einembodyprotokolliert, warum er ausgeführt wurde (@self,@identityoder die Eigenschaft, die sich geändert hat). Nur Debug – vor dem Ausliefern entfernen.- Instruments → SwiftUI-Vorlage für Body-Zählungen und langsame Aktualisierungen; Hangs/Hitches für die Auswirkungen.
- Identitätstest: Jedes Präfix von gestreamten Inhalten rendern und bestätigen, dass jedes abgeschlossene Element-ID im endgültigen Rendering unverändert erscheint; nach Interaktionen bestätigen, dass überlebende Elemente ihre IDs behalten; keine doppelten Geschwister-IDs bestätigen.
- Pixel-Diff vor/nach einem Rendering-Refactoring: Views auf macOS mit
ImageRenderer(gleicher Code,.frame(width:),proposedSizemit nil Höhe) außerhalb des Bildschirms rendern und PNGs vergleichen. Es erfasst Layout-Regressionen wie dieEquatableView/layoutPriority-Regression. (Map,ScrollViewund entfernte Bilder werden nicht außerhalb des Bildschirms gerendert; alles andere vergleichen.) Eine Differenz eingrenzen, indem Sie jeweils eine geänderte Datei ein-/ausschalten.
Referenzen
references/case-study.md— der AddToChatMenu-Antwortrenderer (gestreamtes, modellgeneriertes Markup in Collection View-Zellen): jedes Problem, das anhand dieser Regeln gefunden wurde, und seine Behebung.- WWDC21 Demystify SwiftUI (Session 10022); WWDC23 Demystify SwiftUI performance (Session 10160).