opencodekit 0.16.15 → 0.16.17
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.
- package/dist/index.js +1 -1
- package/dist/template/.opencode/AGENTS.md +1 -1
- package/dist/template/.opencode/agent/plan.md +77 -161
- package/dist/template/.opencode/command/create.md +75 -307
- package/dist/template/.opencode/command/design.md +53 -589
- package/dist/template/.opencode/command/handoff.md +76 -180
- package/dist/template/.opencode/command/init.md +45 -211
- package/dist/template/.opencode/command/plan.md +62 -514
- package/dist/template/.opencode/command/pr.md +56 -226
- package/dist/template/.opencode/command/research.md +55 -266
- package/dist/template/.opencode/command/resume.md +33 -138
- package/dist/template/.opencode/command/review-codebase.md +54 -202
- package/dist/template/.opencode/command/ship.md +78 -127
- package/dist/template/.opencode/command/start.md +47 -577
- package/dist/template/.opencode/command/status.md +55 -354
- package/dist/template/.opencode/command/ui-review.md +52 -298
- package/dist/template/.opencode/command/verify.md +36 -250
- package/dist/template/.opencode/memory.db-shm +0 -0
- package/dist/template/.opencode/memory.db-wal +0 -0
- package/dist/template/.opencode/plugin/README.md +8 -4
- package/dist/template/.opencode/plugin/swarm-enforcer.ts +182 -27
- package/dist/template/.opencode/skill/augment-context-engine/SKILL.md +112 -0
- package/dist/template/.opencode/skill/augment-context-engine/mcp.json +6 -0
- package/dist/template/.opencode/skill/core-data-expert/SKILL.md +82 -0
- package/dist/template/.opencode/skill/core-data-expert/references/batch-operations.md +543 -0
- package/dist/template/.opencode/skill/core-data-expert/references/cloudkit-integration.md +259 -0
- package/dist/template/.opencode/skill/core-data-expert/references/concurrency.md +522 -0
- package/dist/template/.opencode/skill/core-data-expert/references/fetch-requests.md +643 -0
- package/dist/template/.opencode/skill/core-data-expert/references/glossary.md +233 -0
- package/dist/template/.opencode/skill/core-data-expert/references/migration.md +393 -0
- package/dist/template/.opencode/skill/core-data-expert/references/model-configuration.md +597 -0
- package/dist/template/.opencode/skill/core-data-expert/references/performance.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/persistent-history.md +553 -0
- package/dist/template/.opencode/skill/core-data-expert/references/project-audit.md +60 -0
- package/dist/template/.opencode/skill/core-data-expert/references/saving.md +574 -0
- package/dist/template/.opencode/skill/core-data-expert/references/stack-setup.md +625 -0
- package/dist/template/.opencode/skill/core-data-expert/references/testing.md +300 -0
- package/dist/template/.opencode/skill/core-data-expert/references/threading.md +589 -0
- package/dist/template/.opencode/skill/swift-concurrency/SKILL.md +246 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/actors.md +640 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-algorithms.md +822 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-await-basics.md +249 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/async-sequences.md +670 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/core-data.md +533 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/glossary.md +128 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/linting.md +142 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/memory-management.md +542 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/migration.md +1076 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/performance.md +574 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/sendable.md +578 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/tasks.md +604 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/testing.md +565 -0
- package/dist/template/.opencode/skill/swift-concurrency/references/threading.md +452 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/SKILL.md +290 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-advanced.md +351 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-basics.md +284 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/animation-transitions.md +326 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/image-optimization.md +286 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/layout-best-practices.md +312 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/liquid-glass.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/list-patterns.md +153 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/modern-apis.md +400 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/performance-patterns.md +377 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/scroll-patterns.md +305 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/sheet-navigation-patterns.md +292 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/state-management.md +447 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/text-formatting.md +285 -0
- package/dist/template/.opencode/skill/swiftui-expert-skill/references/view-structure.md +276 -0
- package/package.json +1 -1
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# SwiftUI Liquid Glass Reference (iOS 26+)
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Liquid Glass is Apple's new design language introduced in iOS 26. It provides translucent, dynamic surfaces that respond to content and user interaction. This reference covers the native SwiftUI APIs for implementing Liquid Glass effects.
|
|
6
|
+
|
|
7
|
+
## Availability
|
|
8
|
+
|
|
9
|
+
All Liquid Glass APIs require iOS 26 or later. Always provide fallbacks:
|
|
10
|
+
|
|
11
|
+
```swift
|
|
12
|
+
if #available(iOS 26, *) {
|
|
13
|
+
// Liquid Glass implementation
|
|
14
|
+
} else {
|
|
15
|
+
// Fallback using materials
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Core APIs
|
|
20
|
+
|
|
21
|
+
### glassEffect Modifier
|
|
22
|
+
|
|
23
|
+
The primary modifier for applying glass effects to views:
|
|
24
|
+
|
|
25
|
+
```swift
|
|
26
|
+
.glassEffect(_ style: GlassEffectStyle = .regular, in shape: some Shape = .rect)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### Basic Usage
|
|
30
|
+
|
|
31
|
+
```swift
|
|
32
|
+
Text("Hello")
|
|
33
|
+
.padding()
|
|
34
|
+
.glassEffect() // Default regular style, rect shape
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### With Shape
|
|
38
|
+
|
|
39
|
+
```swift
|
|
40
|
+
Text("Rounded Glass")
|
|
41
|
+
.padding()
|
|
42
|
+
.glassEffect(in: .rect(cornerRadius: 16))
|
|
43
|
+
|
|
44
|
+
Image(systemName: "star")
|
|
45
|
+
.padding()
|
|
46
|
+
.glassEffect(in: .circle)
|
|
47
|
+
|
|
48
|
+
Text("Capsule")
|
|
49
|
+
.padding(.horizontal, 20)
|
|
50
|
+
.padding(.vertical, 10)
|
|
51
|
+
.glassEffect(in: .capsule)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### GlassEffectStyle
|
|
55
|
+
|
|
56
|
+
#### Prominence Levels
|
|
57
|
+
|
|
58
|
+
```swift
|
|
59
|
+
.glassEffect(.regular) // Standard glass appearance
|
|
60
|
+
.glassEffect(.prominent) // More visible, higher contrast
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Tinting
|
|
64
|
+
|
|
65
|
+
Add color tint to the glass:
|
|
66
|
+
|
|
67
|
+
```swift
|
|
68
|
+
.glassEffect(.regular.tint(.blue))
|
|
69
|
+
.glassEffect(.prominent.tint(.red.opacity(0.3)))
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
#### Interactivity
|
|
73
|
+
|
|
74
|
+
Make glass respond to touch/pointer hover:
|
|
75
|
+
|
|
76
|
+
```swift
|
|
77
|
+
// Interactive glass - responds to user interaction
|
|
78
|
+
.glassEffect(.regular.interactive())
|
|
79
|
+
|
|
80
|
+
// Combined with tint
|
|
81
|
+
.glassEffect(.regular.tint(.blue).interactive())
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Important**: Only use `.interactive()` on elements that actually respond to user input (buttons, tappable views, focusable elements).
|
|
85
|
+
|
|
86
|
+
## GlassEffectContainer
|
|
87
|
+
|
|
88
|
+
Wraps multiple glass elements for proper visual grouping and spacing:
|
|
89
|
+
|
|
90
|
+
```swift
|
|
91
|
+
GlassEffectContainer {
|
|
92
|
+
HStack {
|
|
93
|
+
Button("One") { }
|
|
94
|
+
.glassEffect()
|
|
95
|
+
Button("Two") { }
|
|
96
|
+
.glassEffect()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### With Spacing
|
|
102
|
+
|
|
103
|
+
Control the visual spacing between glass elements:
|
|
104
|
+
|
|
105
|
+
```swift
|
|
106
|
+
GlassEffectContainer(spacing: 24) {
|
|
107
|
+
HStack(spacing: 24) {
|
|
108
|
+
GlassChip(icon: "pencil")
|
|
109
|
+
GlassChip(icon: "eraser")
|
|
110
|
+
GlassChip(icon: "trash")
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Note**: The container's `spacing` parameter should match the actual spacing in your layout for proper glass effect rendering.
|
|
116
|
+
|
|
117
|
+
## Glass Button Styles
|
|
118
|
+
|
|
119
|
+
Built-in button styles for glass appearance:
|
|
120
|
+
|
|
121
|
+
```swift
|
|
122
|
+
// Standard glass button
|
|
123
|
+
Button("Action") { }
|
|
124
|
+
.buttonStyle(.glass)
|
|
125
|
+
|
|
126
|
+
// Prominent glass button (higher visibility)
|
|
127
|
+
Button("Primary Action") { }
|
|
128
|
+
.buttonStyle(.glassProminent)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Custom Glass Buttons
|
|
132
|
+
|
|
133
|
+
For more control, apply glass effect manually:
|
|
134
|
+
|
|
135
|
+
```swift
|
|
136
|
+
Button(action: { }) {
|
|
137
|
+
Label("Settings", systemImage: "gear")
|
|
138
|
+
.padding()
|
|
139
|
+
}
|
|
140
|
+
.glassEffect(.regular.interactive(), in: .capsule)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Morphing Transitions
|
|
144
|
+
|
|
145
|
+
Create smooth transitions between glass elements using `glassEffectID` and `@Namespace`:
|
|
146
|
+
|
|
147
|
+
```swift
|
|
148
|
+
struct MorphingExample: View {
|
|
149
|
+
@Namespace private var animation
|
|
150
|
+
@State private var isExpanded = false
|
|
151
|
+
|
|
152
|
+
var body: some View {
|
|
153
|
+
GlassEffectContainer {
|
|
154
|
+
if isExpanded {
|
|
155
|
+
ExpandedCard()
|
|
156
|
+
.glassEffect()
|
|
157
|
+
.glassEffectID("card", in: animation)
|
|
158
|
+
} else {
|
|
159
|
+
CompactCard()
|
|
160
|
+
.glassEffect()
|
|
161
|
+
.glassEffectID("card", in: animation)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
.animation(.smooth, value: isExpanded)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Requirements for Morphing
|
|
170
|
+
|
|
171
|
+
1. Both views must have the same `glassEffectID`
|
|
172
|
+
2. Use the same `@Namespace`
|
|
173
|
+
3. Wrap in `GlassEffectContainer`
|
|
174
|
+
4. Apply animation to the container or parent
|
|
175
|
+
|
|
176
|
+
## Modifier Order
|
|
177
|
+
|
|
178
|
+
**Critical**: Apply `glassEffect` after layout and visual modifiers:
|
|
179
|
+
|
|
180
|
+
```swift
|
|
181
|
+
// CORRECT order
|
|
182
|
+
Text("Label")
|
|
183
|
+
.font(.headline) // 1. Typography
|
|
184
|
+
.foregroundStyle(.primary) // 2. Color
|
|
185
|
+
.padding() // 3. Layout
|
|
186
|
+
.glassEffect() // 4. Glass effect LAST
|
|
187
|
+
|
|
188
|
+
// WRONG order - glass applied too early
|
|
189
|
+
Text("Label")
|
|
190
|
+
.glassEffect() // Wrong position
|
|
191
|
+
.padding()
|
|
192
|
+
.font(.headline)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Complete Examples
|
|
196
|
+
|
|
197
|
+
### Toolbar with Glass Buttons
|
|
198
|
+
|
|
199
|
+
```swift
|
|
200
|
+
struct GlassToolbar: View {
|
|
201
|
+
var body: some View {
|
|
202
|
+
if #available(iOS 26, *) {
|
|
203
|
+
GlassEffectContainer(spacing: 16) {
|
|
204
|
+
HStack(spacing: 16) {
|
|
205
|
+
ToolbarButton(icon: "pencil", action: { })
|
|
206
|
+
ToolbarButton(icon: "eraser", action: { })
|
|
207
|
+
ToolbarButton(icon: "scissors", action: { })
|
|
208
|
+
Spacer()
|
|
209
|
+
ToolbarButton(icon: "square.and.arrow.up", action: { })
|
|
210
|
+
}
|
|
211
|
+
.padding(.horizontal)
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// Fallback toolbar
|
|
215
|
+
HStack(spacing: 16) {
|
|
216
|
+
// ... fallback implementation
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
struct ToolbarButton: View {
|
|
223
|
+
let icon: String
|
|
224
|
+
let action: () -> Void
|
|
225
|
+
|
|
226
|
+
var body: some View {
|
|
227
|
+
Button(action: action) {
|
|
228
|
+
Image(systemName: icon)
|
|
229
|
+
.font(.title2)
|
|
230
|
+
.frame(width: 44, height: 44)
|
|
231
|
+
}
|
|
232
|
+
.glassEffect(.regular.interactive(), in: .circle)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Card with Glass Effect
|
|
238
|
+
|
|
239
|
+
```swift
|
|
240
|
+
struct GlassCard: View {
|
|
241
|
+
let title: String
|
|
242
|
+
let subtitle: String
|
|
243
|
+
|
|
244
|
+
var body: some View {
|
|
245
|
+
if #available(iOS 26, *) {
|
|
246
|
+
cardContent
|
|
247
|
+
.glassEffect(.regular, in: .rect(cornerRadius: 20))
|
|
248
|
+
} else {
|
|
249
|
+
cardContent
|
|
250
|
+
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20))
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private var cardContent: some View {
|
|
255
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
256
|
+
Text(title)
|
|
257
|
+
.font(.headline)
|
|
258
|
+
Text(subtitle)
|
|
259
|
+
.font(.subheadline)
|
|
260
|
+
.foregroundStyle(.secondary)
|
|
261
|
+
}
|
|
262
|
+
.padding()
|
|
263
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Segmented Control
|
|
269
|
+
|
|
270
|
+
```swift
|
|
271
|
+
struct GlassSegmentedControl: View {
|
|
272
|
+
@Binding var selection: Int
|
|
273
|
+
let options: [String]
|
|
274
|
+
@Namespace private var animation
|
|
275
|
+
|
|
276
|
+
var body: some View {
|
|
277
|
+
if #available(iOS 26, *) {
|
|
278
|
+
GlassEffectContainer(spacing: 4) {
|
|
279
|
+
HStack(spacing: 4) {
|
|
280
|
+
ForEach(options.indices, id: \.self) { index in
|
|
281
|
+
Button(options[index]) {
|
|
282
|
+
withAnimation(.smooth) {
|
|
283
|
+
selection = index
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
.padding(.horizontal, 16)
|
|
287
|
+
.padding(.vertical, 8)
|
|
288
|
+
.glassEffect(
|
|
289
|
+
selection == index ? .prominent.interactive() : .regular.interactive(),
|
|
290
|
+
in: .capsule
|
|
291
|
+
)
|
|
292
|
+
.glassEffectID(selection == index ? "selected" : "option\(index)", in: animation)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
.padding(4)
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
Picker("Options", selection: $selection) {
|
|
299
|
+
ForEach(options.indices, id: \.self) { index in
|
|
300
|
+
Text(options[index]).tag(index)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
.pickerStyle(.segmented)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Fallback Strategies
|
|
310
|
+
|
|
311
|
+
### Using Materials
|
|
312
|
+
|
|
313
|
+
```swift
|
|
314
|
+
if #available(iOS 26, *) {
|
|
315
|
+
content.glassEffect()
|
|
316
|
+
} else {
|
|
317
|
+
content.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Available Materials for Fallback
|
|
322
|
+
|
|
323
|
+
- `.ultraThinMaterial` - Closest to glass appearance
|
|
324
|
+
- `.thinMaterial` - Slightly more opaque
|
|
325
|
+
- `.regularMaterial` - Standard blur
|
|
326
|
+
- `.thickMaterial` - More opaque
|
|
327
|
+
- `.ultraThickMaterial` - Most opaque
|
|
328
|
+
|
|
329
|
+
### Conditional Modifier Extension
|
|
330
|
+
|
|
331
|
+
```swift
|
|
332
|
+
extension View {
|
|
333
|
+
@ViewBuilder
|
|
334
|
+
func glassEffectWithFallback(
|
|
335
|
+
_ style: GlassEffectStyle = .regular,
|
|
336
|
+
in shape: some Shape = .rect,
|
|
337
|
+
fallbackMaterial: Material = .ultraThinMaterial
|
|
338
|
+
) -> some View {
|
|
339
|
+
if #available(iOS 26, *) {
|
|
340
|
+
self.glassEffect(style, in: shape)
|
|
341
|
+
} else {
|
|
342
|
+
self.background(fallbackMaterial, in: shape)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Best Practices
|
|
349
|
+
|
|
350
|
+
### Do
|
|
351
|
+
|
|
352
|
+
- Use `GlassEffectContainer` for grouped glass elements
|
|
353
|
+
- Apply glass after layout modifiers
|
|
354
|
+
- Use `.interactive()` only on tappable elements
|
|
355
|
+
- Match container spacing with layout spacing
|
|
356
|
+
- Provide material-based fallbacks for older iOS
|
|
357
|
+
- Keep glass shapes consistent within a feature
|
|
358
|
+
|
|
359
|
+
### Don't
|
|
360
|
+
|
|
361
|
+
- Apply glass to every element (use sparingly)
|
|
362
|
+
- Use `.interactive()` on static content
|
|
363
|
+
- Mix different corner radii arbitrarily
|
|
364
|
+
- Forget iOS version checks
|
|
365
|
+
- Apply glass before padding/frame modifiers
|
|
366
|
+
- Nest `GlassEffectContainer` unnecessarily
|
|
367
|
+
|
|
368
|
+
## Checklist
|
|
369
|
+
|
|
370
|
+
- [ ] `#available(iOS 26, *)` with fallback
|
|
371
|
+
- [ ] `GlassEffectContainer` wraps grouped elements
|
|
372
|
+
- [ ] `.glassEffect()` applied after layout modifiers
|
|
373
|
+
- [ ] `.interactive()` only on user-interactable elements
|
|
374
|
+
- [ ] `glassEffectID` with `@Namespace` for morphing
|
|
375
|
+
- [ ] Consistent shapes and spacing across feature
|
|
376
|
+
- [ ] Container spacing matches layout spacing
|
|
377
|
+
- [ ] Appropriate prominence levels used
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# SwiftUI List Patterns Reference
|
|
2
|
+
|
|
3
|
+
## ForEach Identity and Stability
|
|
4
|
+
|
|
5
|
+
**Always provide stable identity for `ForEach`.** Never use `.indices` for dynamic content.
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// Good - stable identity via Identifiable
|
|
9
|
+
extension User: Identifiable {
|
|
10
|
+
var id: String { userId }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
ForEach(users) { user in
|
|
14
|
+
UserRow(user: user)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Good - stable identity via keypath
|
|
18
|
+
ForEach(users, id: \.userId) { user in
|
|
19
|
+
UserRow(user: user)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Wrong - indices create static content
|
|
23
|
+
ForEach(users.indices, id: \.self) { index in
|
|
24
|
+
UserRow(user: users[index]) // Can crash on removal!
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Wrong - unstable identity
|
|
28
|
+
ForEach(users, id: \.self) { user in
|
|
29
|
+
UserRow(user: user) // Only works if User is Hashable and stable
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Critical**: Ensure **constant number of views per element** in `ForEach`:
|
|
34
|
+
|
|
35
|
+
```swift
|
|
36
|
+
// Good - consistent view count
|
|
37
|
+
ForEach(items) { item in
|
|
38
|
+
ItemRow(item: item)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Bad - variable view count breaks identity
|
|
42
|
+
ForEach(items) { item in
|
|
43
|
+
if item.isSpecial {
|
|
44
|
+
SpecialRow(item: item)
|
|
45
|
+
DetailRow(item: item)
|
|
46
|
+
} else {
|
|
47
|
+
RegularRow(item: item)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Avoid inline filtering:**
|
|
53
|
+
|
|
54
|
+
```swift
|
|
55
|
+
// Bad - unstable identity, changes on every update
|
|
56
|
+
ForEach(items.filter { $0.isEnabled }) { item in
|
|
57
|
+
ItemRow(item: item)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Good - prefilter and cache
|
|
61
|
+
@State private var enabledItems: [Item] = []
|
|
62
|
+
|
|
63
|
+
var body: some View {
|
|
64
|
+
ForEach(enabledItems) { item in
|
|
65
|
+
ItemRow(item: item)
|
|
66
|
+
}
|
|
67
|
+
.onChange(of: items) { _, newItems in
|
|
68
|
+
enabledItems = newItems.filter { $0.isEnabled }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Avoid `AnyView` in list rows:**
|
|
74
|
+
|
|
75
|
+
```swift
|
|
76
|
+
// Bad - hides identity, increases cost
|
|
77
|
+
ForEach(items) { item in
|
|
78
|
+
AnyView(item.isSpecial ? SpecialRow(item: item) : RegularRow(item: item))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Good - Create a unified row view
|
|
82
|
+
ForEach(items) { item in
|
|
83
|
+
ItemRow(item: item)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
struct ItemRow: View {
|
|
87
|
+
let item: Item
|
|
88
|
+
|
|
89
|
+
var body: some View {
|
|
90
|
+
if item.isSpecial {
|
|
91
|
+
SpecialRow(item: item)
|
|
92
|
+
} else {
|
|
93
|
+
RegularRow(item: item)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Why**: Stable identity is critical for performance and animations. Unstable identity causes excessive diffing, broken animations, and potential crashes.
|
|
100
|
+
|
|
101
|
+
## Enumerated Sequences
|
|
102
|
+
|
|
103
|
+
**Always convert enumerated sequences to arrays. To be able to use them in a ForEach.**
|
|
104
|
+
|
|
105
|
+
```swift
|
|
106
|
+
let items = ["A", "B", "C"]
|
|
107
|
+
|
|
108
|
+
// Correct
|
|
109
|
+
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
|
|
110
|
+
Text("\(index): \(item)")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Wrong - Doesn't compile, enumerated() isn't an array
|
|
114
|
+
ForEach(items.enumerated(), id: \.offset) { index, item in
|
|
115
|
+
Text("\(index): \(item)")
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## List with Custom Styling
|
|
120
|
+
|
|
121
|
+
```swift
|
|
122
|
+
// Remove default background and separators
|
|
123
|
+
List(items) { item in
|
|
124
|
+
ItemRow(item: item)
|
|
125
|
+
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
|
126
|
+
.listRowSeparator(.hidden)
|
|
127
|
+
}
|
|
128
|
+
.listStyle(.plain)
|
|
129
|
+
.scrollContentBackground(.hidden)
|
|
130
|
+
.background(Color.customBackground)
|
|
131
|
+
.environment(\.defaultMinListRowHeight, 1) // Allows custom row heights
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## List with Pull-to-Refresh
|
|
135
|
+
|
|
136
|
+
```swift
|
|
137
|
+
List(items) { item in
|
|
138
|
+
ItemRow(item: item)
|
|
139
|
+
}
|
|
140
|
+
.refreshable {
|
|
141
|
+
await loadItems()
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Summary Checklist
|
|
146
|
+
|
|
147
|
+
- [ ] ForEach uses stable identity (never `.indices` for dynamic content)
|
|
148
|
+
- [ ] Constant number of views per ForEach element
|
|
149
|
+
- [ ] No inline filtering in ForEach (prefilter and cache instead)
|
|
150
|
+
- [ ] No `AnyView` in list rows
|
|
151
|
+
- [ ] Don't convert enumerated sequences to arrays
|
|
152
|
+
- [ ] Use `.refreshable` for pull-to-refresh
|
|
153
|
+
- [ ] Custom list styling uses appropriate modifiers
|