From 0ce51f2e0db5f240a3067a37bbfb86dae6f45bb7 Mon Sep 17 00:00:00 2001 From: Thomas Ricouard Date: Tue, 3 Feb 2026 06:23:23 +0100 Subject: [PATCH] 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. --- swiftui-performance-audit/SKILL.md | 16 ++++++++++++ swiftui-view-refactor/SKILL.md | 39 +++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/swiftui-performance-audit/SKILL.md b/swiftui-performance-audit/SKILL.md index a852a81..c28123f 100644 --- a/swiftui-performance-audit/SKILL.md +++ b/swiftui-performance-audit/SKILL.md @@ -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 diff --git a/swiftui-view-refactor/SKILL.md b/swiftui-view-refactor/SKILL.md index e22ebf3..3904176 100644 --- a/swiftui-view-refactor/SKILL.md +++ b/swiftui-view-refactor/SKILL.md @@ -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