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 Async Data?
name: audit-swiftui-async-data description: Audit macOS SwiftUI async data 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 Async Data
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 async data loading inside a view goes wrong: a bare
Task in .onAppear that never cancels, no loading / error / empty state, raw URLSession decoded on the
main actor, search that fetches on every keystroke, AsyncImage whose failure phase is ignored, a list
with no .refreshable, a stale result overwriting a fresh one, and a load with no .redacted skeleton.
Findings are written to disk in the toolkit's unified schema; the few mechanical defects are fixed under
the fix-safety protocol. This is never a from-scratch data-layer generator.
This domain is net-new (the relevant .task/Sendable facts live in
<swiftui-plugin-root>/skills/build-macos-swiftui/references/concurrency.md); ground every β
in
swiftui-ctx consensus + a permalinked macOS-26 example, not a hand-written snippet.
Boundary / seam note (stay in lane)
Task-in-onAppearis a SHARED seam withconcurrency-safety. This skill owns the LIFECYCLE fix β the Task is unstructured and not cancelled on disappear β move to.task/.task(id:).concurrency-safetyowns the ISOLATION verdict on the captured state (is the crossing typeSendable, does it race a@MainActor). When the captured loading state is non-Sendable, emit across_ref: concurrency-safetyand fix only the lifecycle here.- Where the model lives (
@Statevs@Observablevs@StateObject, observation granularity) belongs tostate-observationβcross_refit; this skill only audits the loading that mutates it. @Query/ SwiftData fetching belongs toswiftdata; the Swift-6Sendable/@concurrent/nonisolatedcorrectness belongs toconcurrency-safety;DispatchQueue.main.asyncas a deprecated currency tell belongs toapi-currency. Defer all three.
The four async-data rules
- Bind async work to view identity. View-lifecycle loads go in
.task/.task(id:)(auto-cancelled on disappear, closure is@MainActor), never a bareTask {}in.onAppear. - Every load has four visible states. loading Β· loaded Β· empty Β· error β each rendered. A spinner
that never resolves, a blank list, or a swallowed
try? awaitis a defect. - Don't fetch faster than the user. Debounce a search query before it drives a request; guard rapid
.task(id:)writes with a generation counter so an older load can't overwrite a newer one. - Remote images and pull-to-refresh are first-class.
AsyncImagehandles its failure phase and is cached in lists; a primary async list offers.refreshable; a load shows a.redactedskeleton.
Defect index (async-01 β¦ async-10)
id Β· tell Β· severity Β· fix Β· open reference. Severities: hard-fail (build break / never-correct),
warning (compiles but non-native), advisory (judgment / perf). auto = mechanical single-answer
fix; flag = show the β
, dev applies. Defects marked DETECT-only are absence defects β no grep/AST
tell can fire on a missing state; find them by READING the load site the proxy tells locate.
| id | One-line tell | Sev | Fix | Reference |
|---|---|---|---|---|
| async-01 | bare Task {} in .onAppear (no cancellation) β .task/.task(id:) |
warn | flag | lifecycle-and-cancellation.md |
| async-02 | async load with no loading state (no isLoading/ProgressView/skeleton) β DETECT-only |
warn | flag | load-states-and-skeletons.md |
| async-03 | try? await swallows the error; no error state rendered |
warn | flag | load-states-and-skeletons.md |
| async-04 | a collection rendered with no empty-case view β DETECT-only | adv | flag | load-states-and-skeletons.md |
| async-05 | raw URLSession in a view; decode on the main actor / un-isolated write |
warn | flag | networking-and-images.md |
| async-06 | .searchable query drives a fetch with no debounce (request per keystroke) |
warn | flag | networking-and-images.md |
| async-07 | AsyncImage(url:) url-only shape β failure phase unhandled, no cache in lists |
warn | flag | networking-and-images.md |
| async-08 | primary async list with no .refreshable β DETECT-only |
adv | flag | networking-and-images.md |
| async-09 | rapid .task(id:) writes with no generation/stale-result guard |
warn | flag | lifecycle-and-cancellation.md |
| async-10 | load with no .redacted(.placeholder) skeleton β DETECT-only |
adv | flag | load-states-and-skeletons.md |
Nothing here is a hallucination class β every API is real and floored low (.task macOS 12,
AsyncImage/refreshable/searchable macOS 12, .redacted macOS 11). The defects are omission and
lifecycle, not invented names; carry behavior claims you cannot place as advisory (source: verify against Xcode 26 SDK), never as fact.
The real API, at a glance
Real (all macOS, floored low β confirm exact floors in floors-master.md, never restate the table):
task(priority:_:) and task(id:priority:_:), refreshable(action:), searchable(text:β¦),
AsyncImage(url:) / AsyncImage(url:content:placeholder:) / AsyncImage(url:transaction:content:) (the
phase form), redacted(reason:) + RedactionReasons.placeholder, unredacted(), Task/Task(id:)
cancellation via Task.isCancelled / Task.checkCancellation().
Floor values are the reconciled truth in <swiftui-plugin-root>/references/_shared/floors-master.md;
the canonical invented-name list is <swiftui-plugin-root>/references/_shared/hallucination-blacklist.md
β read, never restate them. The β
shapes are not hand-written: get the consensus shape + a permalinked
macOS-26 example from swiftui-ctx (VERIFY/FIX below).
β Correct (grounded, not a placeholder) β the async-01 lifecycle anchor
swiftui-ctx lookup task --json consensus: { } 70% Β· (id) 29%, introduced_macos: 12.0. The
top-authority macOS-26 site (swiftui-ctx file ex_a1cff2419c --smart) β bind the load to view identity so
it auto-cancels on disappear; .task(id:) restarts on change:
// sindresorhus/Gifski β Gifski/Utilities.swift L5590 (min_macos 26)
content
.task(id: Tuple3(isActive, options, reason)) { // restarts when the id changes; cancels on disappear
activity = isActive ? SSApp.beginActivity(options, reason: reason) : nil
}
- Real example (permalink):
https://github.com/sindresorhus/Gifski/blob/7f873856e2acd8b52e6681dee3aec31e6cab23e4/Gifski/Utilities.swift#L5590 - Spec (Sosumi):
https://sosumi.ai/documentation/swiftui/view/task(id:priority:_:)(the@MainActor, auto-cancelled lifecycle modifier). Re-confirm the floor infloors-master.mdbefore asserting it.
The 8-step audit workflow (execute verbatim)
- ORIENT.
tree/findthe SwiftUI sources. Read the deployment target (project.pbxprojMACOSX_DEPLOYMENT_TARGET, orPackage.swiftplatforms:). Record it β every API here floors at macOS 11β12, so gating rarely fires, but note any target < macOS 12. - LOCATE. Run the shared hybrid lint runner:
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-async-data --dir <sources> --json /tmp/async.json --sarif /tmp/async.sarif. It runs this skill's tier-1 grep tells (lint/grep-tells.tsv) + tier-2 structural ast-grep rules (lint/ast-grep/*.ymlβ Task-in-onAppear containment, AsyncImage url-only shape), plus a per-file parse probe, and emits unified JSON + SARIF. Read itsparse_warningsβ a flagged file did not fully parse, so a structural miss can't masquerade as clean; READ those by hand. The runner only LOCATES. The absence defects (async-02/04/08/10) will NOT fire any tell β find them in READ. 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. For each view that
loads data, build an inventory: the load trigger (
.task/.onAppear/button), the four states it renders (loading/loaded/empty/error), whether its writes are guarded against stale results, and whether remote images / refresh / skeleton are present. Absence is the finding here. - DETECT. Apply the index. Assign each candidate a confidence; report a finding only at 100%
certainty. The absence defects are certain by inspection (the state is simply not there); lifecycle and
isolation seams route a
cross_ref. - VERIFY. For anything β€ ~70% confidence (a floor you can't place, a behavior claim, "is this the
native shape"), 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. For this domain,swiftui-ctx recipe cached-async-imagegives the consensus AsyncImage loader. (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. Promote with the citation or discard. - REPORT. Write each confirmed finding (output contract below). One finding per file, zero-padded,
ordered. Emit
cross_refon the lifecycle/isolation and model-location seams. 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(async-01 the mechanicalTask{}-in-onAppearβ.taskrewrite when the captured state is alreadySendable/@MainActor-safe; everything elseflag-only), 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 in## Correct, backed by a real macOS-26 example fetched withbash <swiftui-plugin-root>/scripts/swiftui-ctx file <recommended.id> --smartwhose GitHub permalink (plus the Sosumidoc:) goes in## Source. Leaveflag-onlyfindingsopenwith that β . - DOUBLE-CHECK. Re-grep / re-run the runner on each fixed file to confirm the tell no longer matches;
record the evidence in
## Fix applied?. Re-confirm every citation still resolves. If an async-01 fix added a.task(id:)that now needs a generation guard (async-09), loop that file back to DETECT.
Confidence gating (load-bearing)
Report a finding only at 100% certainty. Absence defects are certain by inspection; behavior/floor
doubts go to VERIFY (step 5) first. Auto-fix only async-01 (and only when the captured state is already
Sendable/main-actor-safe β otherwise flag-only + cross_ref: concurrency-safety); 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/async-data/<context>/NN-slug.md(one finding per file, zero-padded, ordered). Per-run index:swiftui-audits/async-data/_index.md. domain: async-data. Frontmatter is the canonical schema;fix_modeisautoonly for the mechanical async-01 rewrite, elseflag-only.availabilityreads fromfloors-master.md.sourceis an Apple URL- access date (fetched via Sosumi) or
verify against Xcode 26 SDK. Emitcross_refon seam findings (concurrency-safety lifecycle/isolation; state-observation model location).
- access date (fetched via Sosumi) or
Starter <context> folders (file here whenβ¦):
<context> |
File a finding here when⦠|
|---|---|
lifecycle/ |
a bare Task in .onAppear, or any view-lifecycle work not bound to .task/.task(id:) (async-01) |
stale-results/ |
rapid .task(id:)/selection writes with no generation or cancelled-task guard (async-09) |
loading-state/ |
no loading indicator / no .redacted(.placeholder) skeleton during the fetch (async-02, async-10) |
error-state/ |
a swallowed try? await or any load with no error surface (async-03) |
empty-state/ |
a collection rendered with no empty-case view (async-04) |
networking/ |
raw URLSession in the view layer, on-main decode, or un-isolated write (async-05) |
search-debounce/ |
a .searchable query that fires a request per keystroke (async-06) |
remote-images/ |
AsyncImage failure phase ignored or no cache in a list/grid (async-07) |
refresh/ |
a primary async list with no .refreshable (async-08) |
New-folder rule: if a finding does not fit any existing context folder, create a new one under
swiftui-audits/async-data/ 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.
Reference routing
| File | Open when |
|---|---|
references/lifecycle-and-cancellation.md |
the Taskβ.task lifecycle fix, .task(id:) restart, cancellation, the generation/stale-result guard (async-01, async-09) |
references/load-states-and-skeletons.md |
the four states β loading/empty/error β and .redacted(.placeholder) skeletons; the swallowed-error trap (async-02/03/04/10) |
references/networking-and-images.md |
raw URLSession isolation, .searchable debounce, AsyncImage phases + caching, .refreshable (async-05/06/07/08) |
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) |
<swiftui-plugin-root>/references/_shared/hallucination-blacklist.md |
the canonical invented-name list |
<swiftui-plugin-root>/references/_shared/macos-arm-gating.md |
the macOS-arm gating rule (for any availability gate on an async API) |
<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/recipe/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 (concurrency-safety, state-observation, swiftdata) |
Detection accelerator
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-async-data --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, async-01/03/05/06/07/08/10) + tier-2
ast-grep structural rules (lint/ast-grep/*.yml β async-01 Task-in-onAppear containment, async-07
AsyncImage url-only shape) 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). The absence defects (async-02/04/08/10) fire NO tell β they are found in READ
(step 3). It only LOCATES β always READ each hit in full before reporting. The thin scripts/async-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 Async Data 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-async-data ~/.claude/skills/A skill is a plain directory. Copy it into `.claude/skills/` in a project or in your home directory.
Score
75 / 100
Good