Add fourteen skills from Dimillian/Skills, integrate the merged Snowflake and WordPress updates into the maintainer sync, and refresh registry metadata, attributions, walkthrough notes, and the 8.9.0 release notes while keeping validation warnings within budget.
3.7 KiB
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
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:
final class DistanceFormatter {
static let shared = DistanceFormatter()
let number = NumberFormatter()
let measure = MeasurementFormatter()
}
Heavy computed properties
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
List {
ForEach(items.sorted(by: sortRule)) { item in
Row(item)
}
}
Prefer sorting before render work begins:
let sortedItems = items.sorted(by: sortRule)
Inline filtering inside ForEach
ForEach(items.filter { $0.isEnabled }) { item in
Row(item)
}
Prefer a prefiltered collection with stable identity.
Unstable identity
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
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
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+
@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
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:
- Broad invalidation and observation fan-out
- Unstable identity and list churn
- Main-thread work during render
- Image decode or resize cost
- Layout and animation complexity