swift-code-reviewer-skill 1.2.0 → 1.2.1

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 (93) hide show
  1. package/CHANGELOG.md +35 -169
  2. package/README.md +43 -2
  3. package/SKILL.md +107 -742
  4. package/bin/install.js +1 -1
  5. package/package.json +2 -1
  6. package/references/companion-skills.md +70 -0
  7. package/skills/README.md +43 -0
  8. package/skills/swift-concurrency/NOTICE.md +18 -0
  9. package/skills/swift-concurrency/SKILL.md +235 -0
  10. package/skills/swift-concurrency/references/actors.md +640 -0
  11. package/skills/swift-concurrency/references/async-await-basics.md +249 -0
  12. package/skills/swift-concurrency/references/async-sequences.md +635 -0
  13. package/skills/swift-concurrency/references/core-data.md +533 -0
  14. package/skills/swift-concurrency/references/glossary.md +96 -0
  15. package/skills/swift-concurrency/references/linting.md +38 -0
  16. package/skills/swift-concurrency/references/memory-management.md +542 -0
  17. package/skills/swift-concurrency/references/migration.md +721 -0
  18. package/skills/swift-concurrency/references/performance.md +574 -0
  19. package/skills/swift-concurrency/references/sendable.md +578 -0
  20. package/skills/swift-concurrency/references/tasks.md +604 -0
  21. package/skills/swift-concurrency/references/testing.md +565 -0
  22. package/skills/swift-concurrency/references/threading.md +452 -0
  23. package/skills/swift-expert/NOTICE.md +18 -0
  24. package/skills/swift-expert/SKILL.md +226 -0
  25. package/skills/swift-expert/references/async-concurrency.md +363 -0
  26. package/skills/swift-expert/references/memory-performance.md +380 -0
  27. package/skills/swift-expert/references/protocol-oriented.md +357 -0
  28. package/skills/swift-expert/references/swiftui-patterns.md +294 -0
  29. package/skills/swift-expert/references/testing-patterns.md +402 -0
  30. package/skills/swift-testing/NOTICE.md +18 -0
  31. package/skills/swift-testing/SKILL.md +295 -0
  32. package/skills/swift-testing/references/async-testing.md +245 -0
  33. package/skills/swift-testing/references/dump-snapshot-testing.md +265 -0
  34. package/skills/swift-testing/references/fixtures.md +193 -0
  35. package/skills/swift-testing/references/integration-testing.md +189 -0
  36. package/skills/swift-testing/references/migration-xctest.md +301 -0
  37. package/skills/swift-testing/references/parameterized-tests.md +171 -0
  38. package/skills/swift-testing/references/snapshot-testing.md +201 -0
  39. package/skills/swift-testing/references/test-doubles.md +243 -0
  40. package/skills/swift-testing/references/test-organization.md +231 -0
  41. package/skills/swiftui-expert-skill/NOTICE.md +18 -0
  42. package/skills/swiftui-expert-skill/SKILL.md +281 -0
  43. package/skills/swiftui-expert-skill/references/accessibility-patterns.md +151 -0
  44. package/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  45. package/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  46. package/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  47. package/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  48. package/skills/swiftui-expert-skill/references/charts.md +602 -0
  49. package/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  50. package/skills/swiftui-expert-skill/references/latest-apis.md +464 -0
  51. package/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  52. package/skills/swiftui-expert-skill/references/liquid-glass.md +414 -0
  53. package/skills/swiftui-expert-skill/references/list-patterns.md +394 -0
  54. package/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  55. package/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  56. package/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  57. package/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  58. package/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  59. package/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  60. package/skills/swiftui-expert-skill/references/state-management.md +417 -0
  61. package/skills/swiftui-expert-skill/references/view-structure.md +389 -0
  62. package/skills/swiftui-ui-patterns/NOTICE.md +18 -0
  63. package/skills/swiftui-ui-patterns/SKILL.md +95 -0
  64. package/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  65. package/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  66. package/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  67. package/skills/swiftui-ui-patterns/references/controls.md +57 -0
  68. package/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  69. package/skills/swiftui-ui-patterns/references/focus.md +90 -0
  70. package/skills/swiftui-ui-patterns/references/form.md +97 -0
  71. package/skills/swiftui-ui-patterns/references/grids.md +71 -0
  72. package/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  73. package/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  74. package/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  75. package/skills/swiftui-ui-patterns/references/list.md +86 -0
  76. package/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  77. package/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  78. package/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  79. package/skills/swiftui-ui-patterns/references/media.md +73 -0
  80. package/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  81. package/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  82. package/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  83. package/skills/swiftui-ui-patterns/references/performance.md +62 -0
  84. package/skills/swiftui-ui-patterns/references/previews.md +48 -0
  85. package/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  86. package/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  87. package/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  88. package/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  89. package/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  90. package/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  91. package/skills/swiftui-ui-patterns/references/theming.md +71 -0
  92. package/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  93. package/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
@@ -0,0 +1,403 @@
1
+ # SwiftUI Performance Patterns Reference
2
+
3
+ ## Table of Contents
4
+
5
+ - [Performance Optimization](#performance-optimization)
6
+ - [Anti-Patterns](#anti-patterns)
7
+ - [Summary Checklist](#summary-checklist)
8
+
9
+ ## Performance Optimization
10
+
11
+ ### 1. Avoid Redundant State Updates
12
+
13
+ SwiftUI doesn't compare values before triggering updates:
14
+
15
+ ```swift
16
+ // BAD - triggers update even if value unchanged
17
+ .onReceive(publisher) { value in
18
+ self.currentValue = value // Always triggers body re-evaluation
19
+ }
20
+
21
+ // GOOD - only update when different
22
+ .onReceive(publisher) { value in
23
+ if self.currentValue != value {
24
+ self.currentValue = value
25
+ }
26
+ }
27
+ ```
28
+
29
+ ### 2. Optimize Hot Paths
30
+
31
+ Hot paths are frequently executed code (scroll handlers, animations, gestures):
32
+
33
+ ```swift
34
+ // BAD - updates state on every scroll position change
35
+ .onPreferenceChange(ScrollOffsetKey.self) { offset in
36
+ shouldShowTitle = offset.y <= -32 // Fires constantly during scroll!
37
+ }
38
+
39
+ // GOOD - only update when threshold crossed
40
+ .onPreferenceChange(ScrollOffsetKey.self) { offset in
41
+ let shouldShow = offset.y <= -32
42
+ if shouldShow != shouldShowTitle {
43
+ shouldShowTitle = shouldShow // Fires only when crossing threshold
44
+ }
45
+ }
46
+ ```
47
+
48
+ ### 3. Pass Only What Views Need
49
+
50
+ **Avoid passing large "config" or "context" objects.** Pass only the specific values each view needs.
51
+
52
+ ```swift
53
+ // Good - pass specific values
54
+ ThemeSelector(theme: config.theme)
55
+ FontSizeSlider(fontSize: config.fontSize)
56
+
57
+ // Avoid - passing entire config (creates broad dependency)
58
+ ThemeSelector(config: config) // Notified of ALL config changes
59
+ ```
60
+
61
+ With `ObservableObject`, any `@Published` change triggers all observers. With `@Observable`, views update only when accessed properties change, but passing entire objects still creates broader dependencies than necessary.
62
+
63
+ ### 4. Use Equatable Views
64
+
65
+ For views with expensive bodies, conform to `Equatable`:
66
+
67
+ ```swift
68
+ struct ExpensiveView: View, Equatable {
69
+ let data: SomeData
70
+
71
+ static func == (lhs: Self, rhs: Self) -> Bool {
72
+ lhs.data.id == rhs.data.id // Custom equality check
73
+ }
74
+
75
+ var body: some View {
76
+ // Expensive computation
77
+ }
78
+ }
79
+
80
+ // Usage
81
+ ExpensiveView(data: data)
82
+ .equatable() // Use custom equality
83
+ ```
84
+
85
+ **Caution**: If you add new state or dependencies to your view, remember to update your `==` function!
86
+
87
+ ### 5. POD Views for Fast Diffing
88
+
89
+ **POD (Plain Old Data) views use `memcmp` for fastest diffing.** A view is POD if it only contains simple value types and no property wrappers.
90
+
91
+ ```swift
92
+ // POD view - fastest diffing
93
+ struct FastView: View {
94
+ let title: String
95
+ let count: Int
96
+
97
+ var body: some View {
98
+ Text("\(title): \(count)")
99
+ }
100
+ }
101
+
102
+ // Non-POD view - uses reflection or custom equality
103
+ struct SlowerView: View {
104
+ let title: String
105
+ @State private var isExpanded = false // Property wrapper makes it non-POD
106
+
107
+ var body: some View {
108
+ Text(title)
109
+ }
110
+ }
111
+ ```
112
+
113
+ **Advanced Pattern**: Wrap expensive non-POD views in POD parent views:
114
+
115
+ ```swift
116
+ // POD wrapper for fast diffing
117
+ struct ExpensiveView: View {
118
+ let value: Int
119
+
120
+ var body: some View {
121
+ ExpensiveViewInternal(value: value)
122
+ }
123
+ }
124
+
125
+ // Internal view with state
126
+ private struct ExpensiveViewInternal: View {
127
+ let value: Int
128
+ @State private var item: Item?
129
+
130
+ var body: some View {
131
+ // Expensive rendering
132
+ }
133
+ }
134
+ ```
135
+
136
+ **Why**: The POD parent uses fast `memcmp` comparison. Only when `value` changes does the internal view get diffed.
137
+
138
+ ### 6. Lazy Loading
139
+
140
+ Use lazy containers for large collections:
141
+
142
+ ```swift
143
+ // BAD - creates all views immediately
144
+ ScrollView {
145
+ VStack {
146
+ ForEach(items) { item in
147
+ ExpensiveRow(item: item)
148
+ }
149
+ }
150
+ }
151
+
152
+ // GOOD - creates views on demand
153
+ ScrollView {
154
+ LazyVStack {
155
+ ForEach(items) { item in
156
+ ExpensiveRow(item: item)
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ **iOS 26+ note**: Nested scroll views containing lazy stacks now automatically defer loading their children until they are about to appear, matching the behavior of top-level lazy stacks. This benefits patterns like horizontal photo carousels inside a vertical scroll view.
163
+
164
+ > Source: "What's new in SwiftUI" (WWDC25, session 256)
165
+
166
+ ### 7. Task Cancellation
167
+
168
+ Cancel async work when view disappears:
169
+
170
+ ```swift
171
+ struct DataView: View {
172
+ @State private var data: [Item] = []
173
+
174
+ var body: some View {
175
+ List(data) { item in
176
+ Text(item.name)
177
+ }
178
+ .task {
179
+ // Automatically cancelled when view disappears
180
+ data = await fetchData()
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ ### 8. Debug View Updates
187
+
188
+ **Use `Self._printChanges()` or `Self._logChanges()` to debug unexpected view updates.**
189
+
190
+ ```swift
191
+ struct DebugView: View {
192
+ @State private var count = 0
193
+ @State private var name = ""
194
+
195
+ var body: some View {
196
+ #if DEBUG
197
+ let _ = Self._logChanges() // Xcode 15.1+: logs to com.apple.SwiftUI subsystem
198
+ #endif
199
+
200
+ VStack {
201
+ Text("Count: \(count)")
202
+ Text("Name: \(name)")
203
+ }
204
+ }
205
+ }
206
+ ```
207
+
208
+ - `Self._printChanges()`: Prints which properties changed to standard output.
209
+ - `Self._logChanges()` (iOS 17+): Logs to the `com.apple.SwiftUI` subsystem with category "Changed Body Properties", using `os_log` for structured output.
210
+
211
+ Both print `@self` when the view value itself changed and `@identity` when the view's persistent data was recycled.
212
+
213
+ **Why**: This helps identify which state changes are causing view updates. Isolating redraw triggers into single-responsibility subviews is often the fix -- extracting a subview means SwiftUI can skip its body when its inputs haven't changed.
214
+
215
+ ### 9. Eliminate Unnecessary Dependencies
216
+
217
+ **Narrow state scope to reduce update fan-out.** Instead of passing an entire `@Observable` model to a row view (which creates a dependency on all accessed properties), pass only the specific values the view needs as `let` properties.
218
+
219
+ ```swift
220
+ // Bad - broad dependency on entire model
221
+ struct ItemRow: View {
222
+ @Environment(AppModel.self) private var model
223
+ let item: Item
224
+ var body: some View { Text(item.name).foregroundStyle(model.theme.primaryColor) }
225
+ }
226
+
227
+ // Good - narrow dependency
228
+ struct ItemRow: View {
229
+ let item: Item
230
+ let themeColor: Color
231
+ var body: some View { Text(item.name).foregroundStyle(themeColor) }
232
+ }
233
+ ```
234
+
235
+ **Avoid storing frequently-changing values in the environment.** Even when a view doesn't read the changed key, SwiftUI still checks all environment readers. This cost adds up with many views and frequent updates (geometry values, timers).
236
+
237
+ > Source: "Optimize SwiftUI performance with Instruments" (WWDC25, session 306)
238
+
239
+ ### 10. @Observable Dependency Granularity
240
+
241
+ **Consider per-item `@Observable` state holders (one per row/item) to narrow update scope.** When multiple list items share a dependency on the same `@Observable` array, changing one element causes all items to re-evaluate their bodies.
242
+
243
+ ```swift
244
+ // BAD - all item views depend on the full favorites array
245
+ @Observable
246
+ class ModelData {
247
+ var favorites: [Landmark] = []
248
+
249
+ func isFavorite(_ landmark: Landmark) -> Bool {
250
+ favorites.contains(landmark)
251
+ }
252
+ }
253
+
254
+ struct LandmarkRow: View {
255
+ let landmark: Landmark
256
+ @Environment(ModelData.self) private var model
257
+
258
+ var body: some View {
259
+ HStack {
260
+ Text(landmark.name)
261
+ if model.isFavorite(landmark) {
262
+ Image(systemName: "heart.fill")
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ // GOOD - each item has its own observable view model
269
+ @Observable
270
+ class LandmarkViewModel {
271
+ var isFavorite: Bool = false
272
+ }
273
+
274
+ struct LandmarkRow: View {
275
+ let landmark: Landmark
276
+ let viewModel: LandmarkViewModel
277
+
278
+ var body: some View {
279
+ HStack {
280
+ Text(landmark.name)
281
+ if viewModel.isFavorite {
282
+ Image(systemName: "heart.fill")
283
+ }
284
+ }
285
+ }
286
+ }
287
+ ```
288
+
289
+ **Why**: With the bad pattern, toggling one favorite marks the entire array as changed, causing every `LandmarkRow` to re-run its body. With per-item view models, only the toggled item's body runs.
290
+
291
+ > Source: "Optimize SwiftUI performance with Instruments" (WWDC25, session 306)
292
+
293
+ ### 11. Off-Main-Thread Closures
294
+
295
+ **SwiftUI may call certain closures on a background thread for performance.** These closures must be `Sendable` and should avoid accessing `@MainActor`-isolated state directly. Instead, capture needed values in the closure's capture list.
296
+
297
+ Closures that may run off the main thread:
298
+ - `Shape.path(in:)`
299
+ - `visualEffect` closure
300
+ - `Layout` protocol methods
301
+ - `onGeometryChange` transform closure
302
+
303
+ ```swift
304
+ // BAD - accessing @MainActor state directly
305
+ .visualEffect { content, geometry in
306
+ content.blur(radius: self.pulse ? 5 : 0) // Compiler error: @MainActor isolated
307
+ }
308
+
309
+ // GOOD - capture the value
310
+ .visualEffect { [pulse] content, geometry in
311
+ content.blur(radius: pulse ? 5 : 0)
312
+ }
313
+ ```
314
+
315
+ > Source: "Explore concurrency in SwiftUI" (WWDC25, session 266)
316
+
317
+ ### 12. Common Performance Issues
318
+
319
+ **Be aware of common performance bottlenecks in SwiftUI:**
320
+
321
+ - View invalidation storms from broad state changes
322
+ - Unstable identity in lists causing excessive diffing
323
+ - Heavy work in `body` (formatting, sorting, image decoding)
324
+ - Layout thrash from deep stacks or preference chains
325
+
326
+ **When performance issues arise**, suggest the user profile with Instruments (SwiftUI template) to identify specific bottlenecks.
327
+
328
+ ## Anti-Patterns
329
+
330
+ ### 1. Creating Objects in Body
331
+
332
+ ```swift
333
+ // BAD - creates new formatter every body call
334
+ var body: some View {
335
+ let formatter = DateFormatter()
336
+ formatter.dateStyle = .long
337
+ return Text(formatter.string(from: date))
338
+ }
339
+
340
+ // GOOD - static or stored formatter
341
+ private static let dateFormatter: DateFormatter = {
342
+ let f = DateFormatter()
343
+ f.dateStyle = .long
344
+ return f
345
+ }()
346
+
347
+ var body: some View {
348
+ Text(Self.dateFormatter.string(from: date))
349
+ }
350
+ ```
351
+
352
+ ### 2. Heavy Computation in Body
353
+
354
+ **Keep view body simple and pure.** Avoid side effects, dispatching, or complex logic.
355
+
356
+ ```swift
357
+ // BAD - sorts array every body call
358
+ var body: some View {
359
+ List(items.sorted { $0.name < $1.name }) { item in Text(item.name) }
360
+ }
361
+
362
+ // GOOD - compute once, update via onChange or a computed property in the model
363
+ @State private var sortedItems: [Item] = []
364
+
365
+ var body: some View {
366
+ List(sortedItems) { item in Text(item.name) }
367
+ .onChange(of: items) { _, newItems in
368
+ sortedItems = newItems.sorted { $0.name < $1.name }
369
+ }
370
+ }
371
+ ```
372
+
373
+ Move sorting, filtering, and formatting into models or computed properties. The `body` should be a pure structural representation of state.
374
+
375
+ ### 3. Unnecessary State
376
+
377
+ ```swift
378
+ // BAD - derived state stored separately
379
+ @State private var items: [Item] = []
380
+ @State private var itemCount: Int = 0 // Unnecessary!
381
+
382
+ // GOOD - compute derived values
383
+ @State private var items: [Item] = []
384
+
385
+ var itemCount: Int { items.count } // Computed property
386
+ ```
387
+
388
+ ## Summary Checklist
389
+
390
+ - [ ] State updates check for value changes before assigning
391
+ - [ ] Hot paths minimize state updates
392
+ - [ ] Pass only needed values to views (avoid large config objects)
393
+ - [ ] Large lists use `LazyVStack`/`LazyHStack`
394
+ - [ ] No object creation in `body`
395
+ - [ ] Heavy computation moved out of `body`
396
+ - [ ] Body kept simple and pure (no side effects)
397
+ - [ ] Derived state computed, not stored
398
+ - [ ] Use `Self._logChanges()` or `Self._printChanges()` to debug unexpected updates
399
+ - [ ] Equatable conformance for expensive views (when appropriate)
400
+ - [ ] Consider POD view wrappers for advanced optimization
401
+ - [ ] Consider using granular @Observable dependencies for list items (smaller observable units per row when it measurably reduces updates)
402
+ - [ ] Frequently-changing values not stored in the environment
403
+ - [ ] Sendable closures (Shape, visualEffect, Layout) capture values instead of accessing @MainActor state
@@ -0,0 +1,293 @@
1
+ # SwiftUI ScrollView Patterns Reference
2
+
3
+ ## Table of Contents
4
+
5
+ - [ScrollViewReader for Programmatic Scrolling](#scrollviewreader-for-programmatic-scrolling)
6
+ - [Scroll Position Tracking](#scroll-position-tracking)
7
+ - [Scroll Transitions and Effects](#scroll-transitions-and-effects)
8
+ - [Scroll Target Behavior](#scroll-target-behavior)
9
+ - [Summary Checklist](#summary-checklist)
10
+
11
+ ## ScrollViewReader for Programmatic Scrolling
12
+
13
+ **Use `ScrollViewReader` for scroll-to-top, scroll-to-bottom, and anchor-based jumps.**
14
+
15
+ ```swift
16
+ struct ChatView: View {
17
+ @State private var messages: [Message] = []
18
+ private let bottomID = "bottom"
19
+
20
+ var body: some View {
21
+ ScrollViewReader { proxy in
22
+ ScrollView {
23
+ LazyVStack {
24
+ ForEach(messages) { message in
25
+ MessageRow(message: message)
26
+ .id(message.id)
27
+ }
28
+ Color.clear
29
+ .frame(height: 1)
30
+ .id(bottomID)
31
+ }
32
+ }
33
+ .onChange(of: messages.count) { _, _ in
34
+ withAnimation {
35
+ proxy.scrollTo(bottomID, anchor: .bottom)
36
+ }
37
+ }
38
+ .onAppear {
39
+ proxy.scrollTo(bottomID, anchor: .bottom)
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ### Scroll-to-Top Pattern
47
+
48
+ ```swift
49
+ struct FeedView: View {
50
+ @State private var items: [Item] = []
51
+ @State private var scrollToTop = false
52
+ private let topID = "top"
53
+
54
+ var body: some View {
55
+ ScrollViewReader { proxy in
56
+ ScrollView {
57
+ LazyVStack {
58
+ Color.clear
59
+ .frame(height: 1)
60
+ .id(topID)
61
+
62
+ ForEach(items) { item in
63
+ ItemRow(item: item)
64
+ }
65
+ }
66
+ }
67
+ .onChange(of: scrollToTop) { _, shouldScroll in
68
+ if shouldScroll {
69
+ withAnimation {
70
+ proxy.scrollTo(topID, anchor: .top)
71
+ }
72
+ scrollToTop = false
73
+ }
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ **Why**: `ScrollViewReader` provides programmatic scroll control with stable anchors. Always use stable IDs and explicit animations.
81
+
82
+ ## Scroll Position Tracking
83
+
84
+ ### Basic Scroll Position
85
+
86
+ **Avoid** - Storing scroll position directly triggers view updates on every scroll frame:
87
+
88
+ ```swift
89
+ // ❌ Bad Practice - causes unnecessary re-renders
90
+ struct ContentView: View {
91
+ @State private var scrollPosition: CGFloat = 0
92
+
93
+ var body: some View {
94
+ ScrollView {
95
+ content
96
+ .background(
97
+ GeometryReader { geometry in
98
+ Color.clear
99
+ .preference(
100
+ key: ScrollOffsetPreferenceKey.self,
101
+ value: geometry.frame(in: .named("scroll")).minY
102
+ )
103
+ }
104
+ )
105
+ }
106
+ .coordinateSpace(name: "scroll")
107
+ .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
108
+ scrollPosition = value
109
+ }
110
+ }
111
+ }
112
+ ```
113
+
114
+ **Preferred** - Check scroll position and update a flag based on thresholds for smoother, more efficient scrolling:
115
+
116
+ ```swift
117
+ // ✅ Good Practice - only updates state when crossing threshold
118
+ struct ContentView: View {
119
+ @State private var startAnimation: Bool = false
120
+
121
+ var body: some View {
122
+ ScrollView {
123
+ content
124
+ .background(
125
+ GeometryReader { geometry in
126
+ Color.clear
127
+ .preference(
128
+ key: ScrollOffsetPreferenceKey.self,
129
+ value: geometry.frame(in: .named("scroll")).minY
130
+ )
131
+ }
132
+ )
133
+ }
134
+ .coordinateSpace(name: "scroll")
135
+ .onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
136
+ if value < -100 {
137
+ startAnimation = true
138
+ } else {
139
+ startAnimation = false
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ struct ScrollOffsetPreferenceKey: PreferenceKey {
146
+ static var defaultValue: CGFloat = 0
147
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
148
+ value = nextValue()
149
+ }
150
+ }
151
+ ```
152
+
153
+ ### Scroll-Based Header Visibility
154
+
155
+ ```swift
156
+ struct ContentView: View {
157
+ @State private var showHeader = true
158
+
159
+ var body: some View {
160
+ VStack(spacing: 0) {
161
+ if showHeader {
162
+ HeaderView()
163
+ .transition(.move(edge: .top))
164
+ }
165
+
166
+ ScrollView {
167
+ content
168
+ .background(
169
+ GeometryReader { geometry in
170
+ Color.clear
171
+ .preference(
172
+ key: ScrollOffsetPreferenceKey.self,
173
+ value: geometry.frame(in: .named("scroll")).minY
174
+ )
175
+ }
176
+ )
177
+ }
178
+ .coordinateSpace(name: "scroll")
179
+ .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in
180
+ if offset < -50 { // Scrolling down
181
+ withAnimation { showHeader = false }
182
+ } else if offset > 50 { // Scrolling up
183
+ withAnimation { showHeader = true }
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ ```
190
+
191
+ ## Scroll Transitions and Effects
192
+
193
+ > **iOS 17+**: All APIs in this section require iOS 17 or later.
194
+
195
+ ### Scroll-Based Opacity
196
+
197
+ ```swift
198
+ struct ParallaxView: View {
199
+ var body: some View {
200
+ ScrollView {
201
+ LazyVStack(spacing: 20) {
202
+ ForEach(items) { item in
203
+ ItemCard(item: item)
204
+ .visualEffect { content, geometry in
205
+ let frame = geometry.frame(in: .scrollView)
206
+ let distance = min(0, frame.minY)
207
+ return content
208
+ .opacity(1 + distance / 200)
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ ```
216
+
217
+ ### Parallax Effect
218
+
219
+ ```swift
220
+ struct ParallaxHeader: View {
221
+ var body: some View {
222
+ ScrollView {
223
+ VStack(spacing: 0) {
224
+ Image("hero")
225
+ .resizable()
226
+ .aspectRatio(contentMode: .fill)
227
+ .frame(height: 300)
228
+ .visualEffect { content, geometry in
229
+ let offset = geometry.frame(in: .scrollView).minY
230
+ return content
231
+ .offset(y: offset > 0 ? -offset * 0.5 : 0)
232
+ }
233
+ .clipped()
234
+
235
+ ContentView()
236
+ }
237
+ }
238
+ }
239
+ }
240
+ ```
241
+
242
+ ## Scroll Target Behavior
243
+
244
+ > **iOS 17+**: All APIs in this section require iOS 17 or later.
245
+
246
+ ### Paging ScrollView
247
+
248
+ ```swift
249
+ struct PagingView: View {
250
+ var body: some View {
251
+ ScrollView(.horizontal) {
252
+ LazyHStack(spacing: 0) {
253
+ ForEach(pages) { page in
254
+ PageView(page: page)
255
+ .containerRelativeFrame(.horizontal)
256
+ }
257
+ }
258
+ .scrollTargetLayout()
259
+ }
260
+ .scrollTargetBehavior(.paging)
261
+ }
262
+ }
263
+ ```
264
+
265
+ ### Snap to Items
266
+
267
+ ```swift
268
+ struct SnapScrollView: View {
269
+ var body: some View {
270
+ ScrollView(.horizontal) {
271
+ LazyHStack(spacing: 16) {
272
+ ForEach(items) { item in
273
+ ItemCard(item: item)
274
+ .frame(width: 280)
275
+ }
276
+ }
277
+ .scrollTargetLayout()
278
+ }
279
+ .scrollTargetBehavior(.viewAligned)
280
+ .contentMargins(.horizontal, 20)
281
+ }
282
+ }
283
+ ```
284
+
285
+ ## Summary Checklist
286
+
287
+ - [ ] Use `ScrollViewReader` with stable IDs for programmatic scrolling
288
+ - [ ] Always use explicit animations with `scrollTo()`
289
+ - [ ] Use `.visualEffect` for scroll-based visual changes
290
+ - [ ] Use `.scrollTargetBehavior(.paging)` for paging behavior
291
+ - [ ] Use `.scrollTargetBehavior(.viewAligned)` for snap-to-item behavior
292
+ - [ ] Gate frequent scroll position updates by thresholds
293
+ - [ ] Use preference keys for custom scroll position tracking