Avoid top-level view branch swapping

Add guidance to SwiftUI skill docs recommending a stable view tree and avoiding top-level conditional branch swapping. Updated swiftui-performance-audit/SKILL.md and swiftui-view-refactor/SKILL.md to include checklist bullets, a new section with examples demonstrating preferred (single base view, localized conditions) vs. avoid patterns, and an updated refactor step order to emphasize stable structure. This reduces root identity churn, unnecessary invalidation, and improves SwiftUI diffing and performance.
This commit is contained in:
Thomas Ricouard 2026-02-03 06:23:23 +01:00
parent 527d60383c
commit 0ce51f2e0d
2 changed files with 52 additions and 3 deletions

View file

@ -25,6 +25,7 @@ Collect:
Focus on:
- View invalidation storms from broad state changes.
- Unstable identity in lists (`id` churn, `UUID()` per render).
- Top-level conditional view swapping (`if/else` returning different root branches).
- Heavy work in `body` (formatting, sorting, image decoding).
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
- Large images without downsampling or resizing.
@ -52,6 +53,7 @@ Ask for:
Prioritize likely SwiftUI culprits:
- View invalidation storms from broad state changes.
- Unstable identity in lists (`id` churn, `UUID()` per render).
- Top-level conditional view swapping (`if/else` returning different root branches).
- Heavy work in `body` (formatting, sorting, image decoding).
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
- Large images without downsampling or resizing.
@ -144,6 +146,20 @@ ForEach(items, id: \.self) { item in
Avoid `id: \.self` for non-stable values; use a stable ID.
### Top-level conditional view swapping
```swift
var content: some View {
if isEditing {
editingView
} else {
readOnlyView
}
}
```
Prefer one stable base view and localize conditions to sections/modifiers (for example inside `toolbar`, row content, `overlay`, or `disabled`). This reduces root identity churn and helps SwiftUI diffing stay efficient.
### Image decoding on the main thread
```swift

View file

@ -97,6 +97,38 @@ private struct HeaderSection: View {
}
```
### 3b) Keep a stable view tree (avoid top-level conditional view swapping)
- Avoid patterns where a computed view (or `body`) returns completely different root branches using `if/else`.
- Prefer a single stable base view, and place conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, row content, etc.).
- Root-level branch swapping can cause identity churn, broader invalidation, and extra recomputation in SwiftUI.
Prefer:
```swift
var body: some View {
List {
documentsListContent
}
.toolbar {
if canEdit {
editToolbar
}
}
}
```
Avoid:
```swift
var documentsListView: some View {
if canEdit {
editableDocumentsList
} else {
readOnlyDocumentsList
}
}
```
### 4) View model handling (only if already present)
- Do not introduce a view model unless the request or existing code clearly calls for one.
- If a view model exists, make it non-optional when possible.
@ -121,9 +153,10 @@ init(dependency: Dependency) {
1) Reorder the view to match the ordering rules.
2) Favor MV: move lightweight orchestration into the view using `@State`, `@Environment`, `@Query`, `task`, and `onChange`.
3) If a view model exists, replace optional view models with a non-optional `@State` view model initialized in `init` by passing dependencies from the view.
4) Confirm Observation usage: `@State` for root `@Observable` view models, no redundant wrappers.
5) Keep behavior intact: do not change layout or business logic unless requested.
3) Ensure stable view structure: avoid top-level `if`-based branch swapping; move conditions to localized sections/modifiers.
4) If a view model exists, replace optional view models with a non-optional `@State` view model initialized in `init` by passing dependencies from the view.
5) Confirm Observation usage: `@State` for root `@Observable` view models, no redundant wrappers.
6) Keep behavior intact: do not change layout or business logic unless requested.
## Notes