151 lines
3.7 KiB
Markdown
151 lines
3.7 KiB
Markdown
# Common code smells and remediation patterns
|
|
|
|
## Intent
|
|
|
|
Use this reference during code-first review to map visible SwiftUI patterns to likely runtime costs and safer remediation guidance.
|
|
|
|
## High-priority smells
|
|
|
|
### Expensive formatters in `body`
|
|
|
|
```swift
|
|
var body: some View {
|
|
let number = NumberFormatter()
|
|
let measure = MeasurementFormatter()
|
|
Text(measure.string(from: .init(value: meters, unit: .meters)))
|
|
}
|
|
```
|
|
|
|
Prefer cached formatters in a model or dedicated helper:
|
|
|
|
```swift
|
|
final class DistanceFormatter {
|
|
static let shared = DistanceFormatter()
|
|
let number = NumberFormatter()
|
|
let measure = MeasurementFormatter()
|
|
}
|
|
```
|
|
|
|
### Heavy computed properties
|
|
|
|
```swift
|
|
var filtered: [Item] {
|
|
items.filter { $0.isEnabled }
|
|
}
|
|
```
|
|
|
|
Prefer deriving this once per meaningful input change in a model/helper, or store derived view-owned state only when the view truly owns the transformation lifecycle.
|
|
|
|
### Sorting or filtering inside `body`
|
|
|
|
```swift
|
|
List {
|
|
ForEach(items.sorted(by: sortRule)) { item in
|
|
Row(item)
|
|
}
|
|
}
|
|
```
|
|
|
|
Prefer sorting before render work begins:
|
|
|
|
```swift
|
|
let sortedItems = items.sorted(by: sortRule)
|
|
```
|
|
|
|
### Inline filtering inside `ForEach`
|
|
|
|
```swift
|
|
ForEach(items.filter { $0.isEnabled }) { item in
|
|
Row(item)
|
|
}
|
|
```
|
|
|
|
Prefer a prefiltered collection with stable identity.
|
|
|
|
### Unstable identity
|
|
|
|
```swift
|
|
ForEach(items, id: \.self) { item in
|
|
Row(item)
|
|
}
|
|
```
|
|
|
|
Avoid `id: \.self` for non-stable values or collections that reorder. Use a stable domain identifier.
|
|
|
|
### Top-level conditional view swapping
|
|
|
|
```swift
|
|
var content: some View {
|
|
if isEditing {
|
|
editingView
|
|
} else {
|
|
readOnlyView
|
|
}
|
|
}
|
|
```
|
|
|
|
Prefer one stable base view and localize conditions to sections or modifiers. This reduces root identity churn and makes diffing cheaper.
|
|
|
|
### Image decoding on the main thread
|
|
|
|
```swift
|
|
Image(uiImage: UIImage(data: data)!)
|
|
```
|
|
|
|
Prefer decode and downsample work off the main thread, then store the processed image.
|
|
|
|
## Observation fan-out
|
|
|
|
### Broad `@Observable` reads on iOS 17+
|
|
|
|
```swift
|
|
@Observable final class Model {
|
|
var items: [Item] = []
|
|
}
|
|
|
|
var body: some View {
|
|
Row(isFavorite: model.items.contains(item))
|
|
}
|
|
```
|
|
|
|
If many views read the same broad collection or root model, small changes can fan out into wide invalidation. Prefer narrower derived inputs, smaller observable surfaces, or per-item state closer to the leaf views.
|
|
|
|
### Broad `ObservableObject` reads on iOS 16 and earlier
|
|
|
|
```swift
|
|
final class Model: ObservableObject {
|
|
@Published var items: [Item] = []
|
|
}
|
|
```
|
|
|
|
The same warning applies to legacy observation. Avoid having many descendants observe a large shared object when they only need one derived field.
|
|
|
|
## Remediation notes
|
|
|
|
### `@State` is not a generic cache
|
|
|
|
Use `@State` for view-owned state and derived values that intentionally belong to the view lifecycle. Do not move arbitrary expensive computation into `@State` unless you also define when and why it updates.
|
|
|
|
Better alternatives:
|
|
- precompute in the model or store
|
|
- update derived state in response to a specific input change
|
|
- memoize in a dedicated helper
|
|
- preprocess on a background task before rendering
|
|
|
|
### `equatable()` is conditional guidance
|
|
|
|
Use `equatable()` only when:
|
|
- equality is cheaper than recomputing the subtree, and
|
|
- the view inputs are value-semantic and stable enough for meaningful equality checks
|
|
|
|
Do not apply `equatable()` as a blanket fix for all redraws.
|
|
|
|
## Triage order
|
|
|
|
When multiple smells appear together, prioritize in this order:
|
|
1. Broad invalidation and observation fan-out
|
|
2. Unstable identity and list churn
|
|
3. Main-thread work during render
|
|
4. Image decode or resize cost
|
|
5. Layout and animation complexity
|