@kata-sh/cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +56 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +95 -0
  9. package/dist/resource-loader.d.ts +18 -0
  10. package/dist/resource-loader.js +50 -0
  11. package/dist/wizard.d.ts +15 -0
  12. package/dist/wizard.js +159 -0
  13. package/package.json +50 -21
  14. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  15. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  16. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  17. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  18. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  19. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  20. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  21. package/pkg/package.json +8 -0
  22. package/scripts/postinstall.js +45 -0
  23. package/src/resources/AGENTS.md +108 -0
  24. package/src/resources/KATA-WORKFLOW.md +661 -0
  25. package/src/resources/agents/researcher.md +29 -0
  26. package/src/resources/agents/scout.md +56 -0
  27. package/src/resources/agents/worker.md +31 -0
  28. package/src/resources/extensions/ask-user-questions.ts +200 -0
  29. package/src/resources/extensions/bg-shell/index.ts +2758 -0
  30. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  31. package/src/resources/extensions/browser-tools/core.js +1057 -0
  32. package/src/resources/extensions/browser-tools/index.ts +4916 -0
  33. package/src/resources/extensions/browser-tools/package.json +20 -0
  34. package/src/resources/extensions/context7/index.ts +428 -0
  35. package/src/resources/extensions/context7/package.json +11 -0
  36. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  37. package/src/resources/extensions/github/formatters.ts +207 -0
  38. package/src/resources/extensions/github/gh-api.ts +537 -0
  39. package/src/resources/extensions/github/index.ts +778 -0
  40. package/src/resources/extensions/kata/activity-log.ts +88 -0
  41. package/src/resources/extensions/kata/auto.ts +2786 -0
  42. package/src/resources/extensions/kata/commands.ts +355 -0
  43. package/src/resources/extensions/kata/crash-recovery.ts +85 -0
  44. package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
  45. package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
  46. package/src/resources/extensions/kata/doctor.ts +683 -0
  47. package/src/resources/extensions/kata/files.ts +730 -0
  48. package/src/resources/extensions/kata/gitignore.ts +165 -0
  49. package/src/resources/extensions/kata/guided-flow.ts +976 -0
  50. package/src/resources/extensions/kata/index.ts +556 -0
  51. package/src/resources/extensions/kata/metrics.ts +397 -0
  52. package/src/resources/extensions/kata/observability-validator.ts +408 -0
  53. package/src/resources/extensions/kata/package.json +11 -0
  54. package/src/resources/extensions/kata/paths.ts +346 -0
  55. package/src/resources/extensions/kata/preferences.ts +695 -0
  56. package/src/resources/extensions/kata/prompt-loader.ts +50 -0
  57. package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
  58. package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
  59. package/src/resources/extensions/kata/prompts/discuss.md +151 -0
  60. package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
  61. package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
  62. package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
  63. package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
  64. package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
  65. package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
  66. package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
  67. package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
  68. package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
  69. package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
  70. package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
  71. package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
  72. package/src/resources/extensions/kata/prompts/queue.md +85 -0
  73. package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
  74. package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
  75. package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
  76. package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
  77. package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
  78. package/src/resources/extensions/kata/prompts/system.md +341 -0
  79. package/src/resources/extensions/kata/session-forensics.ts +550 -0
  80. package/src/resources/extensions/kata/skill-discovery.ts +137 -0
  81. package/src/resources/extensions/kata/state.ts +509 -0
  82. package/src/resources/extensions/kata/templates/context.md +76 -0
  83. package/src/resources/extensions/kata/templates/decisions.md +8 -0
  84. package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
  85. package/src/resources/extensions/kata/templates/plan.md +133 -0
  86. package/src/resources/extensions/kata/templates/preferences.md +15 -0
  87. package/src/resources/extensions/kata/templates/project.md +31 -0
  88. package/src/resources/extensions/kata/templates/reassessment.md +28 -0
  89. package/src/resources/extensions/kata/templates/requirements.md +81 -0
  90. package/src/resources/extensions/kata/templates/research.md +46 -0
  91. package/src/resources/extensions/kata/templates/roadmap.md +118 -0
  92. package/src/resources/extensions/kata/templates/slice-context.md +58 -0
  93. package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
  94. package/src/resources/extensions/kata/templates/state.md +19 -0
  95. package/src/resources/extensions/kata/templates/task-plan.md +52 -0
  96. package/src/resources/extensions/kata/templates/task-summary.md +57 -0
  97. package/src/resources/extensions/kata/templates/uat.md +54 -0
  98. package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
  99. package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
  100. package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
  101. package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
  102. package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
  103. package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
  104. package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
  105. package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
  106. package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
  107. package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
  108. package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
  109. package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
  110. package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
  111. package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
  112. package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
  113. package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
  114. package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
  115. package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
  116. package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
  117. package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
  118. package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
  119. package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
  120. package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
  121. package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
  122. package/src/resources/extensions/kata/types.ts +159 -0
  123. package/src/resources/extensions/kata/unit-runtime.ts +163 -0
  124. package/src/resources/extensions/kata/workspace-index.ts +203 -0
  125. package/src/resources/extensions/kata/worktree.ts +182 -0
  126. package/src/resources/extensions/mac-tools/index.ts +852 -0
  127. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  128. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  129. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  130. package/src/resources/extensions/search-the-web/format.ts +258 -0
  131. package/src/resources/extensions/search-the-web/http.ts +238 -0
  132. package/src/resources/extensions/search-the-web/index.ts +68 -0
  133. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  134. package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
  135. package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
  136. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  137. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  138. package/src/resources/extensions/shared/interview-ui.ts +822 -0
  139. package/src/resources/extensions/shared/next-action-ui.ts +235 -0
  140. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  141. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  142. package/src/resources/extensions/shared/ui.ts +400 -0
  143. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  144. package/src/resources/extensions/slash-commands/audit.ts +92 -0
  145. package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
  146. package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
  147. package/src/resources/extensions/slash-commands/index.ts +12 -0
  148. package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
  149. package/src/resources/extensions/subagent/agents.ts +126 -0
  150. package/src/resources/extensions/subagent/index.ts +1293 -0
  151. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  152. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  153. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  154. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  155. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  156. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  157. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  158. package/src/resources/skills/swiftui/SKILL.md +208 -0
  159. package/src/resources/skills/swiftui/references/animations.md +921 -0
  160. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  161. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  162. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  163. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  164. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  165. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  166. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  167. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  168. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  169. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  170. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  171. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  172. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  173. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  174. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  175. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
  176. package/dist/commands/task.d.ts +0 -9
  177. package/dist/commands/task.d.ts.map +0 -1
  178. package/dist/commands/task.js +0 -129
  179. package/dist/commands/task.js.map +0 -1
  180. package/dist/commands/task.test.d.ts +0 -2
  181. package/dist/commands/task.test.d.ts.map +0 -1
  182. package/dist/commands/task.test.js +0 -169
  183. package/dist/commands/task.test.js.map +0 -1
  184. package/dist/e2e/task-e2e.test.d.ts +0 -2
  185. package/dist/e2e/task-e2e.test.d.ts.map +0 -1
  186. package/dist/e2e/task-e2e.test.js +0 -173
  187. package/dist/e2e/task-e2e.test.js.map +0 -1
  188. package/dist/index.d.ts +0 -3
  189. package/dist/index.d.ts.map +0 -1
  190. package/dist/index.js +0 -93
  191. package/dist/index.js.map +0 -1
  192. package/dist/slug.d.ts +0 -2
  193. package/dist/slug.d.ts.map +0 -1
  194. package/dist/slug.js +0 -12
  195. package/dist/slug.js.map +0 -1
  196. package/dist/slug.test.d.ts +0 -2
  197. package/dist/slug.test.d.ts.map +0 -1
  198. package/dist/slug.test.js +0 -32
  199. package/dist/slug.test.js.map +0 -1
@@ -0,0 +1,1706 @@
1
+ # SwiftUI Performance Reference
2
+
3
+ <overview>
4
+ SwiftUI's declarative, data-driven architecture provides automatic UI updates, but this comes with performance implications. Understanding the update cycle, view identity, and optimization strategies enables building responsive apps.
5
+
6
+ **Core Performance Model:**
7
+
8
+ 1. **State Change**: A property wrapped with @State, @Observable, or similar changes
9
+ 2. **Body Recomputation**: SwiftUI evaluates the view's body property
10
+ 3. **Diffing**: SwiftUI compares new view hierarchy against previous
11
+ 4. **Minimal Updates**: Only changed parts render to screen via Core Animation
12
+
13
+ **Key Principle**: SwiftUI only recomputes body when dependency values change. Mastering what triggers recomputation and how to minimize it is essential for performance.
14
+
15
+ **Performance Philosophy**: Profile before optimizing. SwiftUI includes automatic optimizations. Only intervene when profiling identifies actual bottlenecks. Premature optimization adds complexity without benefit.
16
+ </overview>
17
+
18
+ <view_identity>
19
+ **View Identity** determines how SwiftUI tracks views across updates. Identity affects state preservation, transitions, and performance.
20
+
21
+ ## Two Types of Identity
22
+
23
+ ### Structural Identity
24
+
25
+ SwiftUI identifies views by their position in the view hierarchy. Most common form of identity.
26
+
27
+ ```swift
28
+ // Structural identity - views identified by position
29
+ VStack {
30
+ Text("First") // Identity: VStack > position 0
31
+ Text("Second") // Identity: VStack > position 1
32
+ }
33
+
34
+ // Problematic: branches change structural identity
35
+ if isLoggedIn {
36
+ ProfileView() // Identity: if branch > ProfileView
37
+ } else {
38
+ LoginView() // Identity: else branch > LoginView
39
+ }
40
+ ```
41
+
42
+ **Best Practice**: Prefer conditional modifiers over branches to preserve identity:
43
+
44
+ ```swift
45
+ // Bad - changes structural identity, loses state
46
+ if isExpanded {
47
+ DetailView(expanded: true)
48
+ } else {
49
+ DetailView(expanded: false)
50
+ }
51
+
52
+ // Good - preserves structural identity
53
+ DetailView(expanded: isExpanded)
54
+ ```
55
+
56
+ ### Explicit Identity
57
+
58
+ Use the `.id()` modifier to explicitly control identity. SwiftUI treats views with different IDs as completely distinct.
59
+
60
+ ```swift
61
+ // Force view recreation by changing ID
62
+ ScrollView {
63
+ ContentView()
64
+ .id(selectedCategory) // New ID = destroy and recreate
65
+ }
66
+
67
+ // List items use Identifiable for explicit identity
68
+ struct Item: Identifiable {
69
+ let id: UUID
70
+ let name: String
71
+ }
72
+
73
+ List(items) { item in
74
+ Text(item.name) // SwiftUI tracks by item.id
75
+ }
76
+ ```
77
+
78
+ **When to Use .id()**:
79
+
80
+ - Reset view state (form after submission, scroll position)
81
+ - Force view recreation when data fundamentally changes
82
+ - Ensure transitions work correctly
83
+
84
+ **Performance Impact**: Changing a view's ID destroys the old view and creates a new one, discarding all state. Expensive operation - use judiciously.
85
+
86
+ ## Identity and State Preservation
87
+
88
+ SwiftUI maintains @State values as long as view identity remains stable:
89
+
90
+ ```swift
91
+ struct CounterView: View {
92
+ @State private var count = 0 // Preserved while identity stable
93
+
94
+ var body: some View {
95
+ VStack {
96
+ Text("Count: \(count)")
97
+ Button("Increment") { count += 1 }
98
+ }
99
+ }
100
+ }
101
+
102
+ // Branching destroys identity and @State
103
+ if showCounter {
104
+ CounterView() // count resets to 0 when toggled
105
+ }
106
+
107
+ // Better: preserve identity with opacity/hidden
108
+ CounterView()
109
+ .opacity(showCounter ? 1 : 0) // State preserved
110
+ ```
111
+
112
+ ## Debugging Identity
113
+
114
+ Use `Self._printChanges()` to see what triggers body recomputation:
115
+
116
+ ```swift
117
+ var body: some View {
118
+ let _ = Self._printChanges() // Xcode console shows changed properties
119
+
120
+ VStack {
121
+ Text("Content")
122
+ }
123
+ }
124
+ ```
125
+ </view_identity>
126
+
127
+ <lazy_containers>
128
+ **Lazy containers** create views on-demand as they scroll into view, rather than creating all views upfront.
129
+
130
+ ## Lazy Stack Types
131
+
132
+ ```swift
133
+ // LazyVStack - vertical scrolling
134
+ ScrollView {
135
+ LazyVStack(spacing: 16) {
136
+ ForEach(items) { item in
137
+ ItemRow(item: item) // Created only when visible
138
+ }
139
+ }
140
+ }
141
+
142
+ // LazyHStack - horizontal scrolling
143
+ ScrollView(.horizontal) {
144
+ LazyHStack(spacing: 16) {
145
+ ForEach(items) { item in
146
+ ItemCard(item: item)
147
+ }
148
+ }
149
+ }
150
+
151
+ // LazyVGrid - grid layout
152
+ ScrollView {
153
+ LazyVGrid(columns: [
154
+ GridItem(.adaptive(minimum: 150))
155
+ ], spacing: 16) {
156
+ ForEach(items) { item in
157
+ ItemCard(item: item)
158
+ }
159
+ }
160
+ }
161
+
162
+ // LazyHGrid - horizontal grid
163
+ ScrollView(.horizontal) {
164
+ LazyHGrid(rows: [
165
+ GridItem(.fixed(100)),
166
+ GridItem(.fixed(100))
167
+ ], spacing: 16) {
168
+ ForEach(items) { item in
169
+ ItemCard(item: item)
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ ## Performance Characteristics
176
+
177
+ **Benefits**:
178
+ - **Reduced Memory**: 80-90% less memory than non-lazy equivalents for large lists
179
+ - **Faster Load**: Milliseconds vs seconds for initial render
180
+ - **Smooth Scrolling**: Maintains 60fps even with hundreds of items
181
+
182
+ **Tradeoffs**:
183
+ - Views created lazily incur small bookkeeping overhead
184
+ - Once created, views stay in memory (not recycled like UITableView)
185
+ - For very large datasets (thousands of items), List provides view recycling
186
+
187
+ ```swift
188
+ // Memory comparison for 200 items:
189
+ // VStack: ~300MB, 2-3 second load
190
+ // LazyVStack: ~40MB, <100ms load
191
+ // List: ~40MB with view recycling (better for 1000+ items)
192
+ ```
193
+
194
+ ## When to Use Lazy Containers
195
+
196
+ **Use LazyVStack/LazyHStack when**:
197
+ - Scrolling list with dozens to hundreds of items
198
+ - Items contain images, videos, or heavy views
199
+ - Custom animations and transitions required
200
+ - ScrollView directly wraps the stack
201
+
202
+ **Use List when**:
203
+ - Thousands of items (view recycling needed)
204
+ - Standard list appearance acceptable
205
+ - Platform-native behavior desired
206
+
207
+ **Avoid Lazy when**:
208
+ - Small number of items (< 20)
209
+ - All views fit on screen without scrolling
210
+ - Lazy overhead exceeds benefit (profile first)
211
+
212
+ ```swift
213
+ // Decision example
214
+ struct ContentView: View {
215
+ let items: [Item]
216
+
217
+ var body: some View {
218
+ ScrollView {
219
+ if items.count > 50 {
220
+ // Use lazy for large lists
221
+ LazyVStack {
222
+ ForEach(items) { ItemRow(item: $0) }
223
+ }
224
+ } else {
225
+ // Regular stack fine for small lists
226
+ VStack {
227
+ ForEach(items) { ItemRow(item: $0) }
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+ ```
234
+
235
+ ## Lazy Container Best Practices
236
+
237
+ **Design Lightweight Views**: Lazy loading doesn't eliminate cost of heavy views.
238
+
239
+ ```swift
240
+ // Bad - heavy view defeats lazy loading benefits
241
+ struct ItemRow: View {
242
+ let item: Item
243
+
244
+ var body: some View {
245
+ VStack {
246
+ AsyncImage(url: item.imageURL) { image in
247
+ image.resizable() // No size limit - uses full resolution
248
+ } placeholder: {
249
+ ProgressView()
250
+ }
251
+ Text(item.longDescription) // Renders all text upfront
252
+ }
253
+ }
254
+ }
255
+
256
+ // Good - lightweight view
257
+ struct ItemRow: View {
258
+ let item: Item
259
+
260
+ var body: some View {
261
+ VStack {
262
+ AsyncImage(url: item.imageURL) { image in
263
+ image
264
+ .resizable()
265
+ .aspectRatio(contentMode: .fill)
266
+ .frame(height: 200) // Limit size
267
+ .clipped()
268
+ } placeholder: {
269
+ Color.gray.frame(height: 200)
270
+ }
271
+ Text(item.shortDescription) // Just what's visible
272
+ }
273
+ }
274
+ }
275
+ ```
276
+
277
+ **Pinned Views**: Use for sticky headers/footers.
278
+
279
+ ```swift
280
+ LazyVStack(pinnedViews: [.sectionHeaders]) {
281
+ ForEach(sections) { section in
282
+ Section {
283
+ ForEach(section.items) { item in
284
+ ItemRow(item: item)
285
+ }
286
+ } header: {
287
+ Text(section.title)
288
+ .font(.headline)
289
+ .frame(maxWidth: .infinity, alignment: .leading)
290
+ .background(Color.gray.opacity(0.2))
291
+ }
292
+ }
293
+ }
294
+ ```
295
+ </lazy_containers>
296
+
297
+ <body_recomputation>
298
+ Understanding what triggers body recomputation and how to minimize it is critical for performance.
299
+
300
+ ## What Triggers Body Evaluation
301
+
302
+ SwiftUI evaluates body when:
303
+
304
+ 1. **@State property changes**: View owns the state
305
+ 2. **@Binding updates**: Parent changed bound value
306
+ 3. **@Observable property accessed in body changes**: Fine-grained observation
307
+ 4. **ObservableObject publishes change**: Any @Published property (not fine-grained)
308
+ 5. **@Environment value changes**: Environment changed
309
+ 6. **Parent view recreates child**: Parent's body evaluated with different child value
310
+
311
+ ```swift
312
+ struct ProfileView: View {
313
+ @State private var name = "User" // Change triggers body
314
+ @State private var age = 25 // Change triggers body
315
+ let id: UUID // Never changes - no trigger
316
+
317
+ var body: some View {
318
+ let _ = Self._printChanges() // Debug what changed
319
+
320
+ VStack {
321
+ Text("Name: \(name)") // Depends on name
322
+ Text("Age: \(age)") // Depends on age
323
+ }
324
+ }
325
+ }
326
+ ```
327
+
328
+ ## Minimizing Recomputation
329
+
330
+ ### Extract Subviews
331
+
332
+ Move stable content into separate views to prevent recomputation:
333
+
334
+ ```swift
335
+ // Bad - entire body recomputes on count change
336
+ struct ContentView: View {
337
+ @State private var count = 0
338
+
339
+ var body: some View {
340
+ VStack {
341
+ ExpensiveHeaderView() // Recomputes unnecessarily
342
+ Text("Count: \(count)")
343
+ Button("Increment") { count += 1 }
344
+ ExpensiveFooterView() // Recomputes unnecessarily
345
+ }
346
+ }
347
+ }
348
+
349
+ // Good - isolate expensive views
350
+ struct ContentView: View {
351
+ @State private var count = 0
352
+
353
+ var body: some View {
354
+ VStack {
355
+ StaticHeader() // Separate view - stable identity
356
+ CounterDisplay(count: count) // Only this recomputes
357
+ StaticFooter() // Separate view - stable identity
358
+ }
359
+ }
360
+ }
361
+
362
+ struct StaticHeader: View {
363
+ var body: some View {
364
+ ExpensiveHeaderView() // Body only called once
365
+ }
366
+ }
367
+ ```
368
+
369
+ ### Avoid Expensive Computations in Body
370
+
371
+ ```swift
372
+ // Bad - recalculates on every body evaluation
373
+ struct ListView: View {
374
+ let items: [Item]
375
+
376
+ var body: some View {
377
+ let sortedItems = items.sorted { $0.date > $1.date } // Expensive!
378
+ List(sortedItems) { item in
379
+ Text(item.name)
380
+ }
381
+ }
382
+ }
383
+
384
+ // Good - compute once, cache result
385
+ struct ListView: View {
386
+ let items: [Item]
387
+
388
+ private var sortedItems: [Item] {
389
+ items.sorted { $0.date > $1.date }
390
+ }
391
+
392
+ var body: some View {
393
+ List(sortedItems) { item in
394
+ Text(item.name)
395
+ }
396
+ }
397
+ }
398
+
399
+ // Better - compute outside view if possible
400
+ struct ListView: View {
401
+ let sortedItems: [Item] // Parent sorted once
402
+
403
+ var body: some View {
404
+ List(sortedItems) { item in
405
+ Text(item.name)
406
+ }
407
+ }
408
+ }
409
+ ```
410
+
411
+ ### Use Equatable for Custom Comparison
412
+
413
+ Tell SwiftUI exactly when to recompute by conforming to Equatable:
414
+
415
+ ```swift
416
+ struct ItemDetailView: View, Equatable {
417
+ let item: Item
418
+ let metadata: Metadata // Large, rarely changes
419
+
420
+ static func == (lhs: ItemDetailView, rhs: ItemDetailView) -> Bool {
421
+ lhs.item.id == rhs.item.id // Only recompute if item ID changes
422
+ // Ignores metadata changes
423
+ }
424
+
425
+ var body: some View {
426
+ VStack {
427
+ Text(item.name)
428
+ Text(metadata.description)
429
+ }
430
+ }
431
+ }
432
+
433
+ // Use with .equatable() modifier
434
+ ParentView {
435
+ ItemDetailView(item: item, metadata: metadata)
436
+ .equatable() // Uses custom == for comparison
437
+ }
438
+ ```
439
+
440
+ ### Scope Data Sources Appropriately
441
+
442
+ ```swift
443
+ // Bad - entire hierarchy recomputes
444
+ struct AppView: View {
445
+ @State private var settings = AppSettings() // Top-level state
446
+
447
+ var body: some View {
448
+ NavigationStack {
449
+ ContentView() // Recomputes when settings change
450
+ .environment(settings)
451
+ }
452
+ }
453
+ }
454
+
455
+ // Good - state lives close to where it's used
456
+ struct SettingsButton: View {
457
+ @State private var showSettings = false // Local state
458
+
459
+ var body: some View {
460
+ Button("Settings") { showSettings = true }
461
+ .sheet(isPresented: $showSettings) {
462
+ SettingsView()
463
+ }
464
+ }
465
+ }
466
+ ```
467
+
468
+ ## Understanding Body Evaluation vs Rendering
469
+
470
+ **Critical distinction**: Body evaluation ≠ rendering to screen.
471
+
472
+ ```swift
473
+ // Body evaluated frequently...
474
+ struct CounterView: View {
475
+ @State private var count = 0
476
+
477
+ var body: some View {
478
+ // This code runs on every evaluation
479
+ VStack {
480
+ Text("Count: \(count)") // ...but SwiftUI only renders if text changed
481
+ Button("Increment") { count += 1 }
482
+ }
483
+ }
484
+ }
485
+ ```
486
+
487
+ SwiftUI evaluates body, then diffs the result. If nothing changed, no rendering occurs. This is why expensive computations hurt even if output is identical.
488
+ </body_recomputation>
489
+
490
+ <observable_performance>
491
+ **@Observable** (iOS 17+) provides superior performance compared to ObservableObject through fine-grained change tracking.
492
+
493
+ ## ObservableObject Limitations
494
+
495
+ ```swift
496
+ // ObservableObject - coarse-grained updates
497
+ class UserSettings: ObservableObject {
498
+ @Published var username = "User"
499
+ @Published var theme = "Light"
500
+ @Published var notifications = true
501
+ }
502
+
503
+ struct ProfileView: View {
504
+ @ObservedObject var settings: UserSettings
505
+
506
+ var body: some View {
507
+ VStack {
508
+ Text(settings.username) // Only reads username...
509
+ }
510
+ // ...but body recomputes when theme or notifications change!
511
+ }
512
+ }
513
+ ```
514
+
515
+ **Problem**: If ANY @Published property changes, ALL views observing the object recompute, regardless of which properties they actually read.
516
+
517
+ ## @Observable Solution
518
+
519
+ ```swift
520
+ // @Observable - fine-grained updates
521
+ @Observable
522
+ class UserSettings {
523
+ var username = "User"
524
+ var theme = "Light"
525
+ var notifications = true
526
+ }
527
+
528
+ struct ProfileView: View {
529
+ @State var settings: UserSettings
530
+
531
+ var body: some View {
532
+ VStack {
533
+ Text(settings.username) // Only reads username...
534
+ }
535
+ // ...body only recomputes when username changes!
536
+ }
537
+ }
538
+ ```
539
+
540
+ **Benefit**: Body only evaluates when properties actually accessed in body change. Automatic, compiler-generated tracking.
541
+
542
+ ## Performance Impact
543
+
544
+ Real-world measurements:
545
+
546
+ - **80-90% fewer body evaluations** for views reading subset of properties
547
+ - **No Combine overhead**: @Observable uses Swift's observation system, not Combine
548
+ - **Automatic optimization**: No manual effort to minimize updates
549
+
550
+ ```swift
551
+ // Performance comparison
552
+ @Observable
553
+ class DataStore {
554
+ var items: [Item] = [] // Changes frequently
555
+ var settings: Settings = .default // Changes rarely
556
+ }
557
+
558
+ struct ItemListView: View {
559
+ @State var store: DataStore
560
+
561
+ var body: some View {
562
+ // With ObservableObject: recomputes on settings change (unnecessary)
563
+ // With @Observable: only recomputes on items change (correct)
564
+ List(store.items) { item in
565
+ ItemRow(item: item)
566
+ }
567
+ }
568
+ }
569
+ ```
570
+
571
+ ## Migration Guidelines
572
+
573
+ **Use @Observable for new code**. It's simpler and faster:
574
+
575
+ ```swift
576
+ // Old pattern - remove
577
+ class ViewModel: ObservableObject {
578
+ @Published var name = ""
579
+ @Published var count = 0
580
+ }
581
+
582
+ struct OldView: View {
583
+ @StateObject private var viewModel = ViewModel() // ObservableObject
584
+ }
585
+
586
+ // New pattern - use
587
+ @Observable
588
+ class ViewModel {
589
+ var name = ""
590
+ var count = 0
591
+ }
592
+
593
+ struct NewView: View {
594
+ @State private var viewModel = ViewModel() // @Observable
595
+ }
596
+ ```
597
+
598
+ **Key differences**:
599
+
600
+ 1. No ObservableObject conformance
601
+ 2. No @Published wrapper
602
+ 3. Use @State (not @StateObject) for ownership
603
+ 4. Use @Bindable for bindings
604
+
605
+ ```swift
606
+ @Observable
607
+ class FormData {
608
+ var name = ""
609
+ var email = ""
610
+ }
611
+
612
+ struct FormView: View {
613
+ @State private var formData = FormData()
614
+
615
+ var body: some View {
616
+ Form {
617
+ // Need @Bindable for bindings
618
+ TextField("Name", text: $formData.name)
619
+ TextField("Email", text: $formData.email)
620
+ }
621
+ }
622
+ }
623
+
624
+ // Alternative: @Bindable parameter
625
+ struct FormFields: View {
626
+ @Bindable var formData: FormData
627
+
628
+ var body: some View {
629
+ Form {
630
+ TextField("Name", text: $formData.name)
631
+ TextField("Email", text: $formData.email)
632
+ }
633
+ }
634
+ }
635
+ ```
636
+
637
+ ## Important: @State Behavior Difference
638
+
639
+ Critical difference between @StateObject and @State:
640
+
641
+ ```swift
642
+ // ObservableObject with @StateObject
643
+ class OldModel: ObservableObject {
644
+ init() { print("OldModel init") }
645
+ }
646
+
647
+ struct OldView: View {
648
+ @StateObject private var model = OldModel()
649
+ // Prints "OldModel init" ONCE - @StateObject preserves across view recreations
650
+ }
651
+
652
+ // @Observable with @State
653
+ @Observable
654
+ class NewModel {
655
+ init() { print("NewModel init") }
656
+ }
657
+
658
+ struct NewView: View {
659
+ @State private var model = NewModel()
660
+ // Prints "NewModel init" on EVERY view recreation!
661
+ // SwiftUI preserves the instance, but re-runs initializer
662
+ }
663
+ ```
664
+
665
+ **Best practice**: Only use @State for @Observable at the view that creates the instance. Pass to child views without @State:
666
+
667
+ ```swift
668
+ struct ParentView: View {
669
+ @State private var model = DataModel() // Owner uses @State
670
+
671
+ var body: some View {
672
+ ChildView(model: model) // Child receives plain reference
673
+ }
674
+ }
675
+
676
+ struct ChildView: View {
677
+ let model: DataModel // NOT @State
678
+
679
+ var body: some View {
680
+ Text(model.name) // Still reactive - automatic observation
681
+ }
682
+ }
683
+ ```
684
+ </observable_performance>
685
+
686
+ <images>
687
+ Image loading and rendering are common performance bottlenecks. SwiftUI provides AsyncImage for remote images, but requires careful optimization.
688
+
689
+ ## AsyncImage Basics
690
+
691
+ ```swift
692
+ // Basic AsyncImage
693
+ AsyncImage(url: URL(string: "https://example.com/image.jpg")) { image in
694
+ image
695
+ .resizable()
696
+ .aspectRatio(contentMode: .fill)
697
+ } placeholder: {
698
+ ProgressView()
699
+ }
700
+ ```
701
+
702
+ ## Critical Issue: AsyncImage Does Not Cache
703
+
704
+ **Important**: AsyncImage does NOT cache images between screen loads. Scrolling an image off-screen and back may trigger a new network request.
705
+
706
+ ```swift
707
+ // Problem: re-downloads on scroll
708
+ ScrollView {
709
+ LazyVStack {
710
+ ForEach(items) { item in
711
+ AsyncImage(url: item.imageURL) { image in
712
+ image.resizable()
713
+ } placeholder: {
714
+ ProgressView()
715
+ }
716
+ // Scrolls off screen -> image released
717
+ // Scrolls back on screen -> downloads again!
718
+ }
719
+ }
720
+ }
721
+ ```
722
+
723
+ ## Solution 1: Configure URLCache
724
+
725
+ AsyncImage uses URLSession.shared, which respects URLCache. Configure cache size:
726
+
727
+ ```swift
728
+ // In @main App init
729
+ @main
730
+ struct MyApp: App {
731
+ init() {
732
+ // Configure URLCache for AsyncImage
733
+ URLCache.shared.memoryCapacity = 50_000_000 // 50 MB memory
734
+ URLCache.shared.diskCapacity = 1_000_000_000 // 1 GB disk
735
+ }
736
+
737
+ var body: some Scene {
738
+ WindowGroup {
739
+ ContentView()
740
+ }
741
+ }
742
+ }
743
+ ```
744
+
745
+ **Limitation**: URLCache respects HTTP cache headers. If server doesn't provide appropriate headers, caching may not work as expected.
746
+
747
+ ## Solution 2: Build Custom Cached AsyncImage
748
+
749
+ Use NSCache for in-memory caching with custom control:
750
+
751
+ ```swift
752
+ // Image cache manager
753
+ @Observable
754
+ class ImageCache {
755
+ static let shared = ImageCache()
756
+ private var cache = NSCache<NSString, UIImage>()
757
+
758
+ func get(url: String) -> UIImage? {
759
+ cache.object(forKey: url as NSString)
760
+ }
761
+
762
+ func set(url: String, image: UIImage) {
763
+ cache.setObject(image, forKey: url as NSString)
764
+ }
765
+ }
766
+
767
+ // Cached AsyncImage view
768
+ struct CachedAsyncImage<Content: View, Placeholder: View>: View {
769
+ let url: URL?
770
+ let content: (Image) -> Content
771
+ let placeholder: () -> Placeholder
772
+
773
+ @State private var image: UIImage?
774
+
775
+ var body: some View {
776
+ Group {
777
+ if let image {
778
+ content(Image(uiImage: image))
779
+ } else {
780
+ placeholder()
781
+ .task {
782
+ await loadImage()
783
+ }
784
+ }
785
+ }
786
+ }
787
+
788
+ private func loadImage() async {
789
+ guard let url else { return }
790
+
791
+ // Check cache first
792
+ if let cached = ImageCache.shared.get(url: url.absoluteString) {
793
+ image = cached
794
+ return
795
+ }
796
+
797
+ // Download if not cached
798
+ do {
799
+ let (data, _) = try await URLSession.shared.data(from: url)
800
+ if let downloaded = UIImage(data: data) {
801
+ ImageCache.shared.set(url: url.absoluteString, image: downloaded)
802
+ image = downloaded
803
+ }
804
+ } catch {
805
+ print("Failed to load image: \(error)")
806
+ }
807
+ }
808
+ }
809
+ ```
810
+
811
+ ## Solution 3: Use Third-Party Libraries
812
+
813
+ For production apps, consider mature image loading libraries:
814
+
815
+ - **Nuke**: High-performance image loading with aggressive caching
816
+ - **Kingfisher**: Feature-rich with SwiftUI support
817
+ - **SDWebImage**: Battle-tested, widely used
818
+
819
+ ```swift
820
+ // Example with third-party library
821
+ import Nuke
822
+ import NukeUI
823
+
824
+ LazyImage(url: item.imageURL) { state in
825
+ if let image = state.image {
826
+ image.resizable().aspectRatio(contentMode: .fill)
827
+ } else {
828
+ ProgressView()
829
+ }
830
+ }
831
+ ```
832
+
833
+ ## Image Sizing Best Practices
834
+
835
+ **Always specify image dimensions** to prevent SwiftUI from using full resolution:
836
+
837
+ ```swift
838
+ // Bad - loads full resolution image
839
+ AsyncImage(url: imageURL) { image in
840
+ image.resizable() // Loads 4K image for 100x100 display
841
+ }
842
+
843
+ // Good - constrains size
844
+ AsyncImage(url: imageURL) { image in
845
+ image
846
+ .resizable()
847
+ .aspectRatio(contentMode: .fill)
848
+ .frame(width: 100, height: 100) // Limits memory usage
849
+ .clipped()
850
+ }
851
+
852
+ // Better - serve appropriately sized images
853
+ // Use CDN or server-side resizing to deliver thumbnails, not full resolution
854
+ AsyncImage(url: item.thumbnailURL) { image in // 200x200 version
855
+ image
856
+ .resizable()
857
+ .aspectRatio(contentMode: .fill)
858
+ .frame(width: 100, height: 100)
859
+ }
860
+ ```
861
+
862
+ ## Prefetching for Scrolling
863
+
864
+ Prefetch images just before they become visible:
865
+
866
+ ```swift
867
+ struct OptimizedImageList: View {
868
+ let items: [Item]
869
+
870
+ var body: some View {
871
+ ScrollView {
872
+ LazyVStack {
873
+ ForEach(items) { item in
874
+ CachedAsyncImage(url: item.imageURL) { image in
875
+ image.resizable()
876
+ } placeholder: {
877
+ Color.gray
878
+ }
879
+ .onAppear {
880
+ // Prefetch next items
881
+ prefetchNextImages(after: item)
882
+ }
883
+ }
884
+ }
885
+ }
886
+ }
887
+
888
+ private func prefetchNextImages(after item: Item) {
889
+ guard let index = items.firstIndex(where: { $0.id == item.id }) else { return }
890
+ let nextItems = items.dropFirst(index + 1).prefix(3)
891
+
892
+ Task {
893
+ for nextItem in nextItems {
894
+ // Start download without displaying
895
+ _ = try? await URLSession.shared.data(from: nextItem.imageURL)
896
+ }
897
+ }
898
+ }
899
+ }
900
+ ```
901
+ </images>
902
+
903
+ <instruments>
904
+ Xcode's Instruments app provides powerful profiling for SwiftUI performance analysis.
905
+
906
+ ## Starting a Profile Session
907
+
908
+ 1. Build in Release mode: Product > Profile (Cmd+I)
909
+ 2. Select **SwiftUI** template (Xcode 16+) or **Time Profiler** (earlier versions)
910
+ 3. **Always profile on real device**, never simulator
911
+
912
+ ```bash
913
+ # Release mode optimizations match production
914
+ # Simulator performance doesn't reflect real device
915
+ ```
916
+
917
+ ## SwiftUI Instruments Template (Xcode 16+)
918
+
919
+ The SwiftUI template includes specialized tracks:
920
+
921
+ ### 1. Update Groups Lane
922
+
923
+ Shows when SwiftUI is performing update work. If CPU spikes when this lane is empty, the bottleneck is outside SwiftUI (networking, data processing, etc.).
924
+
925
+ ### 2. View Body Lane
926
+
927
+ Tracks how often view body properties are evaluated.
928
+
929
+ **Key metrics**:
930
+ - **Count**: Number of times body evaluated
931
+ - **Avg Duration**: Average time per evaluation
932
+ - **Total Duration**: Cumulative time
933
+
934
+ **What to look for**:
935
+ - Views with high count but low duration: Unnecessary evaluations (fix with Equatable, extract subviews)
936
+ - Views with high duration: Expensive computations in body (move outside body)
937
+
938
+ ### 3. View Properties Lane
939
+
940
+ Shows every view property change. Property updates are more frequent than body updates (SwiftUI batches multiple property changes into single body update).
941
+
942
+ **Use to identify**:
943
+ - Properties updating more frequently than expected
944
+ - Cascading updates from parent to children
945
+
946
+ ### 4. Core Animation Commits Lane
947
+
948
+ Shows when SwiftUI commits changes to Core Animation for rendering. Expensive commits indicate actual pixel changes on screen.
949
+
950
+ **Correlation**:
951
+ - Many body evaluations + few commits = good (SwiftUI diffing working)
952
+ - Many commits = actual rendering work (investigate why so many pixel changes)
953
+
954
+ ### 5. Time Profiler Lane
955
+
956
+ Shows CPU usage by function. Reveals which code is running and how long.
957
+
958
+ **How to use**:
959
+ 1. Record profile session
960
+ 2. Stop after representative user interaction
961
+ 3. Look for heavy call stacks
962
+ 4. Drill into SwiftUI view types to find bottlenecks
963
+
964
+ ## Analyzing Body Evaluations
965
+
966
+ After profiling, Instruments shows "All Updates Summary":
967
+
968
+ ```swift
969
+ // Example summary
970
+ ViewType Count Avg Duration Total Duration
971
+ ------------------------------------------------------------------
972
+ ProductListView 456 2.3ms 1,048ms
973
+ ProductCard 2,340 0.8ms 1,872ms
974
+ HeaderView 1 0.2ms 0.2ms
975
+ ```
976
+
977
+ **Interpretation**:
978
+ - ProductListView: 456 evaluations in one session is suspicious - should be much fewer
979
+ - ProductCard: High count expected (many instances), but 0.8ms average is acceptable
980
+ - HeaderView: 1 evaluation is ideal for static content
981
+
982
+ ## Finding Excessive Updates
983
+
984
+ Use Cmd+1 or select "Summary: All Updates" from jump bar:
985
+
986
+ ```swift
987
+ // Views updating the most appear at top
988
+ // Click view name -> see what triggered updates
989
+ ```
990
+
991
+ Look for:
992
+ - Static views updating repeatedly (should be 1-2 times)
993
+ - Views updating when dependencies haven't changed
994
+ - Cascading updates (parent change triggers all children)
995
+
996
+ ## Debugging with _printChanges()
997
+
998
+ Combine Instruments with runtime debugging:
999
+
1000
+ ```swift
1001
+ struct ProblematicView: View {
1002
+ @State private var count = 0
1003
+ @State private var name = "Test"
1004
+
1005
+ var body: some View {
1006
+ let _ = Self._printChanges() // Prints to Xcode console
1007
+
1008
+ VStack {
1009
+ Text("Count: \(count)")
1010
+ Text("Name: \(name)")
1011
+ }
1012
+ }
1013
+ }
1014
+
1015
+ // Console output when count changes:
1016
+ // ProblematicView: @self, @identity, _count changed.
1017
+ ```
1018
+
1019
+ ## Common Findings and Solutions
1020
+
1021
+ | Finding | Cause | Solution |
1022
+ |---------|-------|----------|
1023
+ | Header view updates 100+ times | Parent state change | Extract to separate view |
1024
+ | Image view high duration | Full resolution loading | Constrain frame size |
1025
+ | List scrolling causes body storm | Expensive row computations | Move computation outside body |
1026
+ | State changes cause app-wide updates | Top-level state | Move state closer to usage |
1027
+
1028
+ ## Weekly Profiling Practice
1029
+
1030
+ Profile incrementally to catch performance regressions early:
1031
+
1032
+ ```swift
1033
+ // Profiling routine
1034
+ 1. Profile baseline before changes
1035
+ 2. Implement feature
1036
+ 3. Profile again
1037
+ 4. Compare metrics
1038
+ 5. Fix regressions before merging
1039
+ ```
1040
+
1041
+ Small, consistent profiling catches issues when they're easy to fix, rather than debugging performance problems across large changesets.
1042
+ </instruments>
1043
+
1044
+ <optimization_strategies>
1045
+ Specific techniques for optimizing SwiftUI performance.
1046
+
1047
+ ## 1. Task Prioritization with Priority
1048
+
1049
+ Control async task priority to keep UI responsive:
1050
+
1051
+ ```swift
1052
+ struct DataLoadingView: View {
1053
+ @State private var essentialData: [Item] = []
1054
+ @State private var optionalData: [Detail] = []
1055
+
1056
+ var body: some View {
1057
+ VStack {
1058
+ List(essentialData) { item in
1059
+ ItemRow(item: item)
1060
+ }
1061
+ }
1062
+ .task(priority: .high) {
1063
+ // Load critical data first
1064
+ essentialData = await dataStore.loadEssentialData()
1065
+ }
1066
+ .task(priority: .low) {
1067
+ // Load nice-to-have data later
1068
+ optionalData = await dataStore.loadOptionalData()
1069
+ }
1070
+ }
1071
+ }
1072
+ ```
1073
+
1074
+ ## 2. Pagination for Large Datasets
1075
+
1076
+ Load data in chunks as user scrolls:
1077
+
1078
+ ```swift
1079
+ struct PaginatedListView: View {
1080
+ @State private var items: [Item] = []
1081
+ @State private var page = 1
1082
+ @State private var isLoading = false
1083
+
1084
+ var body: some View {
1085
+ ScrollView {
1086
+ LazyVStack {
1087
+ ForEach(items) { item in
1088
+ ItemRow(item: item)
1089
+ .onAppear {
1090
+ loadMoreIfNeeded(currentItem: item)
1091
+ }
1092
+ }
1093
+
1094
+ if isLoading {
1095
+ ProgressView()
1096
+ }
1097
+ }
1098
+ }
1099
+ .task {
1100
+ await loadPage()
1101
+ }
1102
+ }
1103
+
1104
+ private func loadMoreIfNeeded(currentItem: Item) {
1105
+ guard let index = items.firstIndex(where: { $0.id == currentItem.id }) else { return }
1106
+
1107
+ // Load next page when reaching last 5 items
1108
+ if index >= items.count - 5 && !isLoading {
1109
+ Task {
1110
+ await loadPage()
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ private func loadPage() async {
1116
+ guard !isLoading else { return }
1117
+ isLoading = true
1118
+
1119
+ let newItems = await dataStore.loadItems(page: page)
1120
+ items.append(contentsOf: newItems)
1121
+ page += 1
1122
+
1123
+ isLoading = false
1124
+ }
1125
+ }
1126
+ ```
1127
+
1128
+ ## 3. Debouncing Expensive Updates
1129
+
1130
+ Delay expensive operations while user is typing:
1131
+
1132
+ ```swift
1133
+ struct SearchView: View {
1134
+ @State private var searchText = ""
1135
+ @State private var searchResults: [Item] = []
1136
+ @State private var searchTask: Task<Void, Never>?
1137
+
1138
+ var body: some View {
1139
+ VStack {
1140
+ TextField("Search", text: $searchText)
1141
+ .onChange(of: searchText) { oldValue, newValue in
1142
+ // Cancel previous search
1143
+ searchTask?.cancel()
1144
+
1145
+ // Debounce: wait 300ms before searching
1146
+ searchTask = Task {
1147
+ try? await Task.sleep(for: .milliseconds(300))
1148
+ guard !Task.isCancelled else { return }
1149
+ await performSearch(query: newValue)
1150
+ }
1151
+ }
1152
+
1153
+ List(searchResults) { result in
1154
+ Text(result.name)
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ private func performSearch(query: String) async {
1160
+ searchResults = await searchService.search(query)
1161
+ }
1162
+ }
1163
+ ```
1164
+
1165
+ ## 4. Drawing Performance with Canvas
1166
+
1167
+ For complex custom drawing, use Canvas instead of GeometryReader and Path:
1168
+
1169
+ ```swift
1170
+ // Slow - triggers relayout frequently
1171
+ struct SlowGraph: View {
1172
+ let data: [Double]
1173
+
1174
+ var body: some View {
1175
+ GeometryReader { geometry in
1176
+ Path { path in
1177
+ // Complex path drawing
1178
+ for (index, value) in data.enumerated() {
1179
+ let x = CGFloat(index) * geometry.size.width / CGFloat(data.count)
1180
+ let y = geometry.size.height * (1 - CGFloat(value))
1181
+ if index == 0 {
1182
+ path.move(to: CGPoint(x: x, y: y))
1183
+ } else {
1184
+ path.addLine(to: CGPoint(x: x, y: y))
1185
+ }
1186
+ }
1187
+ }
1188
+ .stroke(Color.blue, lineWidth: 2)
1189
+ }
1190
+ }
1191
+ }
1192
+
1193
+ // Fast - optimized drawing
1194
+ struct FastGraph: View {
1195
+ let data: [Double]
1196
+
1197
+ var body: some View {
1198
+ Canvas { context, size in
1199
+ var path = Path()
1200
+ for (index, value) in data.enumerated() {
1201
+ let x = CGFloat(index) * size.width / CGFloat(data.count)
1202
+ let y = size.height * (1 - CGFloat(value))
1203
+ if index == 0 {
1204
+ path.move(to: CGPoint(x: x, y: y))
1205
+ } else {
1206
+ path.addLine(to: CGPoint(x: x, y: y))
1207
+ }
1208
+ }
1209
+ context.stroke(path, with: .color(.blue), lineWidth: 2)
1210
+ }
1211
+ }
1212
+ }
1213
+ ```
1214
+
1215
+ ## 5. Reduce Modifier Overhead
1216
+
1217
+ Combine modifiers when possible:
1218
+
1219
+ ```swift
1220
+ // Multiple modifier evaluations
1221
+ Text("Hello")
1222
+ .foregroundStyle(.blue)
1223
+ .font(.headline)
1224
+ .padding()
1225
+ .background(.gray)
1226
+ .cornerRadius(8)
1227
+
1228
+ // Combined where possible - no performance gain in most cases,
1229
+ // but clearer code. SwiftUI optimizes modifier chains automatically.
1230
+ // Real optimization: avoid conditional modifiers if value doesn't change.
1231
+
1232
+ // Inefficient - creates new modifier on every body evaluation
1233
+ .opacity(isVisible ? 1.0 : 1.0) // Condition always results in same value
1234
+
1235
+ // Efficient - only apply when needed
1236
+ .opacity(isVisible ? 1.0 : 0.0)
1237
+ ```
1238
+
1239
+ ## 6. PreferenceKey for Bottom-Up Communication
1240
+
1241
+ Use PreferenceKey instead of @Binding for child-to-parent data flow when performance matters:
1242
+
1243
+ ```swift
1244
+ struct SizePreferenceKey: PreferenceKey {
1245
+ static var defaultValue: CGSize = .zero
1246
+ static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
1247
+ value = nextValue()
1248
+ }
1249
+ }
1250
+
1251
+ struct ParentView: View {
1252
+ @State private var childSize: CGSize = .zero
1253
+
1254
+ var body: some View {
1255
+ VStack {
1256
+ Text("Child size: \(childSize.width) x \(childSize.height)")
1257
+
1258
+ ChildView()
1259
+ .onPreferenceChange(SizePreferenceKey.self) { size in
1260
+ childSize = size
1261
+ }
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ struct ChildView: View {
1267
+ var body: some View {
1268
+ Text("Hello")
1269
+ .background(
1270
+ GeometryReader { geometry in
1271
+ Color.clear.preference(
1272
+ key: SizePreferenceKey.self,
1273
+ value: geometry.size
1274
+ )
1275
+ }
1276
+ )
1277
+ }
1278
+ }
1279
+ ```
1280
+ </optimization_strategies>
1281
+
1282
+ <decision_tree>
1283
+ When to investigate performance and what to optimize.
1284
+
1285
+ ## Should You Optimize?
1286
+
1287
+ ```
1288
+ Is there a user-facing performance issue?
1289
+ ├─ No → Don't optimize
1290
+ └─ Yes → Continue
1291
+
1292
+ Have you profiled with Instruments?
1293
+ ├─ No → Profile first (never optimize without data)
1294
+ └─ Yes → Continue
1295
+
1296
+ Did profiling identify a specific bottleneck?
1297
+ ├─ No → Issue might not be SwiftUI (check networking, data layer)
1298
+ └─ Yes → Continue
1299
+
1300
+ Is the bottleneck in SwiftUI view updates?
1301
+ ├─ No → Optimize data layer, networking, image loading
1302
+ └─ Yes → Continue to optimization strategies
1303
+ ```
1304
+
1305
+ ## Optimization Priority
1306
+
1307
+ **1. High Impact, Low Effort**:
1308
+ - Switch VStack to LazyVStack for long lists
1309
+ - Configure URLCache for AsyncImage
1310
+ - Extract static subviews from frequently updating views
1311
+
1312
+ **2. High Impact, Medium Effort**:
1313
+ - Replace ObservableObject with @Observable
1314
+ - Implement pagination for large datasets
1315
+ - Add custom Equatable to expensive views
1316
+
1317
+ **3. Medium Impact, Low Effort**:
1318
+ - Debounce text field updates
1319
+ - Use task priority for non-critical work
1320
+ - Constrain image sizes with frame modifiers
1321
+
1322
+ **4. Medium Impact, High Effort**:
1323
+ - Build custom cached image loading
1324
+ - Rewrite complex views to reduce body complexity
1325
+ - Implement view recycling for very large datasets
1326
+
1327
+ **5. Low Priority** (only if profiling shows specific issue):
1328
+ - Optimize modifier ordering
1329
+ - Use Canvas for complex drawing
1330
+ - PreferenceKey instead of Binding
1331
+
1332
+ ## Red Flags Requiring Optimization
1333
+
1334
+ | Symptom | Likely Cause | Action |
1335
+ |---------|--------------|--------|
1336
+ | Scrolling stutters | Heavy row views | Profile, use lazy loading, simplify rows |
1337
+ | Typing lags | Expensive search on every keystroke | Debounce, move work off main thread |
1338
+ | Navigation slow | Loading all data upfront | Implement pagination, async loading |
1339
+ | App hangs on launch | Too much work in view init | Move to task, use loading states |
1340
+ | Memory growing unbounded | Images not releasing | Implement image cache with limits |
1341
+
1342
+ ## Performance Targets
1343
+
1344
+ **Scrolling**: Maintain 60fps (16.67ms per frame)
1345
+ - Budget ~10ms for SwiftUI updates
1346
+ - Budget ~6ms for rendering
1347
+
1348
+ **Interactions**: Respond within 100ms
1349
+ - User perceives instant response < 100ms
1350
+ - 100-300ms feels sluggish
1351
+ - \> 300ms feels broken
1352
+
1353
+ **Launch**: Show content within 1 second
1354
+ - Use skeleton screens / placeholders
1355
+ - Load critical content first, optional content later
1356
+ </decision_tree>
1357
+
1358
+ <anti_patterns>
1359
+ Common performance mistakes and how to avoid them.
1360
+
1361
+ ## 1. Using @State with Reference Types
1362
+
1363
+ **Problem**: @State creates new instance on every view recreation when used with classes.
1364
+
1365
+ ```swift
1366
+ // Wrong - creates new instance repeatedly
1367
+ struct BadView: View {
1368
+ @State private var viewModel = ViewModel() // ViewModel is a class
1369
+
1370
+ var body: some View {
1371
+ Text(viewModel.text)
1372
+ }
1373
+ }
1374
+
1375
+ // Correct - use @Observable and @State for iOS 17+
1376
+ @Observable
1377
+ class ViewModel {
1378
+ var text = "Hello"
1379
+ }
1380
+
1381
+ struct GoodView: View {
1382
+ @State private var viewModel = ViewModel()
1383
+
1384
+ var body: some View {
1385
+ Text(viewModel.text)
1386
+ }
1387
+ }
1388
+
1389
+ // Alternative for iOS 16- - use @StateObject with ObservableObject
1390
+ class LegacyViewModel: ObservableObject {
1391
+ @Published var text = "Hello"
1392
+ }
1393
+
1394
+ struct LegacyView: View {
1395
+ @StateObject private var viewModel = LegacyViewModel()
1396
+
1397
+ var body: some View {
1398
+ Text(viewModel.text)
1399
+ }
1400
+ }
1401
+ ```
1402
+
1403
+ ## 2. Overusing AnyView
1404
+
1405
+ **Problem**: Type erasure prevents SwiftUI from diffing efficiently, forcing complete view recreation.
1406
+
1407
+ ```swift
1408
+ // Wrong - loses type information
1409
+ func makeView(type: ViewType) -> some View {
1410
+ switch type {
1411
+ case .text:
1412
+ return AnyView(Text("Hello"))
1413
+ case .image:
1414
+ return AnyView(Image(systemName: "star"))
1415
+ }
1416
+ }
1417
+
1418
+ // Correct - preserve types with @ViewBuilder
1419
+ @ViewBuilder
1420
+ func makeView(type: ViewType) -> some View {
1421
+ switch type {
1422
+ case .text:
1423
+ Text("Hello")
1424
+ case .image:
1425
+ Image(systemName: "star")
1426
+ }
1427
+ }
1428
+
1429
+ // Alternative - use Group for conditional views
1430
+ var body: some View {
1431
+ Group {
1432
+ if showText {
1433
+ Text("Hello")
1434
+ } else {
1435
+ Image(systemName: "star")
1436
+ }
1437
+ }
1438
+ }
1439
+ ```
1440
+
1441
+ ## 3. Creating New Objects in Body
1442
+
1443
+ **Problem**: Every body evaluation creates new instances, preventing SwiftUI from recognizing stable values.
1444
+
1445
+ ```swift
1446
+ // Wrong - creates new DateFormatter on every body evaluation
1447
+ struct BadDateView: View {
1448
+ let date: Date
1449
+
1450
+ var body: some View {
1451
+ let formatter = DateFormatter() // New instance every time!
1452
+ formatter.dateStyle = .medium
1453
+ return Text(formatter.string(from: date))
1454
+ }
1455
+ }
1456
+
1457
+ // Correct - create once, reuse
1458
+ struct GoodDateView: View {
1459
+ let date: Date
1460
+
1461
+ private static let formatter: DateFormatter = {
1462
+ let f = DateFormatter()
1463
+ f.dateStyle = .medium
1464
+ return f
1465
+ }()
1466
+
1467
+ var body: some View {
1468
+ Text(Self.formatter.string(from: date))
1469
+ }
1470
+ }
1471
+
1472
+ // Alternative - use built-in formatters
1473
+ struct BetterDateView: View {
1474
+ let date: Date
1475
+
1476
+ var body: some View {
1477
+ Text(date, style: .date) // SwiftUI handles formatting
1478
+ }
1479
+ }
1480
+ ```
1481
+
1482
+ ## 4. Branching on View State Instead of Modifiers
1483
+
1484
+ **Problem**: Branches change structural identity, losing state and triggering transitions.
1485
+
1486
+ ```swift
1487
+ // Wrong - structural identity changes on toggle
1488
+ struct BadToggleView: View {
1489
+ @State private var isExpanded = false
1490
+
1491
+ var body: some View {
1492
+ if isExpanded {
1493
+ ExpandedContentView() // Destroyed on collapse
1494
+ } else {
1495
+ CollapsedContentView() // Destroyed on expand
1496
+ }
1497
+ }
1498
+ }
1499
+
1500
+ // Correct - preserve identity with conditional modifiers
1501
+ struct GoodToggleView: View {
1502
+ @State private var isExpanded = false
1503
+
1504
+ var body: some View {
1505
+ ContentView(isExpanded: isExpanded) // Same view, different state
1506
+ }
1507
+ }
1508
+
1509
+ // Alternative - use opacity/frame to hide
1510
+ struct AlternativeToggleView: View {
1511
+ @State private var isExpanded = false
1512
+
1513
+ var body: some View {
1514
+ VStack {
1515
+ HeaderView()
1516
+
1517
+ DetailView()
1518
+ .frame(height: isExpanded ? nil : 0) // Collapse without destroying
1519
+ .opacity(isExpanded ? 1 : 0)
1520
+ }
1521
+ }
1522
+ }
1523
+ ```
1524
+
1525
+ ## 5. Excessive GeometryReader Usage
1526
+
1527
+ **Problem**: GeometryReader recalculates on every layout change, triggering cascade of updates.
1528
+
1529
+ ```swift
1530
+ // Wrong - unnecessary GeometryReader
1531
+ struct BadLayout: View {
1532
+ var body: some View {
1533
+ GeometryReader { geometry in
1534
+ VStack {
1535
+ Text("Width: \(geometry.size.width)")
1536
+ .frame(width: geometry.size.width * 0.8) // Could use .frame(maxWidth:)
1537
+ }
1538
+ }
1539
+ }
1540
+ }
1541
+
1542
+ // Correct - use frame modifiers
1543
+ struct GoodLayout: View {
1544
+ var body: some View {
1545
+ VStack {
1546
+ Text("Responsive width")
1547
+ .frame(maxWidth: .infinity) // Fills available space
1548
+ .padding(.horizontal) // 80% width effect
1549
+ }
1550
+ }
1551
+ }
1552
+
1553
+ // Use GeometryReader only when truly needed
1554
+ struct ValidGeometryUse: View {
1555
+ var body: some View {
1556
+ GeometryReader { geometry in
1557
+ // Valid: need actual size for custom drawing
1558
+ CustomShape(size: geometry.size)
1559
+ }
1560
+ }
1561
+ }
1562
+ ```
1563
+
1564
+ ## 6. Not Using Lazy Containers for Long Lists
1565
+
1566
+ **Problem**: Non-lazy stacks create all views immediately, consuming excessive memory.
1567
+
1568
+ ```swift
1569
+ // Wrong - loads all 1000 items immediately
1570
+ ScrollView {
1571
+ VStack {
1572
+ ForEach(0..<1000) { index in
1573
+ HeavyItemView(index: index) // All 1000 created at once
1574
+ }
1575
+ }
1576
+ }
1577
+
1578
+ // Correct - lazy loading
1579
+ ScrollView {
1580
+ LazyVStack {
1581
+ ForEach(0..<1000) { index in
1582
+ HeavyItemView(index: index) // Created as scrolled into view
1583
+ }
1584
+ }
1585
+ }
1586
+ ```
1587
+
1588
+ ## 7. Performing Expensive Work on Main Thread
1589
+
1590
+ **Problem**: Blocking main thread makes UI unresponsive.
1591
+
1592
+ ```swift
1593
+ // Wrong - expensive work blocks UI
1594
+ struct BadDataView: View {
1595
+ @State private var data: [Item] = []
1596
+
1597
+ var body: some View {
1598
+ List(data) { item in
1599
+ Text(item.name)
1600
+ }
1601
+ .onAppear {
1602
+ // Blocks UI while loading
1603
+ data = loadDataFromDisk() // Expensive!
1604
+ }
1605
+ }
1606
+ }
1607
+
1608
+ // Correct - async work off main thread
1609
+ struct GoodDataView: View {
1610
+ @State private var data: [Item] = []
1611
+ @State private var isLoading = true
1612
+
1613
+ var body: some View {
1614
+ Group {
1615
+ if isLoading {
1616
+ ProgressView()
1617
+ } else {
1618
+ List(data) { item in
1619
+ Text(item.name)
1620
+ }
1621
+ }
1622
+ }
1623
+ .task {
1624
+ // Runs on background thread
1625
+ data = await loadDataAsync()
1626
+ isLoading = false
1627
+ }
1628
+ }
1629
+ }
1630
+ ```
1631
+
1632
+ ## 8. Using ObservableObject Without Scoping Published Properties
1633
+
1634
+ **Problem**: Views recompute when any @Published property changes, even ones they don't use.
1635
+
1636
+ ```swift
1637
+ // Problematic - view recomputes on all changes
1638
+ class AppState: ObservableObject {
1639
+ @Published var userProfile: User? // Changes rarely
1640
+ @Published var unreadCount: Int = 0 // Changes frequently
1641
+ @Published var networkStatus: Status = .online // Changes frequently
1642
+ }
1643
+
1644
+ struct ProfileView: View {
1645
+ @ObservedObject var appState: AppState
1646
+
1647
+ var body: some View {
1648
+ // Only uses userProfile, but recomputes on unreadCount changes!
1649
+ Text(appState.userProfile?.name ?? "")
1650
+ }
1651
+ }
1652
+
1653
+ // Solution 1: Use @Observable (iOS 17+) for fine-grained observation
1654
+ @Observable
1655
+ class AppState {
1656
+ var userProfile: User? // ProfileView only observes this
1657
+ var unreadCount: Int = 0
1658
+ var networkStatus: Status = .online
1659
+ }
1660
+
1661
+ // Solution 2: Split into focused ObservableObjects
1662
+ class UserState: ObservableObject {
1663
+ @Published var profile: User?
1664
+ }
1665
+
1666
+ class NotificationState: ObservableObject {
1667
+ @Published var unreadCount: Int = 0
1668
+ }
1669
+
1670
+ struct ProfileView: View {
1671
+ @ObservedObject var userState: UserState // Only observes relevant state
1672
+
1673
+ var body: some View {
1674
+ Text(userState.profile?.name ?? "")
1675
+ }
1676
+ }
1677
+ ```
1678
+
1679
+ These anti-patterns account for the majority of SwiftUI performance issues. Profiling with Instruments reveals which patterns affect your specific app.
1680
+ </anti_patterns>
1681
+
1682
+ ---
1683
+
1684
+ ## Sources
1685
+
1686
+ - [Optimizing SwiftUI Performance: Best Practices](https://medium.com/@garejakirit/optimizing-swiftui-performance-best-practices-93b9cc91c623)
1687
+ - [Demystify SwiftUI performance - WWDC23](https://developer.apple.com/videos/play/wwdc2023/10160/)
1688
+ - [Making our production SwiftUI app 100x faster — Clay](https://clay.earth/stories/production-swiftui-performance-increase)
1689
+ - [How the SwiftUI View Lifecycle and Identity work - DoorDash Engineering](https://doordash.engineering/2022/05/31/how-the-swiftui-view-lifecycle-and-identity-work/)
1690
+ - [Identity in SwiftUI - Geek Culture](https://medium.com/geekculture/identity-in-swiftui-6aacf8f587d9)
1691
+ - [Demystify SwiftUI - WWDC21](https://developer.apple.com/videos/play/wwdc2021/10022/)
1692
+ - [id(_): Identifying SwiftUI Views - The SwiftUI Lab](https://swiftui-lab.com/swiftui-id/)
1693
+ - [How to use Instruments to profile your SwiftUI code - Hacking with Swift](https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-instruments-to-profile-your-swiftui-code-and-identify-slow-layouts)
1694
+ - [Profiling SwiftUI app using Instruments - Swift with Majid](https://swiftwithmajid.com/2021/01/20/profiling-swiftui-app-using-instruments/)
1695
+ - [@Observable Macro performance increase over ObservableObject](https://www.avanderlee.com/swiftui/observable-macro-performance-increase-observableobject/)
1696
+ - [@Observable vs ObservableObject in SwiftUI - Malcolm Hall](https://www.malcolmhall.com/2024/04/22/observable-vs-observableobject-in-swiftui/)
1697
+ - [Tuning Lazy Stacks and Grids in SwiftUI: A Performance Guide](https://medium.com/@wesleymatlock/tuning-lazy-stacks-and-grids-in-swiftui-a-performance-guide-2fb10786f76a)
1698
+ - [Tips and Considerations for Using Lazy Containers in SwiftUI](https://fatbobman.com/en/posts/tips-and-considerations-for-using-lazy-containers-in-swiftui/)
1699
+ - [List or LazyVStack - Choosing the Right Lazy Container in SwiftUI](https://fatbobman.com/en/posts/list-or-lazyvstack/)
1700
+ - [Optimizing AsyncImage in SwiftUI: Build a Custom Cached Solution](https://medium.com/@sviatoslav.kliuchev/improve-asyncimage-in-swiftui-5aae28f1a331)
1701
+ - [AsyncImage in SwiftUI: Loading Images from URLs with Caching](https://matteomanferdini.com/swiftui-asyncimage/)
1702
+ - [SwiftUI Performance and Stability: Avoiding the Most Costly Mistakes](https://dev.to/arshtechpro/swiftui-performance-and-stability-avoiding-the-most-costly-mistakes-234c)
1703
+ - [Common SwiftUI Mistakes - Hacking with Swift](https://www.hackingwithswift.com/articles/224/common-swiftui-mistakes-and-how-to-fix-them)
1704
+ - [Avoiding having to recompute values within SwiftUI views - Swift by Sundell](https://www.swiftbysundell.com/articles/avoiding-swiftui-value-recomputation/)
1705
+ - [Optimizing SwiftUI: Reducing Body Recalculation and Minimizing @State Updates](https://medium.com/@wesleymatlock/optimizing-swiftui-reducing-body-recalculation-and-minimizing-state-updates-8f7944253725)
1706
+ - [How to Avoid Repeating SwiftUI View Updates](https://fatbobman.com/en/posts/avoid_repeated_calculations_of_swiftui_views/)