Communitygithub.com

gtokman/skills

Wie SwiftUI entscheidet, was neu gerendert wird – Identität, Lebensdauer, Abhängigkeiten – und wie man Views schreibt, die effizient aktualisieren, insbesondere SwiftUI in UIKit-Zellen (UIHostingConfiguration) und Views, die aus Streaming- oder generierten Inhalten neu aufgebaut werden. Verwenden Sie dies beim Schreiben oder Überprüfen von SwiftUI-Views auf Leistung, beim Beheben von Views, die Zustand verlieren, flackern, zu oft neu rendern oder Bilder neu laden, bei der Auswahl von ForEach-Identifikatoren, beim Hosten von SwiftUI in Collection/Table View-Zellen oder beim Debuggen von Layouts, die sich ändern, wenn Views umschlossen werden. Basierend auf WWDC21 „Demystify SwiftUI“ und WWDC23 „Demystify SwiftUI performance“, plus in der Praxis verifizierte Lektionen.

Was ist skills?

Dieses Skill konzentriert sich auf die Optimierung der SwiftUI-Rendering-Leistung, indem es die drei Kernkonzepte behandelt, die SwiftUI verwendet, um zu entscheiden, was neu gerendert wird: Identität, Lebensdauer und Abhängigkeiten. Es bietet praktische Regeln und Muster, um häufige Probleme wie Zustandsverlust, übermäßiges Neuzeichnen, Bildneuladungen und Animationsfehler zu vermeiden. Der Skill behandelt auch spezifische Szenarien wie das Hosten von SwiftUI in UIKit-Zellen mithilfe von `UIHostingConfiguration` sowie Views, die aus Streaming- oder generierten Inhalten neu aufgebaut werden. Basierend auf offiziellen WWDC-Sessions und praxiserprobten Erkenntnissen bietet es eine Checkliste zur Überprüfung der eigenen SwiftUI-Views, Techniken zur Identitätsprüfung (z.B. mit `_printChanges()` und Pixelvergleichen) und eine Fallstudie, die die Probleme einer echten Implementierung (AddToChatMenu) zeigt.

Funktioniert mit~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/gtokman/skills/tree/HEAD/swiftui-rendering

In Ihrer bevorzugten KI fragen

Öffnet einen neuen Chat, in dem dieser Agent-Skill bereits geladen ist.

Dokumentation

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 und onAppear/task werden 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 eines if/switch ist eine andere Identität.
  • Der Abhängigkeitsgraph. Die Abhängigkeiten einer View sind ihre gespeicherten Eigenschaften (Eingaben von ihrem Eltern-View), @State, @Environment, @Binding und die @Observable-Eigenschaften, die ihr body liest. Wenn sich eine ändert, erhalten nur die Views, die davon abhängen, einen neuen body. 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 → body wird ausgeführt → Kinder verglichen → Rekursion.

Regeln

Identität

  1. 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.
  2. IDs müssen stabil und eindeutig sein. Kein UUID() in einer berechneten id (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 einem ForEach verhalten sich falsch.
  3. 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.
  4. 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, frame ohne Größen, layoutPriority(0), transformEnvironment) kosten so gut wie nichts. Verwenden Sie eine Verzweigung nur für wirklich unterschiedliche Views.
  5. Layouts mit AnyLayout umschalten, nicht if wrap { FlowLayout { … } } else { HStack { … } } — Kinder behalten ihre Identität und ihren Zustand.
  6. AnyView vermeiden. Es verbirgt die Typstruktur vor SwiftUI (schlechterer Diff, und in List/Table wird gezwungen, jede Zeile zu bauen, um sie zu zählen). Verwenden Sie @ViewBuilder-Funktionen, switch, Generics.
  7. 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

  1. Views nur das übergeben, was sie verwenden. Eine View, die ein ganzes Modell annimmt, führt body erneut 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.
  2. @Observable verfolgt, was body liest. Das Lesen einer Eigenschaft in einem Action-Closure erzeugt keine Abhängigkeit; das Lesen in body schon. Setzen Sie den Zustand, den die View anzeigt, in das Modell und lesen Sie ihn in body nur dort, wo er benötigt wird.
  3. Niemals einen neuen Closure (oder anderen nicht-vergleichbaren Wert) pro Rendering in die Umgebung setzen – z. B. .environment(\.actions, Actions { … }) in einem body. 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.
  4. 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.
  5. 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() verbirgt layoutPriority des Kindes vor seinem Container. Innerhalb eines HStack/einer Zeile hört ein layoutPriority(1)-Kind, das in EquatableView eingewickelt 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

  1. Keine Parser-, Decodierungs-, Filterungs-, Sortierungs- oder aufwändige Allokationsarbeit in body. Er wird bei jeder Ungültigmachung ausgeführt. Vorberechnen im Modell, cachen (ein NSCache, abgebildet auf Eingabe) oder in die View verschieben, deren Eingaben sich tatsächlich ändern.
  2. Teure Eingaben von schnell wechselndem Zustand trennen. Wenn eine View Daten decodiert und @State hä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 @State besitzt.
  3. Langsame Arbeit in .task, nicht in init oder body. Machen Sie es async; blockieren Sie niemals den Initialwert einer dynamischen Eigenschaft.
  4. Teure Formatierung cachen — AttributedString(markdown:), String-Interpolation auf heißen Pfaden, Bundle-Suchen.

Listen

  1. List/Table sammeln alle IDs im Voraus. Halten Sie die ID-Generierung billig und geben Sie jedem ForEach-Element eine konstante Anzahl von Views — keine if-Filter oder AnyView innerhalb des Zeilen-Closures (filtern Sie die Daten im Modell, nicht inline in body). Verschachtelte ForEach ist 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 Equatable auf 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 benutzerdefinierten Layout (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 .aspectRatio nur an, wenn Sie ein Verhältnis haben.

Überprüfungs-Checkliste

  • Jedes ForEach hat stabile, eindeutige IDs – kein \.offset, kein berechnetes UUID().
  • Kein AnyView; Verzweigungen nur für wirklich unterschiedliche Views; AnyLayout für Layout-Umschaltungen.
  • Views erhalten nur die Daten, die sie rendern.
  • Nichts Nicht-Vergleichbares wird pro Rendering in der Umgebung erstellt.
  • body führt kein Parsen/Decodieren/Filtern durch; teure Eingaben von schnellem @State getrennt.
  • Observable-Eigenschaften werden nicht auf gleiche Werte zurückgesetzt.
  • .equatable(), wo es Arbeit spart – und nicht bei Kindern, die layoutPriority verwenden.
  • 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 einem body protokolliert, warum er ausgeführt wurde (@self, @identity oder 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:), proposedSize mit nil Höhe) außerhalb des Bildschirms rendern und PNGs vergleichen. Es erfasst Layout-Regressionen wie die EquatableView/layoutPriority-Regression. (Map, ScrollView und 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).

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Verwandte Skills