opencodekit 0.16.15 → 0.16.18
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/README.md +77 -242
- package/dist/index.js +19 -6
- package/dist/template/.opencode/AGENTS.md +72 -236
- package/dist/template/.opencode/README.md +49 -482
- package/dist/template/.opencode/agent/build.md +71 -345
- package/dist/template/.opencode/agent/explore.md +47 -139
- package/dist/template/.opencode/agent/general.md +61 -172
- package/dist/template/.opencode/agent/looker.md +65 -161
- package/dist/template/.opencode/agent/painter.md +46 -200
- package/dist/template/.opencode/agent/plan.md +37 -220
- package/dist/template/.opencode/agent/review.md +72 -153
- package/dist/template/.opencode/agent/scout.md +44 -486
- package/dist/template/.opencode/agent/vision.md +63 -178
- 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 +60 -353
- 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/opencode.json +133 -35
- package/dist/template/.opencode/plugin/README.md +40 -166
- package/dist/template/.opencode/plugin/compaction.ts +162 -131
- package/dist/template/.opencode/plugin/lib/memory-db.ts +112 -0
- 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/dist/template/.opencode/tool/action-queue.ts +308 -0
- package/dist/template/.opencode/tool/swarm.ts +65 -40
- package/package.json +16 -3
- package/dist/template/.opencode/.agents/skills/context7/SKILL.md +0 -88
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# SwiftUI Image Optimization Reference
|
|
2
|
+
|
|
3
|
+
## AsyncImage Best Practices
|
|
4
|
+
|
|
5
|
+
### Basic AsyncImage with Phase Handling
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// Good - handles loading and error states
|
|
9
|
+
AsyncImage(url: imageURL) { phase in
|
|
10
|
+
switch phase {
|
|
11
|
+
case .empty:
|
|
12
|
+
ProgressView()
|
|
13
|
+
case .success(let image):
|
|
14
|
+
image
|
|
15
|
+
.resizable()
|
|
16
|
+
.aspectRatio(contentMode: .fit)
|
|
17
|
+
case .failure:
|
|
18
|
+
Image(systemName: "photo")
|
|
19
|
+
.foregroundStyle(.secondary)
|
|
20
|
+
@unknown default:
|
|
21
|
+
EmptyView()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
.frame(width: 200, height: 200)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### AsyncImage with Custom Placeholder
|
|
28
|
+
|
|
29
|
+
```swift
|
|
30
|
+
struct ImageView: View {
|
|
31
|
+
let url: URL?
|
|
32
|
+
|
|
33
|
+
var body: some View {
|
|
34
|
+
AsyncImage(url: url) { phase in
|
|
35
|
+
switch phase {
|
|
36
|
+
case .empty:
|
|
37
|
+
ZStack {
|
|
38
|
+
Color.gray.opacity(0.2)
|
|
39
|
+
ProgressView()
|
|
40
|
+
}
|
|
41
|
+
case .success(let image):
|
|
42
|
+
image
|
|
43
|
+
.resizable()
|
|
44
|
+
.aspectRatio(contentMode: .fill)
|
|
45
|
+
case .failure:
|
|
46
|
+
ZStack {
|
|
47
|
+
Color.gray.opacity(0.2)
|
|
48
|
+
Image(systemName: "exclamationmark.triangle")
|
|
49
|
+
.foregroundStyle(.secondary)
|
|
50
|
+
}
|
|
51
|
+
@unknown default:
|
|
52
|
+
EmptyView()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
.clipShape(.rect(cornerRadius: 12))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### AsyncImage with Transition
|
|
61
|
+
|
|
62
|
+
```swift
|
|
63
|
+
AsyncImage(url: imageURL) { phase in
|
|
64
|
+
switch phase {
|
|
65
|
+
case .empty:
|
|
66
|
+
ProgressView()
|
|
67
|
+
case .success(let image):
|
|
68
|
+
image
|
|
69
|
+
.resizable()
|
|
70
|
+
.aspectRatio(contentMode: .fit)
|
|
71
|
+
.transition(.opacity)
|
|
72
|
+
case .failure:
|
|
73
|
+
Image(systemName: "photo")
|
|
74
|
+
@unknown default:
|
|
75
|
+
EmptyView()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
.animation(.easeInOut, value: imageURL)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Image Decoding and Downsampling (Optional Optimization)
|
|
82
|
+
|
|
83
|
+
**When you encounter `UIImage(data:)` usage, consider suggesting image downsampling as a potential performance improvement**, especially for large images in lists or grids.
|
|
84
|
+
|
|
85
|
+
### Current Pattern That Could Be Optimized
|
|
86
|
+
|
|
87
|
+
```swift
|
|
88
|
+
// Current pattern - decodes full image on main thread
|
|
89
|
+
// Unsafe - force unwrap can crash if imageData is invalid
|
|
90
|
+
Image(uiImage: UIImage(data: imageData)!)
|
|
91
|
+
.resizable()
|
|
92
|
+
.aspectRatio(contentMode: .fit)
|
|
93
|
+
.frame(width: 200, height: 200)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Suggested Optimization Pattern
|
|
97
|
+
|
|
98
|
+
```swift
|
|
99
|
+
// Suggested optimization - decode and downsample off main thread
|
|
100
|
+
struct OptimizedImageView: View {
|
|
101
|
+
let imageData: Data
|
|
102
|
+
let targetSize: CGSize
|
|
103
|
+
@State private var processedImage: UIImage?
|
|
104
|
+
|
|
105
|
+
var body: some View {
|
|
106
|
+
Group {
|
|
107
|
+
if let processedImage {
|
|
108
|
+
Image(uiImage: processedImage)
|
|
109
|
+
.resizable()
|
|
110
|
+
.aspectRatio(contentMode: .fit)
|
|
111
|
+
} else {
|
|
112
|
+
ProgressView()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
.task {
|
|
116
|
+
processedImage = await decodeAndDownsample(imageData, targetSize: targetSize)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func decodeAndDownsample(_ data: Data, targetSize: CGSize) async -> UIImage? {
|
|
121
|
+
await Task.detached {
|
|
122
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
|
123
|
+
return nil
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let options: [CFString: Any] = [
|
|
127
|
+
kCGImageSourceThumbnailMaxPixelSize: max(targetSize.width, targetSize.height),
|
|
128
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
129
|
+
kCGImageSourceCreateThumbnailWithTransform: true
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
133
|
+
return nil
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return UIImage(cgImage: cgImage)
|
|
137
|
+
}.value
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Usage
|
|
142
|
+
OptimizedImageView(
|
|
143
|
+
imageData: imageData,
|
|
144
|
+
targetSize: CGSize(width: 200, height: 200)
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Reusable Image Downsampling Helper
|
|
149
|
+
|
|
150
|
+
```swift
|
|
151
|
+
actor ImageProcessor {
|
|
152
|
+
func downsample(data: Data, to targetSize: CGSize) -> UIImage? {
|
|
153
|
+
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
|
|
154
|
+
return nil
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let maxDimension = max(targetSize.width, targetSize.height) * UIScreen.main.scale
|
|
158
|
+
|
|
159
|
+
let options: [CFString: Any] = [
|
|
160
|
+
kCGImageSourceThumbnailMaxPixelSize: maxDimension,
|
|
161
|
+
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
162
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
163
|
+
kCGImageSourceShouldCache: false
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
167
|
+
return nil
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return UIImage(cgImage: cgImage)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Usage in view
|
|
175
|
+
struct ImageView: View {
|
|
176
|
+
let imageData: Data
|
|
177
|
+
let targetSize: CGSize
|
|
178
|
+
@State private var image: UIImage?
|
|
179
|
+
|
|
180
|
+
private let processor = ImageProcessor()
|
|
181
|
+
|
|
182
|
+
var body: some View {
|
|
183
|
+
Group {
|
|
184
|
+
if let image {
|
|
185
|
+
Image(uiImage: image)
|
|
186
|
+
.resizable()
|
|
187
|
+
.aspectRatio(contentMode: .fit)
|
|
188
|
+
} else {
|
|
189
|
+
ProgressView()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
.task {
|
|
193
|
+
image = await processor.downsample(data: imageData, to: targetSize)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### When to Suggest This Optimization
|
|
200
|
+
|
|
201
|
+
Mention this optimization when you see `UIImage(data:)` usage, particularly in:
|
|
202
|
+
- Scrollable content (List, ScrollView with LazyVStack/LazyHStack)
|
|
203
|
+
- Grid layouts with many images
|
|
204
|
+
- Image galleries or carousels
|
|
205
|
+
- Any scenario where large images are displayed at smaller sizes
|
|
206
|
+
|
|
207
|
+
**Don't automatically apply it**—present it as an optional improvement for performance-sensitive scenarios.
|
|
208
|
+
|
|
209
|
+
## SF Symbols
|
|
210
|
+
|
|
211
|
+
### Using SF Symbols
|
|
212
|
+
|
|
213
|
+
```swift
|
|
214
|
+
// Basic symbol
|
|
215
|
+
Image(systemName: "star.fill")
|
|
216
|
+
.foregroundStyle(.yellow)
|
|
217
|
+
|
|
218
|
+
// With rendering mode
|
|
219
|
+
Image(systemName: "heart.fill")
|
|
220
|
+
.symbolRenderingMode(.multicolor)
|
|
221
|
+
|
|
222
|
+
// With variable color
|
|
223
|
+
Image(systemName: "speaker.wave.3.fill")
|
|
224
|
+
.symbolRenderingMode(.hierarchical)
|
|
225
|
+
.foregroundStyle(.blue)
|
|
226
|
+
|
|
227
|
+
// Animated symbols (iOS 17+)
|
|
228
|
+
Image(systemName: "antenna.radiowaves.left.and.right")
|
|
229
|
+
.symbolEffect(.variableColor)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### SF Symbol Variants
|
|
233
|
+
|
|
234
|
+
```swift
|
|
235
|
+
// Circle variant
|
|
236
|
+
Image(systemName: "star.circle.fill")
|
|
237
|
+
|
|
238
|
+
// Square variant
|
|
239
|
+
Image(systemName: "star.square.fill")
|
|
240
|
+
|
|
241
|
+
// With badge
|
|
242
|
+
Image(systemName: "folder.badge.plus")
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Image Rendering
|
|
246
|
+
|
|
247
|
+
### ImageRenderer for Snapshots
|
|
248
|
+
|
|
249
|
+
```swift
|
|
250
|
+
// Render SwiftUI view to UIImage
|
|
251
|
+
let renderer = ImageRenderer(content: myView)
|
|
252
|
+
renderer.scale = UIScreen.main.scale
|
|
253
|
+
|
|
254
|
+
if let uiImage = renderer.uiImage {
|
|
255
|
+
// Use the image (save, share, etc.)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Render to CGImage
|
|
259
|
+
if let cgImage = renderer.cgImage {
|
|
260
|
+
// Use CGImage
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Rendering with Custom Size
|
|
265
|
+
|
|
266
|
+
```swift
|
|
267
|
+
let renderer = ImageRenderer(content: myView)
|
|
268
|
+
renderer.proposedSize = ProposedViewSize(width: 400, height: 300)
|
|
269
|
+
|
|
270
|
+
if let uiImage = renderer.uiImage {
|
|
271
|
+
// Image rendered at 400x300 points
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Summary Checklist
|
|
276
|
+
|
|
277
|
+
- [ ] Use `AsyncImage` with proper phase handling
|
|
278
|
+
- [ ] Handle empty, success, and failure states
|
|
279
|
+
- [ ] Consider downsampling for `UIImage(data:)` in performance-sensitive scenarios
|
|
280
|
+
- [ ] Decode and downsample images off the main thread
|
|
281
|
+
- [ ] Use appropriate target sizes for downsampling
|
|
282
|
+
- [ ] Consider image caching for frequently accessed images
|
|
283
|
+
- [ ] Use SF Symbols with appropriate rendering modes
|
|
284
|
+
- [ ] Use `ImageRenderer` for rendering SwiftUI views to images
|
|
285
|
+
|
|
286
|
+
**Performance Note**: Image downsampling is an optional optimization. Only suggest it when you encounter `UIImage(data:)` usage in performance-sensitive contexts like scrollable lists or grids.
|
package/dist/template/.opencode/skill/swiftui-expert-skill/references/layout-best-practices.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# SwiftUI Layout Best Practices Reference
|
|
2
|
+
|
|
3
|
+
## Relative Layout Over Constants
|
|
4
|
+
|
|
5
|
+
**Use dynamic layout calculations instead of hard-coded values.**
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// Good - relative to actual layout
|
|
9
|
+
GeometryReader { geometry in
|
|
10
|
+
VStack {
|
|
11
|
+
HeaderView()
|
|
12
|
+
.frame(height: geometry.size.height * 0.2)
|
|
13
|
+
ContentView()
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Avoid - magic numbers that don't adapt
|
|
18
|
+
VStack {
|
|
19
|
+
HeaderView()
|
|
20
|
+
.frame(height: 150) // Doesn't adapt to different screens
|
|
21
|
+
ContentView()
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Why**: Hard-coded values don't account for different screen sizes, orientations, or dynamic content (like status bars during phone calls).
|
|
26
|
+
|
|
27
|
+
## Context-Agnostic Views
|
|
28
|
+
|
|
29
|
+
**Views should work in any context.** Never assume presentation style or screen size.
|
|
30
|
+
|
|
31
|
+
```swift
|
|
32
|
+
// Good - adapts to given space
|
|
33
|
+
struct ProfileCard: View {
|
|
34
|
+
let user: User
|
|
35
|
+
|
|
36
|
+
var body: some View {
|
|
37
|
+
VStack {
|
|
38
|
+
Image(user.avatar)
|
|
39
|
+
.resizable()
|
|
40
|
+
.aspectRatio(contentMode: .fit)
|
|
41
|
+
Text(user.name)
|
|
42
|
+
Spacer()
|
|
43
|
+
}
|
|
44
|
+
.padding()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Avoid - assumes full screen
|
|
49
|
+
struct ProfileCard: View {
|
|
50
|
+
let user: User
|
|
51
|
+
|
|
52
|
+
var body: some View {
|
|
53
|
+
VStack {
|
|
54
|
+
Image(user.avatar)
|
|
55
|
+
.frame(width: UIScreen.main.bounds.width) // Wrong!
|
|
56
|
+
Text(user.name)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Why**: Views should work as full screens, modals, sheets, popovers, or embedded content.
|
|
63
|
+
|
|
64
|
+
## Own Your Container
|
|
65
|
+
|
|
66
|
+
**Custom views should own static containers but not lazy/repeatable ones.**
|
|
67
|
+
|
|
68
|
+
```swift
|
|
69
|
+
// Good - owns static container
|
|
70
|
+
struct HeaderView: View {
|
|
71
|
+
var body: some View {
|
|
72
|
+
HStack {
|
|
73
|
+
Image(systemName: "star")
|
|
74
|
+
Text("Title")
|
|
75
|
+
Spacer()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Avoid - missing container
|
|
81
|
+
struct HeaderView: View {
|
|
82
|
+
var body: some View {
|
|
83
|
+
Image(systemName: "star")
|
|
84
|
+
Text("Title")
|
|
85
|
+
// Caller must wrap in HStack
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Good - caller owns lazy container
|
|
90
|
+
struct FeedView: View {
|
|
91
|
+
let items: [Item]
|
|
92
|
+
|
|
93
|
+
var body: some View {
|
|
94
|
+
LazyVStack {
|
|
95
|
+
ForEach(items) { item in
|
|
96
|
+
ItemRow(item: item)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Layout Performance
|
|
104
|
+
|
|
105
|
+
### Avoid Layout Thrash
|
|
106
|
+
|
|
107
|
+
**Minimize deep view hierarchies and excessive layout dependencies.**
|
|
108
|
+
|
|
109
|
+
```swift
|
|
110
|
+
// Bad - deep nesting, excessive layout passes
|
|
111
|
+
VStack {
|
|
112
|
+
HStack {
|
|
113
|
+
VStack {
|
|
114
|
+
HStack {
|
|
115
|
+
VStack {
|
|
116
|
+
Text("Deep")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Good - flatter hierarchy
|
|
124
|
+
VStack {
|
|
125
|
+
Text("Shallow")
|
|
126
|
+
Text("Structure")
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Avoid excessive `GeometryReader` and preference chains:**
|
|
131
|
+
|
|
132
|
+
```swift
|
|
133
|
+
// Bad - multiple geometry readers cause layout thrash
|
|
134
|
+
GeometryReader { outerGeometry in
|
|
135
|
+
VStack {
|
|
136
|
+
GeometryReader { innerGeometry in
|
|
137
|
+
// Layout recalculates multiple times
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Good - single geometry reader or use alternatives (iOS 17+)
|
|
143
|
+
containerRelativeFrame(.horizontal) { width, _ in
|
|
144
|
+
width * 0.8
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Gate frequent geometry updates:**
|
|
149
|
+
|
|
150
|
+
```swift
|
|
151
|
+
// Bad - updates on every pixel change
|
|
152
|
+
.onPreferenceChange(ViewSizeKey.self) { size in
|
|
153
|
+
currentSize = size
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Good - gate by threshold
|
|
157
|
+
.onPreferenceChange(ViewSizeKey.self) { size in
|
|
158
|
+
let difference = abs(size.width - currentSize.width)
|
|
159
|
+
if difference > 10 { // Only update if significant change
|
|
160
|
+
currentSize = size
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## View Logic and Testability
|
|
166
|
+
|
|
167
|
+
### Separate View Logic from Views
|
|
168
|
+
|
|
169
|
+
**Place view logic into view models or similar, so it can be tested.**
|
|
170
|
+
|
|
171
|
+
> **iOS 17+**: Use `@Observable` macro with `@State` for view models.
|
|
172
|
+
|
|
173
|
+
```swift
|
|
174
|
+
// Good - logic in testable model (iOS 17+)
|
|
175
|
+
@Observable
|
|
176
|
+
@MainActor
|
|
177
|
+
final class LoginViewModel {
|
|
178
|
+
var email = ""
|
|
179
|
+
var password = ""
|
|
180
|
+
var isValid: Bool {
|
|
181
|
+
!email.isEmpty && password.count >= 8
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func login() async throws {
|
|
185
|
+
// Business logic here
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
struct LoginView: View {
|
|
190
|
+
@State private var viewModel = LoginViewModel()
|
|
191
|
+
|
|
192
|
+
var body: some View {
|
|
193
|
+
Form {
|
|
194
|
+
TextField("Email", text: $viewModel.email)
|
|
195
|
+
SecureField("Password", text: $viewModel.password)
|
|
196
|
+
Button("Login") {
|
|
197
|
+
Task {
|
|
198
|
+
try? await viewModel.login()
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
.disabled(!viewModel.isValid)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
> **iOS 16 and earlier**: Use `ObservableObject` protocol with `@StateObject`.
|
|
208
|
+
|
|
209
|
+
```swift
|
|
210
|
+
// Good - logic in testable model (iOS 16 and earlier)
|
|
211
|
+
@MainActor
|
|
212
|
+
final class LoginViewModel: ObservableObject {
|
|
213
|
+
@Published var email = ""
|
|
214
|
+
@Published var password = ""
|
|
215
|
+
var isValid: Bool {
|
|
216
|
+
!email.isEmpty && password.count >= 8
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
func login() async throws {
|
|
220
|
+
// Business logic here
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
struct LoginView: View {
|
|
225
|
+
@StateObject private var viewModel = LoginViewModel()
|
|
226
|
+
|
|
227
|
+
var body: some View {
|
|
228
|
+
Form {
|
|
229
|
+
TextField("Email", text: $viewModel.email)
|
|
230
|
+
SecureField("Password", text: $viewModel.password)
|
|
231
|
+
Button("Login") {
|
|
232
|
+
Task {
|
|
233
|
+
try? await viewModel.login()
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
.disabled(!viewModel.isValid)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
```swift
|
|
243
|
+
// Bad - logic embedded in view
|
|
244
|
+
struct LoginView: View {
|
|
245
|
+
@State private var email = ""
|
|
246
|
+
@State private var password = ""
|
|
247
|
+
|
|
248
|
+
var body: some View {
|
|
249
|
+
Form {
|
|
250
|
+
TextField("Email", text: $email)
|
|
251
|
+
SecureField("Password", text: $password)
|
|
252
|
+
Button("Login") {
|
|
253
|
+
// Business logic directly in view - hard to test
|
|
254
|
+
Task {
|
|
255
|
+
if !email.isEmpty && password.count >= 8 {
|
|
256
|
+
// Login logic...
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
**Note**: This is about separating business logic for testability, not about enforcing specific architectures like MVVM. The goal is to make logic testable while keeping views simple.
|
|
266
|
+
|
|
267
|
+
## Action Handlers
|
|
268
|
+
|
|
269
|
+
**Separate layout from logic.** View body should reference action methods, not contain logic.
|
|
270
|
+
|
|
271
|
+
```swift
|
|
272
|
+
// Good - action references method
|
|
273
|
+
struct PublishView: View {
|
|
274
|
+
@State private var viewModel = PublishViewModel()
|
|
275
|
+
|
|
276
|
+
var body: some View {
|
|
277
|
+
Button("Publish Project", action: viewModel.handlePublish)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Avoid - logic in closure
|
|
282
|
+
struct PublishView: View {
|
|
283
|
+
@State private var isLoading = false
|
|
284
|
+
@State private var showError = false
|
|
285
|
+
|
|
286
|
+
var body: some View {
|
|
287
|
+
Button("Publish Project") {
|
|
288
|
+
isLoading = true
|
|
289
|
+
apiService.publish(project) { result in
|
|
290
|
+
if case .error = result {
|
|
291
|
+
showError = true
|
|
292
|
+
}
|
|
293
|
+
isLoading = false
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
**Why**: Separating logic from layout improves readability, testability, and maintainability.
|
|
301
|
+
|
|
302
|
+
## Summary Checklist
|
|
303
|
+
|
|
304
|
+
- [ ] Use relative layout over hard-coded constants
|
|
305
|
+
- [ ] Views work in any context (don't assume screen size)
|
|
306
|
+
- [ ] Custom views own static containers
|
|
307
|
+
- [ ] Avoid deep view hierarchies (layout thrash)
|
|
308
|
+
- [ ] Gate frequent geometry updates by thresholds
|
|
309
|
+
- [ ] View logic separated into testable models/classes
|
|
310
|
+
- [ ] Action handlers reference methods, not inline logic
|
|
311
|
+
- [ ] Avoid excessive `GeometryReader` usage
|
|
312
|
+
- [ ] Use `containerRelativeFrame()` when appropriate
|