NOASSERTIONupdated 1mo ago
Let be the absolute plugin directory two levels above this SKILL.md. Resolve that path before running commands or opening shared references. When these instructions say swiftui-ctx, invoke /scripts/swiftui-ctx; do not assume the command is on PATH.
What can you do with Audit Swiftui View Performance?
name: audit-swiftui-view-performance description: Audit macOS SwiftUI view performance for correctness, current APIs, and production conventions. Use for that domain or as part of a full audit.
Bundled resource root
Let <swiftui-plugin-root> be the absolute plugin directory two levels above this SKILL.md. Resolve that path before running commands or opening shared references. When these instructions say swiftui-ctx, invoke <swiftui-plugin-root>/scripts/swiftui-ctx; do not assume the command is on PATH.
Audit SwiftUI View Performance
AUDIT-ONLY Β· macOS-only Β· SwiftUI-only. Run this on a finished or in-progress macOS SwiftUI
project to detect β and where certain, fix β every way view rendering goes needlessly expensive:
heavyweight allocations in body, identity churn that recreates subtrees, type-erasure that defeats
diffing, un-skippable children, greedy GeometryReader, work in init, per-render filter/sort, and
high-frequency @Environment fan-out. Findings are written to disk in the toolkit's unified schema;
only the genuinely mechanical defects are fixed under the fix-safety protocol. This is never a
from-scratch view generator.
SwiftUI re-renders are driven by view identity and dependency tracking. These anti-patterns
force needless body re-evaluation or full view recreation. They bite harder on macOS: a
resizable, long-lived, multi-window Mac app re-renders far more than a transient iOS screen, so a
greedy GeometryReader or an @Environment timer tick fires constantly on every drag of a window edge.
Boundary / seam note (stay in lane)
- State correctness β "the view won't update", "state resets", wrong ownership wrapper β belongs
to
audit-swiftui-state-observation. This skill owns the render-cost of over-broad observation and the computed-some Viewsmell (the perf number); state-observation owns the granularity / correctness angle. Emit across_refon that shared seam β don't double-own. - Animation cost (
.repeatForever, expensivewithAnimationdriving re-renders) β the UX restraint call isaudit-swiftui-animation-motion's; this skill takes only the render-cost angle andcross_refs it. .drawingGroup()usage decision belongs toaudit-swiftui-drawing-canvas; this skill measures only its cost andcross_refs.Tablecolumn structure / large-grid layout belongs toaudit-swiftui-layout-and-tables; this skill flags the dataset-size ceiling andcross_refs.NSTableView/NSTextViewbridge implementation isaudit-swiftui-appkit-interop's; the render-cost ceiling that justifies the bridge is ours βcross_refit. Liquid Glass GPU cost isaudit-swiftui-liquid-glass's API/placement turf; we note the high-frequency-glass smell and route there.- The blanket "is every OS-floored API gated" sweep belongs to
audit-swiftui-availability-gating; this skill gates the one floored API it suggests (Text(_:format:), macOS 12.0+ forFormatOutput == String/ macOS 15.0+ forAttributedString) and defers there.
The rendering model (three load-bearing facts)
bodyruns on every dependency change. Anything allocated or computed inside it pays that price on every render β hoist heavyweight work tostatic let,.task, or an@Observablemethod.- Identity is the diffing key. A fresh
.id(UUID())or anAnyViewthrows away the identity SwiftUI needs to skip an unchanged subtree, forcing recreation and state loss. - Dependencies fan out by where they're read. A fast-changing value in
@Environmentre-evaluates every subscriber's subtree on every tick; a closure prop a child can't value-compare makes that child un-skippable. Keep fast state narrowly scoped (WWDC25 session 306).
The render test: drop Self._printChanges() as the first line of a suspect body (it prints which
dependency β @self, a named property, or @identity β caused that re-evaluation). @identity
churn β identity bug (vperf-02/03); a property you didn't expect β over-broad observation
(vperf-09). Full recipe + reasoning: references/rendering-model-and-profiling.md.
Defect index (vperf-01 β¦ vperf-12)
id Β· tell Β· severity Β· fix Β· open reference. Severities: hard-fail (never-correct / build-break),
warning (compiles but wasteful), advisory (judgment / measure-first). auto = mechanical
single-answer fix; flag = show the β
, dev applies.
| id | One-line tell | Sev | Fix | Reference |
|---|---|---|---|---|
| vperf-01 | DateFormatter(/NumberFormatter(/JSONDecoder(/ISO8601DateFormatter(/JSONEncoder(/RelativeDateTimeFormatter( built inside body or a computed view prop |
warning | flag | body-and-init-cost.md |
| vperf-02 | .id(UUID()) / .id(UUID().uuidString) β fresh identity every render |
warning | auto | identity-and-erasure.md |
| vperf-03 | AnyView( in view code β erases the type SwiftUI diffs on |
warning | flag | identity-and-erasure.md |
| vperf-04 | a closure passed as a child view's stored prop (child can't be skipped) | advisory | flag | skippability-and-observation.md |
| vperf-05 | GeometryReader wrapping a whole screen / large subtree |
advisory | flag | body-and-init-cost.md |
| vperf-06 | non-trivial statements inside a View's init |
warning | flag | body-and-init-cost.md |
| vperf-07 | .filter/.sorted/.map directly inside a ForEach(...) argument |
warning | flag | collections-and-ceilings.md |
| vperf-08 | a fast-changing value (timer/drag/scroll geometry) stored in @Environment read by many views |
advisory | flag | skippability-and-observation.md |
| vperf-09 | a view reads a whole broad @Observable model where one field would do (over-broad observation) |
advisory | flag | skippability-and-observation.md |
| vperf-10 | Table( over a 10k+ row dataset with heavy/editable cells (FB13639482 ceiling) |
advisory | flag | collections-and-ceilings.md |
| vperf-11 | a large ForEach not inside a LazyVStack/LazyVGrid/List/Table (eager build) |
advisory | flag | collections-and-ceilings.md |
| vperf-12 | Self._printChanges() left in a shipping body |
advisory | auto | rendering-model-and-profiling.md |
One claim is measurement-bound β carry as advisory, never assert a fixed threshold as fact
(flagged in its reference + becomes source: verify against Xcode 26 SDK): the Table row count where
jank starts (vperf-10) β FB13639482 has no confirmed fix milestone in Apple release notes, and
practitioner reports put a plain List at ~10k smooth / ~50k usable on macOS 26, so the old
"few-hundred-row" ceiling no longer holds for plain List. Measure on your target.
The real API, at a glance
These are the fix targets β all real on macOS, confirmed via swiftui-ctx lookup (see VERIFY):
Text(_:format:)(FormatStyle overload, macOS 12.0+ forFormatOutput == String; macOS 15.0+ forFormatOutput == AttributedString) β replaces aDateFormatterinbody.@ViewBuilder(returnssome View) β replaces anAnyView-returning helper.EquatableView/Equatableconformance (macOS 10.15+) β makes a child with a closure prop skippable by comparing its other props.LazyVStack/LazyVGrid/List/Tableβ lazy containers for large collections.Layout(macOS 13.0+) /.frame/.alignmentGuide/containerRelativeFrame(macOS 14.0+) β replace a greedyGeometryReaderwhen you only need arrangement, not the measured size..task/@Observablemodel methods /static letβ homes for work wrongly placed ininit/body.
No view-performance defect is a hallucinated symbol β AnyView, GeometryReader, .id(_:),
@Environment all exist and are real; the defect is misuse, not invention. (Confirmed:
swiftui-ctx deprecated AnyView β not deprecated, no replacement β it's a real API used wrongly.) So
findings here are warning/advisory, never hard-fail for a "fake API." Floor values are the
reconciled truth in <swiftui-plugin-root>/references/_shared/floors-master.md β read, never restate.
Grounded β β the consensus shape (real, permalinked, not invented)
The β
/## Correct block a finding embeds is the swiftui-ctx consensus shape of a real call site,
never a hand-written snippet. Worked example for the large-collection ceiling (vperf-07/10/11), confirmed
live via bash <swiftui-plugin-root>/scripts/swiftui-ctx lookup Table --json β consensus
(_, selection) 34% Β· (_) 26% Β· (of, selection) 5%; Table introduced_macos: 12.0,
deprecated: false. The recommended site (pulled with file ex_0af837984c --smart) virtualizes rows
through the model's already-derived array β rows are materialized lazily by Table, never eagerly built,
and no .filter/.sorted sits in the ForEach argument:
// real macOS-26 call site β github.com/tahseen-kakar/harbor β¦/DownloadsContentView.swift#L13
Table(of: DownloadItem.self, selection: $center.selectedDownloadID) {
TableColumn("Name") { item in DownloadNameCell(item: item) }
TableColumn("Updated") { item in Text(DownloadFormatting.dateString(item.updatedAt)).font(.caption) }
} rows: {
ForEach(center.filteredDownloads) { item in TableRow(item) } // derived array from the model β NOT a .filter/.sorted in the ForEach arg
}
- Real example permalink (goes in
## Source):https://github.com/tahseen-kakar/harbor/blob/064c6b7c706c255ca30ae2c0ce607b6ba21e2edd/Harbor/Views/DownloadsContentView.swift#L13 - Apple spec via Sosumi (the
doc:line):https://sosumi.ai/documentation/swiftui/table
This is the shape of the grounding, not a template to paste β re-run lookup/file --smart per fix
target (e.g. Text β Text(_:format:) for vperf-01) so the β
is current real code, then cite that
permalink. The CLI contract is <swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md.
The 8-step audit workflow (execute verbatim)
- ORIENT.
tree/findthe SwiftUI sources. Read the deployment target (project.pbxprojMACOSX_DEPLOYMENT_TARGET, orPackage.swiftplatforms:). Load-bearing for the one floored fix (Text(_:format:): macOS 12.0+ forFormatOutput == String, macOS 15.0+ forAttributedString); record it. - LOCATE. Run the shared hybrid lint runner:
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-view-performance --dir <sources> --json /tmp/vperf.json --sarif /tmp/vperf.sarif. It runs this skill's tier-1 grep tells (lint/grep-tells.tsv) + tier-2 structural ast-grep rules (lint/ast-grep/*.ymlβ the formatter-inside-bodyand logic-in-initcontainment rules grep can't express), plus a per-file parse probe, and emits unified JSON + SARIF. Read itsparse_warningsβ a flagged file didn't fully parse, so a structural miss can't masquerade as clean; READ those by hand. The runner only LOCATES. Engine + rule-file format + degradation:<swiftui-plugin-root>/references/_shared/lint-architecture.md. - READ. Open every located file in full β never pattern-match-and-patch blind. Whether a
formatter is inside
body, whether aForEachis inside a lazy container, whether a closure prop is stored in the child, and how broad an@Observableread is are all invisible to a flat grep. Build a per-file inventory: each suspect view + which of the three rendering facts it breaks. - DETECT. Apply the index. Assign each candidate a confidence; report a finding only at 100%
certainty (e.g. a literal
DateFormatter()lexically insidebody, an.id(UUID()), anAnyView(in view code). Anything judgment-bound (vperf-04/05/08/09/10/11) needs the READ first. - VERIFY. For anything β€ ~70% confidence (a fix target whose floor you can't place, a behavior
claim, the
Tablethreshold), run both evidence sources. (a) Practice βbash <swiftui-plugin-root>/scripts/swiftui-ctx lookup <api> --json(andswiftui-ctx deprecated <api>for a currency claim): read itsconsensus(the canonical shape),deprecated+replacement,recommendedpermalink,introduced_macos, andco_occurs_with. Alookupexit 3 would corroborate a hallucination β but this domain has none (all symbols are real-but-misused), so use the lookup to ground the β shape, not to prove nonexistence. (b) Spec β confirm via Sosumi:curl -sSL https://sosumi.ai/<apple-path>usingreferences/source-directory.mdfor the path and<swiftui-plugin-root>/references/_shared/sosumi-reference.mdfor the protocol (neverWebFetchdeveloper.apple.com). Cross-checkintroduced_macosagainstfloors-master.md. The CLI contract is<swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md. Carry theTablethreshold asadvisorywithsource: verify against Xcode 26 SDKβ never as a fixed number. - REPORT. Write each confirmed finding (output contract below). One finding per file, zero-padded,
ordered. Write the run's
_index.md. - FIX. Apply corrections under the fix-safety protocol
(
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md): clean-tree gate, findings-first, onlyfix_mode: auto(vperf-02.id(UUID())βstable id, vperf-12 strip the debug line), one conventional commit per finding citing itsrule_id, never weaken a check. The β "Correct" is not a hand-written snippet β it is the swiftui-ctx consensus shape put in## Correct, backed by a real macOS-era example fetched withbash <swiftui-plugin-root>/scripts/swiftui-ctx file <recommended.id> --smartwhose GitHub permalink (plus the Sosumidoc:) goes in## Source. Leaveflag-onlyfindingsopenwith that β in## Correct. - DOUBLE-CHECK. Re-grep each fixed file to confirm the tell no longer matches; record the evidence
in
## Fix applied?. Confirm every citation still resolves. If a fix introduced a new tell (e.g. hoisting a formatter to astatic letyou then mis-floored), loop that file back to DETECT.
Confidence gating (load-bearing)
Report a finding only at 100% certainty. The lexical defects (vperf-01 in body, vperf-02,
vperf-03, vperf-12) clear that bar on a READ; the judgment defects (vperf-04/05/08/09/10/11) must pass
the READ β a lone GeometryReader that genuinely needs the measured size is correct, a small
ForEach outside a lazy container is fine. Anything β€ ~70% goes to VERIFY before it becomes a
finding. Auto-fix only the mechanical set (vperf-02, vperf-12); everything else is fix_mode: flag-only.
Output contract
Inherits the toolkit's unified contract (full schema + body sections + frontmatter keys:
<swiftui-plugin-root>/references/_shared/finding-schema.md β do not restate it). Specialized for this
domain:
- Findings:
swiftui-audits/view-performance/<context>/NN-slug.md(one finding per file, zero-padded, ordered). Per-run index:swiftui-audits/view-performance/_index.md. domain: view-performance. Frontmatter is the canonical schema;fix_modeisautofor vperf-02/12, elseflag-only.availabilityreads fromfloors-master.md(relevant only for theText(_:format:)fix target).sourceis an Apple URL + access date (via Sosumi) orverify against Xcode 26 SDK(theTablethreshold).
Starter <context> folders (file here whenβ¦):
<context> |
File a finding here when⦠|
|---|---|
body-cost/ |
a heavyweight is allocated in body/a computed view prop, or GeometryReader wraps a large subtree (vperf-01, vperf-05) |
identity-churn/ |
identity is thrown away β .id(UUID()) or an AnyView in view code (vperf-02, vperf-03) |
skippability/ |
a child can't be skipped β a closure prop, a high-frequency @Environment value, or over-broad observation (vperf-04, vperf-08, vperf-09) |
init-cost/ |
real logic runs in a View's init (vperf-06) |
collection-cost/ |
per-render filter/sort in ForEach, an eager non-lazy ForEach, or the large-Table ceiling (vperf-07, vperf-10, vperf-11) |
profiling-leftovers/ |
a Self._printChanges() was left in shipping code (vperf-12) |
New-folder rule: if a finding does not fit any existing context folder, create a new one under
swiftui-audits/view-performance/ with a lowercase-hyphen slug naming the sub-category, and note it in
the run's _index.md. Prefer an existing folder when the fit is reasonable; consistency across runs is
a hard requirement. Two runs over the same code produce structurally identical trees.
Go-beyond artifact (optional):
swiftui-audits/view-performance/_render-cost-map.mdlisting each suspect view + theSelf._printChanges()dependency that drives its re-render β seereferences/rendering-model-and-profiling.md.
Reference routing
| File | Open when |
|---|---|
references/body-and-init-cost.md |
a heavyweight in body, greedy GeometryReader, or logic in init β the βββ
hoist patterns (vperf-01/05/06) |
references/identity-and-erasure.md |
identity churn or type-erasure β .id(UUID()), AnyView, @ViewBuilder (vperf-02/03) |
references/skippability-and-observation.md |
un-skippable children β closure props (Equatable/EquatableView), high-frequency @Environment, over-broad @Observable (vperf-04/08/09) |
references/collections-and-ceilings.md |
collection cost β per-render filter/sort in ForEach, lazy containers, the Table/List dataset ceiling (vperf-07/10/11) |
references/rendering-model-and-profiling.md |
the identity/dependency model, Self._printChanges(), the SwiftUI Instrument, the leftover-debug strip (vperf-12) + the render-cost map |
references/source-directory.md |
step VERIFY β the Apple/WWDC/practitioner source map fetched via Sosumi |
lint/grep-tells.tsv + lint/ast-grep/*.yml |
step LOCATE β this skill's declarative lint rule set fed to the shared runner (tier-1 grep tells + tier-2 structural ast-grep); edit here to tune detection |
Shared toolkit references (point in, never restate):
| Shared file | For |
|---|---|
<swiftui-plugin-root>/references/_shared/floors-master.md |
every floor/availability value (the reconciled truth β e.g. Text(_:format:): macOS 12.0+ (String) / 15.0+ (AttributedString)) |
<swiftui-plugin-root>/references/_shared/hallucination-blacklist.md |
the canonical invented-name list |
<swiftui-plugin-root>/references/_shared/finding-schema.md |
the unified finding schema + frontmatter keys |
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md |
the 8-point fix-safety protocol (step 7) |
<swiftui-plugin-root>/references/_shared/sosumi-reference.md |
the Apple-doc spec fetch protocol (step 5 VERIFY) |
<swiftui-plugin-root>/references/_shared/swiftui-ctx-reference.md |
the practice-corpus CLI contract β lookup/deprecated/file --smart for the consensus shape + permalinked example (steps 5 VERIFY Β· 7 FIX) |
<swiftui-plugin-root>/references/_shared/cross-ref-graph.md |
seam ownership + cross_ref targets (state-observation, animation-motion, drawing-canvas, layout-and-tables, appkit-interop, liquid-glass) |
Detection accelerator
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-view-performance --dir <files-or-dir> [--json out.json] [--sarif out.sarif] β the toolkit's one shared hybrid lint engine, fed this
skill's declarative rules: tier-1 grep tells (lint/grep-tells.tsv,
vperf-02/03/04/07/08/09/10/11/12) + tier-2 ast-grep structural rules (lint/ast-grep/*.yml β
vperf-01 formatter-inside-body, vperf-06 logic-in-init) that grep cannot express. It runs a
per-file parse probe (surfaces "did not fully parse" so a structural miss can't look clean), emits
unified JSON + SARIF, and degrades to grep-only with a notice if ast-grep is unreachable
(npx --package @ast-grep/cli ast-grep; faster: brew install ast-grep). This domain has no
hard-fail tell (every defect is real-but-misused, not a build break), so the runner exits 0 here β
the value is the located candidate set, not a CI gate. It only LOCATES β always READ each hit in full
before reporting (step 3). The thin scripts/vperf-lint.sh is a pointer to this runner. Engine +
rule-file format + JSON/SARIF shape + safety rails:
<swiftui-plugin-root>/references/_shared/lint-architecture.md.
Install
Add Audit Swiftui View Performance to your client. Pick the one you use.
npx skills add yigitkonur/plugin-swiftuiInstalls every skill in the repository, then prompts for which to keep.
/plugin marketplace add yigitkonur/plugin-swiftuiAdds the repository as a plugin marketplace; install individual plugins with `/plugin install`.
git clone https://github.com/yigitkonur/plugin-swiftui
cp -r plugins/swiftui/skills/audit-swiftui-view-performance ~/.claude/skills/A skill is a plain directory. Copy it into `.claude/skills/` in a project or in your home directory.
Score
80 / 100
Excellent