swift-code-reviewer-skill 1.0.0
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/CHANGELOG.md +214 -0
- package/CONTRIBUTING.md +271 -0
- package/LICENSE +21 -0
- package/README.md +536 -0
- package/SKILL.md +690 -0
- package/bin/install.js +173 -0
- package/package.json +41 -0
- package/references/architecture-patterns.md +862 -0
- package/references/custom-guidelines.md +852 -0
- package/references/feedback-templates.md +666 -0
- package/references/performance-review.md +914 -0
- package/references/review-workflow.md +1131 -0
- package/references/security-checklist.md +781 -0
- package/references/swift-quality-checklist.md +928 -0
- package/references/swiftui-review-checklist.md +909 -0
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
# SwiftUI Performance Review Guide
|
|
2
|
+
|
|
3
|
+
This guide covers common performance anti-patterns in SwiftUI, view update optimization, ForEach identity issues, layout performance, and resource management. Use this to identify and fix performance problems in SwiftUI code.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. View Update Optimization
|
|
8
|
+
|
|
9
|
+
### 1.1 Unnecessary View Updates
|
|
10
|
+
|
|
11
|
+
**Check for:**
|
|
12
|
+
- [ ] Views re-rendering when data hasn't changed
|
|
13
|
+
- [ ] Excessive @State or @Observable properties
|
|
14
|
+
- [ ] Parent updates causing child re-renders unnecessarily
|
|
15
|
+
|
|
16
|
+
**Common Causes:**
|
|
17
|
+
- Non-Equatable view models
|
|
18
|
+
- Computed properties that always return new values
|
|
19
|
+
- Reference type mutations triggering updates
|
|
20
|
+
- Closures capturing mutable state
|
|
21
|
+
|
|
22
|
+
**Examples:**
|
|
23
|
+
|
|
24
|
+
❌ **Bad: Excessive updates**
|
|
25
|
+
```swift
|
|
26
|
+
@Observable
|
|
27
|
+
final class ViewModel {
|
|
28
|
+
var timestamp: Date { // ❌ New value every access
|
|
29
|
+
Date()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
struct ContentView: View {
|
|
34
|
+
let viewModel: ViewModel
|
|
35
|
+
|
|
36
|
+
var body: some View {
|
|
37
|
+
Text("Current time: \(viewModel.timestamp)") // ❌ Updates constantly
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
✅ **Good: Controlled updates**
|
|
43
|
+
```swift
|
|
44
|
+
@Observable
|
|
45
|
+
final class ViewModel {
|
|
46
|
+
private(set) var timestamp: Date = Date()
|
|
47
|
+
|
|
48
|
+
func updateTimestamp() {
|
|
49
|
+
timestamp = Date() // ✅ Explicit update only
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 1.2 Heavy Computation in Body
|
|
55
|
+
|
|
56
|
+
**Check for:**
|
|
57
|
+
- [ ] Sorting, filtering, or mapping in body
|
|
58
|
+
- [ ] Network calls or database queries in body
|
|
59
|
+
- [ ] Complex calculations during render
|
|
60
|
+
|
|
61
|
+
**Examples:**
|
|
62
|
+
|
|
63
|
+
❌ **Bad: Computation in body**
|
|
64
|
+
```swift
|
|
65
|
+
struct ItemListView: View {
|
|
66
|
+
let items: [Item]
|
|
67
|
+
|
|
68
|
+
var body: some View {
|
|
69
|
+
let filtered = items.filter { $0.isActive } // ❌ Every render
|
|
70
|
+
let sorted = filtered.sorted { $0.date > $1.date } // ❌ Every render
|
|
71
|
+
|
|
72
|
+
List(sorted) { item in
|
|
73
|
+
ItemRow(item: item)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
✅ **Good: Computed property**
|
|
80
|
+
```swift
|
|
81
|
+
struct ItemListView: View {
|
|
82
|
+
let items: [Item]
|
|
83
|
+
|
|
84
|
+
private var processedItems: [Item] { // ✅ Computed property
|
|
85
|
+
items
|
|
86
|
+
.filter { $0.isActive }
|
|
87
|
+
.sorted { $0.date > $1.date }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
var body: some View {
|
|
91
|
+
List(processedItems) { item in
|
|
92
|
+
ItemRow(item: item)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
✅ **Better: View model handles logic**
|
|
99
|
+
```swift
|
|
100
|
+
@Observable
|
|
101
|
+
final class ItemListViewModel {
|
|
102
|
+
var items: [Item] = []
|
|
103
|
+
var showActiveOnly: Bool = true
|
|
104
|
+
|
|
105
|
+
var displayedItems: [Item] { // ✅ Cached by view model
|
|
106
|
+
let filtered = showActiveOnly ? items.filter { $0.isActive } : items
|
|
107
|
+
return filtered.sorted { $0.date > $1.date }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
struct ItemListView: View {
|
|
112
|
+
let viewModel: ItemListViewModel
|
|
113
|
+
|
|
114
|
+
var body: some View {
|
|
115
|
+
List(viewModel.displayedItems) { item in // ✅ Already processed
|
|
116
|
+
ItemRow(item: item)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 1.3 Equatable Conformance
|
|
123
|
+
|
|
124
|
+
**Check for:**
|
|
125
|
+
- [ ] View models conform to Equatable
|
|
126
|
+
- [ ] Views use .equatable() modifier
|
|
127
|
+
- [ ] Proper equality implementation
|
|
128
|
+
|
|
129
|
+
**Examples:**
|
|
130
|
+
|
|
131
|
+
✅ **Good: Equatable view model**
|
|
132
|
+
```swift
|
|
133
|
+
@Observable
|
|
134
|
+
final class ItemViewModel: Equatable {
|
|
135
|
+
let id: UUID
|
|
136
|
+
var title: String
|
|
137
|
+
var subtitle: String
|
|
138
|
+
|
|
139
|
+
static func == (lhs: ItemViewModel, rhs: ItemViewModel) -> Bool {
|
|
140
|
+
lhs.id == rhs.id &&
|
|
141
|
+
lhs.title == rhs.title &&
|
|
142
|
+
lhs.subtitle == rhs.subtitle
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
struct ItemRow: View {
|
|
147
|
+
let viewModel: ItemViewModel
|
|
148
|
+
|
|
149
|
+
var body: some View {
|
|
150
|
+
VStack(alignment: .leading) {
|
|
151
|
+
Text(viewModel.title)
|
|
152
|
+
Text(viewModel.subtitle)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
.equatable() // ✅ Only updates when viewModel changes
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 1.4 Avoid Struct Copying
|
|
160
|
+
|
|
161
|
+
**Check for:**
|
|
162
|
+
- [ ] Large structs being copied frequently
|
|
163
|
+
- [ ] Reference semantics where appropriate
|
|
164
|
+
- [ ] Efficient data structures
|
|
165
|
+
|
|
166
|
+
**Examples:**
|
|
167
|
+
|
|
168
|
+
❌ **Bad: Large struct copying**
|
|
169
|
+
```swift
|
|
170
|
+
struct LargeData {
|
|
171
|
+
let items: [Item] // Large array
|
|
172
|
+
let metadata: [String: Any] // Large dictionary
|
|
173
|
+
// ... more properties
|
|
174
|
+
|
|
175
|
+
func withUpdatedItem(_ item: Item) -> LargeData { // ❌ Copies entire struct
|
|
176
|
+
var copy = self
|
|
177
|
+
// Update logic
|
|
178
|
+
return copy
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
✅ **Good: Reference type for mutable state**
|
|
184
|
+
```swift
|
|
185
|
+
@Observable
|
|
186
|
+
final class LargeDataModel { // ✅ Reference type
|
|
187
|
+
var items: [Item] = []
|
|
188
|
+
var metadata: [String: Any] = [:]
|
|
189
|
+
|
|
190
|
+
func updateItem(_ item: Item) { // ✅ No copying
|
|
191
|
+
// Update in place
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## 2. ForEach Performance
|
|
199
|
+
|
|
200
|
+
### 2.1 Stable Identity
|
|
201
|
+
|
|
202
|
+
**Check for:**
|
|
203
|
+
- [ ] ForEach uses stable IDs (Identifiable or explicit id)
|
|
204
|
+
- [ ] No index-based iteration when data changes
|
|
205
|
+
- [ ] No array.indices or enumerated() in ForEach
|
|
206
|
+
|
|
207
|
+
**Examples:**
|
|
208
|
+
|
|
209
|
+
❌ **Bad: Index-based identity**
|
|
210
|
+
```swift
|
|
211
|
+
List {
|
|
212
|
+
ForEach(items.indices, id: \.self) { index in // ❌ Unstable identity
|
|
213
|
+
ItemRow(item: items[index])
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
❌ **Bad: Enumerated**
|
|
219
|
+
```swift
|
|
220
|
+
ForEach(Array(items.enumerated()), id: \.offset) { index, item in // ❌ Unstable
|
|
221
|
+
ItemRow(item: item)
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
✅ **Good: Identifiable**
|
|
226
|
+
```swift
|
|
227
|
+
struct Item: Identifiable {
|
|
228
|
+
let id: UUID
|
|
229
|
+
let title: String
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
List {
|
|
233
|
+
ForEach(items) { item in // ✅ Using Identifiable
|
|
234
|
+
ItemRow(item: item)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
✅ **Good: Explicit stable ID**
|
|
240
|
+
```swift
|
|
241
|
+
struct Item {
|
|
242
|
+
let id: UUID
|
|
243
|
+
let title: String
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
List {
|
|
247
|
+
ForEach(items, id: \.id) { item in // ✅ Explicit stable ID
|
|
248
|
+
ItemRow(item: item)
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### 2.2 ForEach Identity for Animations
|
|
254
|
+
|
|
255
|
+
**Check for:**
|
|
256
|
+
- [ ] Stable IDs for smooth animations
|
|
257
|
+
- [ ] ID includes all relevant data for transitions
|
|
258
|
+
- [ ] No changing IDs during animations
|
|
259
|
+
|
|
260
|
+
**Examples:**
|
|
261
|
+
|
|
262
|
+
❌ **Bad: Changing ID during animation**
|
|
263
|
+
```swift
|
|
264
|
+
ForEach(items, id: \.timestamp) { item in // ❌ Timestamp changes
|
|
265
|
+
ItemRow(item: item)
|
|
266
|
+
}
|
|
267
|
+
.animation(.default, value: items)
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
✅ **Good: Stable ID for animations**
|
|
271
|
+
```swift
|
|
272
|
+
ForEach(items, id: \.id) { item in // ✅ ID never changes
|
|
273
|
+
ItemRow(item: item)
|
|
274
|
+
}
|
|
275
|
+
.animation(.default, value: items)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 2.3 Large List Performance
|
|
279
|
+
|
|
280
|
+
**Check for:**
|
|
281
|
+
- [ ] LazyVStack/LazyHStack for large lists
|
|
282
|
+
- [ ] Lazy loading for off-screen items
|
|
283
|
+
- [ ] Pagination for very large datasets
|
|
284
|
+
|
|
285
|
+
**Examples:**
|
|
286
|
+
|
|
287
|
+
❌ **Bad: Non-lazy stack for large list**
|
|
288
|
+
```swift
|
|
289
|
+
ScrollView {
|
|
290
|
+
VStack { // ❌ All views created immediately
|
|
291
|
+
ForEach(1000..<10000) { index in
|
|
292
|
+
HeavyView(index: index)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
✅ **Good: Lazy stack**
|
|
299
|
+
```swift
|
|
300
|
+
ScrollView {
|
|
301
|
+
LazyVStack { // ✅ Views created on-demand
|
|
302
|
+
ForEach(1000..<10000) { index in
|
|
303
|
+
HeavyView(index: index)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
✅ **Good: List (built-in lazy loading)**
|
|
310
|
+
```swift
|
|
311
|
+
List(items) { item in // ✅ List is lazy by default
|
|
312
|
+
ItemRow(item: item)
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### 2.4 Cell Reuse Patterns
|
|
317
|
+
|
|
318
|
+
**Check for:**
|
|
319
|
+
- [ ] Minimal state in rows
|
|
320
|
+
- [ ] No heavy initialization in row views
|
|
321
|
+
- [ ] Efficient data passing
|
|
322
|
+
|
|
323
|
+
**Examples:**
|
|
324
|
+
|
|
325
|
+
❌ **Bad: Heavy initialization in row**
|
|
326
|
+
```swift
|
|
327
|
+
struct ItemRow: View {
|
|
328
|
+
let item: Item
|
|
329
|
+
|
|
330
|
+
var body: some View {
|
|
331
|
+
let processedData = heavyProcessing(item) // ❌ Every render
|
|
332
|
+
|
|
333
|
+
VStack {
|
|
334
|
+
Text(processedData.title)
|
|
335
|
+
Text(processedData.subtitle)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private func heavyProcessing(_ item: Item) -> ProcessedData {
|
|
340
|
+
// Expensive operation
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
✅ **Good: Pre-processed data**
|
|
346
|
+
```swift
|
|
347
|
+
struct ItemRow: View {
|
|
348
|
+
let displayData: ItemDisplayData // ✅ Already processed
|
|
349
|
+
|
|
350
|
+
var body: some View {
|
|
351
|
+
VStack {
|
|
352
|
+
Text(displayData.title)
|
|
353
|
+
Text(displayData.subtitle)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Process data before passing to view
|
|
359
|
+
let displayData = items.map { heavyProcessing($0) }
|
|
360
|
+
List(displayData) { data in
|
|
361
|
+
ItemRow(displayData: data)
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## 3. Layout Performance
|
|
368
|
+
|
|
369
|
+
### 3.1 GeometryReader Overuse
|
|
370
|
+
|
|
371
|
+
**Check for:**
|
|
372
|
+
- [ ] Minimal GeometryReader usage
|
|
373
|
+
- [ ] No nested GeometryReaders
|
|
374
|
+
- [ ] Use .frame(maxWidth:) instead when possible
|
|
375
|
+
|
|
376
|
+
**Examples:**
|
|
377
|
+
|
|
378
|
+
❌ **Bad: Unnecessary GeometryReader**
|
|
379
|
+
```swift
|
|
380
|
+
GeometryReader { geometry in // ❌ Not needed
|
|
381
|
+
VStack {
|
|
382
|
+
Text("Hello")
|
|
383
|
+
.frame(width: geometry.size.width)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
✅ **Good: Simple frame modifier**
|
|
389
|
+
```swift
|
|
390
|
+
VStack {
|
|
391
|
+
Text("Hello")
|
|
392
|
+
.frame(maxWidth: .infinity) // ✅ Much simpler
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
❌ **Bad: Nested GeometryReaders**
|
|
397
|
+
```swift
|
|
398
|
+
GeometryReader { outerGeometry in
|
|
399
|
+
VStack {
|
|
400
|
+
ForEach(items) { item in
|
|
401
|
+
GeometryReader { innerGeometry in // ❌ Nested, performance issue
|
|
402
|
+
ItemView(item: item, width: innerGeometry.size.width)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
✅ **Good: Single GeometryReader or layout protocol**
|
|
410
|
+
```swift
|
|
411
|
+
// Option 1: Single GeometryReader
|
|
412
|
+
GeometryReader { geometry in
|
|
413
|
+
VStack {
|
|
414
|
+
ForEach(items) { item in
|
|
415
|
+
ItemView(item: item, width: geometry.size.width) // ✅ Reuse outer
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Option 2: Layout protocol (iOS 16+)
|
|
421
|
+
struct CustomLayout: Layout {
|
|
422
|
+
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
|
|
423
|
+
// Custom layout logic
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
|
|
427
|
+
// Placement logic
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### 3.2 Layout Thrash
|
|
433
|
+
|
|
434
|
+
**Check for:**
|
|
435
|
+
- [ ] No layout changes in onAppear/task
|
|
436
|
+
- [ ] Stable frame sizes
|
|
437
|
+
- [ ] No excessive frame recalculations
|
|
438
|
+
|
|
439
|
+
**Examples:**
|
|
440
|
+
|
|
441
|
+
❌ **Bad: Layout change in onAppear**
|
|
442
|
+
```swift
|
|
443
|
+
struct ContentView: View {
|
|
444
|
+
@State private var width: CGFloat = 100
|
|
445
|
+
|
|
446
|
+
var body: some View {
|
|
447
|
+
Rectangle()
|
|
448
|
+
.frame(width: width, height: 100)
|
|
449
|
+
.onAppear {
|
|
450
|
+
width = 200 // ❌ Layout thrash on appear
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
✅ **Good: Stable initial layout**
|
|
457
|
+
```swift
|
|
458
|
+
struct ContentView: View {
|
|
459
|
+
@State private var width: CGFloat = 200 // ✅ Correct from start
|
|
460
|
+
|
|
461
|
+
var body: some View {
|
|
462
|
+
Rectangle()
|
|
463
|
+
.frame(width: width, height: 100)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### 3.3 Prefer .frame Over Custom Layouts
|
|
469
|
+
|
|
470
|
+
**Check for:**
|
|
471
|
+
- [ ] Use built-in layout modifiers when possible
|
|
472
|
+
- [ ] Custom layouts only when necessary
|
|
473
|
+
- [ ] Efficient layout calculations
|
|
474
|
+
|
|
475
|
+
**Examples:**
|
|
476
|
+
|
|
477
|
+
✅ **Good: Built-in layout modifiers**
|
|
478
|
+
```swift
|
|
479
|
+
// Use built-in modifiers first
|
|
480
|
+
HStack(spacing: 16) {
|
|
481
|
+
Text("Title")
|
|
482
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
483
|
+
Text("Value")
|
|
484
|
+
}
|
|
485
|
+
.padding()
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
✅ **Good: Custom layout for complex needs**
|
|
489
|
+
```swift
|
|
490
|
+
// Only when built-in modifiers insufficient
|
|
491
|
+
struct WaterfallLayout: Layout {
|
|
492
|
+
// Complex custom layout logic
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## 4. Image Performance
|
|
499
|
+
|
|
500
|
+
### 4.1 AsyncImage for Remote Images
|
|
501
|
+
|
|
502
|
+
**Check for:**
|
|
503
|
+
- [ ] AsyncImage for remote images
|
|
504
|
+
- [ ] Proper placeholder and error states
|
|
505
|
+
- [ ] No synchronous image loading
|
|
506
|
+
|
|
507
|
+
**Examples:**
|
|
508
|
+
|
|
509
|
+
❌ **Bad: Synchronous image loading**
|
|
510
|
+
```swift
|
|
511
|
+
if let data = try? Data(contentsOf: imageURL), // ❌ Blocks main thread
|
|
512
|
+
let image = UIImage(data: data) {
|
|
513
|
+
Image(uiImage: image)
|
|
514
|
+
}
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
✅ **Good: AsyncImage**
|
|
518
|
+
```swift
|
|
519
|
+
AsyncImage(url: imageURL) { phase in // ✅ Async with caching
|
|
520
|
+
switch phase {
|
|
521
|
+
case .success(let image):
|
|
522
|
+
image
|
|
523
|
+
.resizable()
|
|
524
|
+
.aspectRatio(contentMode: .fit)
|
|
525
|
+
case .failure:
|
|
526
|
+
Image(systemName: "photo")
|
|
527
|
+
.foregroundColor(.gray)
|
|
528
|
+
case .empty:
|
|
529
|
+
ProgressView()
|
|
530
|
+
@unknown default:
|
|
531
|
+
EmptyView()
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### 4.2 Image Sizing and Scaling
|
|
537
|
+
|
|
538
|
+
**Check for:**
|
|
539
|
+
- [ ] Proper image sizing (not loading huge images for small views)
|
|
540
|
+
- [ ] .resizable() used appropriately
|
|
541
|
+
- [ ] Aspect ratio preserved
|
|
542
|
+
|
|
543
|
+
**Examples:**
|
|
544
|
+
|
|
545
|
+
❌ **Bad: Full-size image in thumbnail**
|
|
546
|
+
```swift
|
|
547
|
+
Image("large-photo") // ❌ 4000x3000 image for 100x100 thumbnail
|
|
548
|
+
.frame(width: 100, height: 100)
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
✅ **Good: Properly sized image**
|
|
552
|
+
```swift
|
|
553
|
+
// Provide appropriately sized asset
|
|
554
|
+
Image("photo-thumbnail") // ✅ 100x100 or 200x200 for @2x
|
|
555
|
+
.resizable()
|
|
556
|
+
.frame(width: 100, height: 100)
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
✅ **Good: Dynamic resizing with AsyncImage**
|
|
560
|
+
```swift
|
|
561
|
+
AsyncImage(url: thumbnailURL) { image in // ✅ Request thumbnail size
|
|
562
|
+
image
|
|
563
|
+
.resizable()
|
|
564
|
+
.aspectRatio(contentMode: .fill)
|
|
565
|
+
.frame(width: 100, height: 100)
|
|
566
|
+
.clipped()
|
|
567
|
+
} placeholder: {
|
|
568
|
+
ProgressView()
|
|
569
|
+
}
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### 4.3 Image Caching
|
|
573
|
+
|
|
574
|
+
**Check for:**
|
|
575
|
+
- [ ] AsyncImage built-in caching leveraged
|
|
576
|
+
- [ ] Custom caching for specific needs
|
|
577
|
+
- [ ] Memory-efficient cache implementation
|
|
578
|
+
|
|
579
|
+
**Examples:**
|
|
580
|
+
|
|
581
|
+
✅ **Good: AsyncImage caching (built-in)**
|
|
582
|
+
```swift
|
|
583
|
+
AsyncImage(url: imageURL) // ✅ Automatically cached
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
✅ **Good: Custom cache for specific needs**
|
|
587
|
+
```swift
|
|
588
|
+
actor ImageCache {
|
|
589
|
+
private var cache: [URL: UIImage] = [:]
|
|
590
|
+
|
|
591
|
+
func image(for url: URL) -> UIImage? {
|
|
592
|
+
cache[url]
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
func cache(_ image: UIImage, for url: URL) {
|
|
596
|
+
cache[url] = image
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
func clearCache() {
|
|
600
|
+
cache.removeAll()
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
---
|
|
606
|
+
|
|
607
|
+
## 5. Memory Management
|
|
608
|
+
|
|
609
|
+
### 5.1 Retain Cycles
|
|
610
|
+
|
|
611
|
+
**Check for:**
|
|
612
|
+
- [ ] No strong reference cycles with closures
|
|
613
|
+
- [ ] [weak self] in closures when necessary
|
|
614
|
+
- [ ] Proper capture lists
|
|
615
|
+
|
|
616
|
+
**Examples:**
|
|
617
|
+
|
|
618
|
+
❌ **Bad: Retain cycle**
|
|
619
|
+
```swift
|
|
620
|
+
@Observable
|
|
621
|
+
final class ViewModel {
|
|
622
|
+
var onComplete: (() -> Void)?
|
|
623
|
+
|
|
624
|
+
func setup() {
|
|
625
|
+
onComplete = {
|
|
626
|
+
self.finish() // ❌ Retain cycle
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
func finish() { }
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
✅ **Good: Weak self**
|
|
635
|
+
```swift
|
|
636
|
+
@Observable
|
|
637
|
+
final class ViewModel {
|
|
638
|
+
var onComplete: (() -> Void)?
|
|
639
|
+
|
|
640
|
+
func setup() {
|
|
641
|
+
onComplete = { [weak self] in // ✅ Weak capture
|
|
642
|
+
self?.finish()
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
func finish() { }
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
✅ **Good: Unowned for guaranteed lifetime**
|
|
651
|
+
```swift
|
|
652
|
+
@Observable
|
|
653
|
+
final class ViewModel {
|
|
654
|
+
let dependency: Dependency
|
|
655
|
+
|
|
656
|
+
func setup() {
|
|
657
|
+
dependency.onEvent = { [unowned self] in // ✅ Unowned when guaranteed
|
|
658
|
+
self.handleEvent()
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
### 5.2 Large Data Structures
|
|
665
|
+
|
|
666
|
+
**Check for:**
|
|
667
|
+
- [ ] Lazy loading for large datasets
|
|
668
|
+
- [ ] Pagination for network data
|
|
669
|
+
- [ ] Data clearing when not needed
|
|
670
|
+
|
|
671
|
+
**Examples:**
|
|
672
|
+
|
|
673
|
+
❌ **Bad: Loading all data at once**
|
|
674
|
+
```swift
|
|
675
|
+
@Observable
|
|
676
|
+
final class DataViewModel {
|
|
677
|
+
var allItems: [Item] = [] // ❌ Could be thousands
|
|
678
|
+
|
|
679
|
+
func loadAllData() async {
|
|
680
|
+
allItems = await fetchAllItems() // ❌ Load everything
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
✅ **Good: Pagination**
|
|
686
|
+
```swift
|
|
687
|
+
@Observable
|
|
688
|
+
final class DataViewModel {
|
|
689
|
+
var items: [Item] = []
|
|
690
|
+
private var currentPage = 0
|
|
691
|
+
private let pageSize = 50
|
|
692
|
+
|
|
693
|
+
func loadNextPage() async {
|
|
694
|
+
let newItems = await fetchItems(page: currentPage, size: pageSize)
|
|
695
|
+
items.append(contentsOf: newItems)
|
|
696
|
+
currentPage += 1
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
### 5.3 Resource Cleanup
|
|
702
|
+
|
|
703
|
+
**Check for:**
|
|
704
|
+
- [ ] Resources released when view disappears
|
|
705
|
+
- [ ] Cancellation for async tasks
|
|
706
|
+
- [ ] Proper cleanup in deinit
|
|
707
|
+
|
|
708
|
+
**Examples:**
|
|
709
|
+
|
|
710
|
+
✅ **Good: Task cancellation**
|
|
711
|
+
```swift
|
|
712
|
+
struct ContentView: View {
|
|
713
|
+
@State private var data: [Item] = []
|
|
714
|
+
|
|
715
|
+
var body: some View {
|
|
716
|
+
List(data) { item in
|
|
717
|
+
ItemRow(item: item)
|
|
718
|
+
}
|
|
719
|
+
.task { // ✅ Automatically cancelled on disappear
|
|
720
|
+
await loadData()
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private func loadData() async {
|
|
725
|
+
// Task automatically cancelled when view disappears
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
✅ **Good: Manual cleanup**
|
|
731
|
+
```swift
|
|
732
|
+
@Observable
|
|
733
|
+
final class ViewModel {
|
|
734
|
+
private var timer: Timer?
|
|
735
|
+
|
|
736
|
+
func startTimer() {
|
|
737
|
+
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
|
738
|
+
// Timer logic
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
deinit {
|
|
743
|
+
timer?.invalidate() // ✅ Cleanup
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
```
|
|
747
|
+
|
|
748
|
+
---
|
|
749
|
+
|
|
750
|
+
## 6. Background Task Efficiency
|
|
751
|
+
|
|
752
|
+
### 6.1 Async/Await Patterns
|
|
753
|
+
|
|
754
|
+
**Check for:**
|
|
755
|
+
- [ ] Proper async/await usage
|
|
756
|
+
- [ ] No blocking main thread
|
|
757
|
+
- [ ] Efficient task management
|
|
758
|
+
|
|
759
|
+
**Examples:**
|
|
760
|
+
|
|
761
|
+
❌ **Bad: Blocking main thread**
|
|
762
|
+
```swift
|
|
763
|
+
Button("Load") {
|
|
764
|
+
let data = loadData() // ❌ Synchronous, blocks UI
|
|
765
|
+
processData(data)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
func loadData() -> Data {
|
|
769
|
+
// Long-running operation
|
|
770
|
+
}
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
✅ **Good: Async operation**
|
|
774
|
+
```swift
|
|
775
|
+
Button("Load") {
|
|
776
|
+
Task { // ✅ Async task
|
|
777
|
+
await loadData()
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
func loadData() async {
|
|
782
|
+
// Long-running operation on background
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### 6.2 Concurrent Operations
|
|
787
|
+
|
|
788
|
+
**Check for:**
|
|
789
|
+
- [ ] TaskGroup for multiple concurrent operations
|
|
790
|
+
- [ ] Efficient concurrency patterns
|
|
791
|
+
- [ ] Proper error handling
|
|
792
|
+
|
|
793
|
+
**Examples:**
|
|
794
|
+
|
|
795
|
+
❌ **Bad: Sequential operations**
|
|
796
|
+
```swift
|
|
797
|
+
func loadAllData() async {
|
|
798
|
+
let users = await fetchUsers() // ❌ Wait
|
|
799
|
+
let posts = await fetchPosts() // ❌ Wait
|
|
800
|
+
let comments = await fetchComments() // ❌ Wait
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
✅ **Good: Concurrent operations**
|
|
805
|
+
```swift
|
|
806
|
+
func loadAllData() async {
|
|
807
|
+
await withTaskGroup(of: Void.self) { group in
|
|
808
|
+
group.addTask { await self.fetchUsers() } // ✅ Parallel
|
|
809
|
+
group.addTask { await self.fetchPosts() } // ✅ Parallel
|
|
810
|
+
group.addTask { await self.fetchComments() } // ✅ Parallel
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## 7. Animation Performance
|
|
818
|
+
|
|
819
|
+
### 7.1 Efficient Animations
|
|
820
|
+
|
|
821
|
+
**Check for:**
|
|
822
|
+
- [ ] Animations on GPU-accelerated properties (opacity, transform)
|
|
823
|
+
- [ ] No animating layout changes excessively
|
|
824
|
+
- [ ] Proper animation curves
|
|
825
|
+
|
|
826
|
+
**Examples:**
|
|
827
|
+
|
|
828
|
+
✅ **Good: GPU-accelerated properties**
|
|
829
|
+
```swift
|
|
830
|
+
Rectangle()
|
|
831
|
+
.opacity(isVisible ? 1.0 : 0.0) // ✅ GPU-accelerated
|
|
832
|
+
.scaleEffect(isExpanded ? 1.2 : 1.0) // ✅ GPU-accelerated
|
|
833
|
+
.animation(.easeInOut, value: isVisible)
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
❌ **Bad: Excessive layout animations**
|
|
837
|
+
```swift
|
|
838
|
+
VStack {
|
|
839
|
+
if isExpanded { // ❌ Layout changes on every animation frame
|
|
840
|
+
ForEach(1...100) { index in
|
|
841
|
+
Text("Item \(index)")
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
.animation(.default, value: isExpanded)
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
✅ **Good: Controlled layout animations**
|
|
849
|
+
```swift
|
|
850
|
+
VStack {
|
|
851
|
+
if isExpanded {
|
|
852
|
+
ForEach(1...10) { index in // ✅ Limited items
|
|
853
|
+
Text("Item \(index)")
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
.animation(.easeInOut(duration: 0.3), value: isExpanded) // ✅ Fast animation
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
## Quick Performance Checklist
|
|
863
|
+
|
|
864
|
+
### Critical (Fix Immediately)
|
|
865
|
+
- [ ] No heavy computation in view body
|
|
866
|
+
- [ ] No synchronous I/O on main thread
|
|
867
|
+
- [ ] No blocking operations in view updates
|
|
868
|
+
- [ ] No retain cycles with closures
|
|
869
|
+
|
|
870
|
+
### High Priority
|
|
871
|
+
- [ ] ForEach uses stable IDs (Identifiable)
|
|
872
|
+
- [ ] Equatable conformance for view models
|
|
873
|
+
- [ ] AsyncImage for remote images
|
|
874
|
+
- [ ] LazyVStack/LazyHStack for large lists
|
|
875
|
+
- [ ] Minimal GeometryReader usage
|
|
876
|
+
|
|
877
|
+
### Medium Priority
|
|
878
|
+
- [ ] Computed properties for derived data
|
|
879
|
+
- [ ] Pagination for large datasets
|
|
880
|
+
- [ ] Proper task cancellation
|
|
881
|
+
- [ ] Efficient image sizing
|
|
882
|
+
- [ ] Resource cleanup in deinit
|
|
883
|
+
|
|
884
|
+
### Low Priority
|
|
885
|
+
- [ ] Animation performance optimization
|
|
886
|
+
- [ ] Layout protocol for custom layouts
|
|
887
|
+
- [ ] Image caching strategies
|
|
888
|
+
- [ ] TaskGroup for concurrent operations
|
|
889
|
+
|
|
890
|
+
---
|
|
891
|
+
|
|
892
|
+
## Performance Profiling Tips
|
|
893
|
+
|
|
894
|
+
When code review identifies potential performance issues, recommend using Instruments:
|
|
895
|
+
|
|
896
|
+
**Key Instruments:**
|
|
897
|
+
- **Time Profiler**: Identify CPU bottlenecks
|
|
898
|
+
- **Allocations**: Track memory usage
|
|
899
|
+
- **Leaks**: Find retain cycles
|
|
900
|
+
- **SwiftUI**: View body execution tracking
|
|
901
|
+
- **Animation Hitches**: Find janky animations
|
|
902
|
+
|
|
903
|
+
**Common Issues to Profile:**
|
|
904
|
+
- View body execution frequency
|
|
905
|
+
- Layout calculation time
|
|
906
|
+
- Image loading and decoding
|
|
907
|
+
- List scrolling performance
|
|
908
|
+
- Memory growth over time
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
## Version
|
|
913
|
+
**Last Updated**: 2026-02-10
|
|
914
|
+
**Version**: 1.0.0
|