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 State Restoration?
name: audit-swiftui-state-restoration description: Audit macOS SwiftUI state restoration 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 State Restoration
AUDIT-ONLY Β· macOS-only Β· SwiftUI-only. Run this on a finished or in-progress macOS SwiftUI
project to detect β and where certain, flag the fix for β every way scene/app state restoration and
lightweight persistence goes wrong: @AppStorageβ@SceneStorage confusion, hand-rolled
UserDefaults where a wrapper fits, tab/sidebar/detail selection that suffers relaunch amnesia,
NavigationPath that is never persisted, custom types or oversized blobs in size-limited storage, dead
deep links, and ungated restorationBehavior / focusedSceneValue. Findings are written to disk in the
toolkit's unified schema. This is never a from-scratch persistence generator.
This is the toolkit's orphaned domain: @AppStorage (app-wide preferences) versus @SceneStorage
(per-window UI state) is the single most-confused axis. The wrapper compiles either way β the defect is
semantic, so READ before you report.
Boundary / seam note (stay in lane)
- SwiftData
@Modeldesign and@Queryfetches belong toaudit-swiftui-swiftdata. This skill owns the decision to move oversized scene/app storage out (sr-08) and emitscross_ref: audit-swiftui-swiftdata; it does not audit the model graph. - Window/scene plumbing (
WindowGroup/Window/MenuBarExtraactivation,.defaultSize,.windowResizability,handlesExternalEventsas a scene matcher) belongs toaudit-swiftui-scenes-windows. This skill owns the restoration of scene UI state andrestorationBehavior; defer scene activation there with across_ref. - Navigation structure (
NavigationStack/NavigationSplitViewcolumns,NavigationLink) belongs toaudit-swiftui-navigation-toolbars. This skill owns persisting theNavigationPath/selection across relaunch (sr-05/06), not the navigation shape. @FocusedValue/focusedSceneValuecommand routing belongs toaudit-swiftui-menus-commands; this skill flags only thefocusedSceneValueavailability floor (sr-11) andcross_refs the routing owner.
The state-restoration model (the load-bearing distinction)
@AppStorage= app-wide preference. One value shared by every window/scene β theme, default units, "show line numbers". Backed byUserDefaults.@SceneStorage= per-window UI state. Restored per scene by the system β selected tab, sidebar selection, current document section, scroll target. Two windows hold independent values.- Both are size-limited key-value stores. A custom type needs
RawRepresentable/Codable; largeData/arrays/images belong in SwiftData or a file, never in scene/app storage.
The wrapper test: would two open windows legitimately disagree about this value? Yes β per-window UI
state β @SceneStorage. No, it's one app-wide truth β @AppStorage. Full reasoning + the βββ
rewrites:
references/wrapper-choice.md.
Grounded β
(real call site, not a placeholder). Per-window UI state restores per scene when its key is
bound to @SceneStorage. The canonical shape β consensus from swiftui-ctx lookup SceneStorage
(introduced_macos 11.0; top shapes ("selectedTab"), ("viewMode"), ("selectedSettingsSection"),
each ~13%) β and a verified shipping example:
// β
TableProApp/TablePro β @SceneStorage restores per-window search text on relaunch
@SceneStorage("tableList.searchText") private var searchText = ""
- Example (
swiftui-ctx file ex_596e50becb --smart, real GitHub permalink):https://github.com/TableProApp/TablePro/blob/e3afc6457cd819eca5226c3874a9b4d7ad318a67/TableProMobile/TableProMobile/Views/TableListView.swift#L12 - Spec (
doc:, via Sosumi):https://sosumi.ai/documentation/swiftui/scenestorage(macOS 11.0+, accessed 2026-06-07).
Re-fetch the live consensus + permalink in step VERIFY/FIX rather than trusting this snapshot.
Defect index (sr-01 β¦ sr-11)
id Β· tell Β· severity Β· fix Β· open reference. Severities: hard-fail (build break / never-correct),
warning (compiles but wrong/amnesiac), advisory (judgment / perf). All fixes are flag-only: every
correction here turns on developer intent (is this value app-wide or per-window?), so none auto-applies.
| id | One-line tell | Sev | Fix | Reference |
|---|---|---|---|---|
| sr-01 | .restorationIdentifier( / @StateRestoration / UIStateRestoring / restorationClass (UIKit/invented on SwiftUI Mac) |
hard-fail | flag | restoration-gating.md |
| sr-02 | @SceneStorage holding an app-wide preference (one truth for all windows) |
warning | flag | wrapper-choice.md |
| sr-03 | @AppStorage holding per-window UI state (tab/sidebar/detail/scroll selection) β leaks across windows |
warning | flag | wrapper-choice.md |
| sr-04 | hand-rolled UserDefaults.standard.set/object(forKey:) for a simple pref @AppStorage fits |
warning | flag | wrapper-choice.md |
| sr-05 | TabView/NavigationSplitView/List selection bound to plain @State (no @SceneStorage) β relaunch amnesia |
warning | flag | navigation-restoration.md |
| sr-06 | NavigationPath never persisted β no NavigationPath(codable:) / .codable round-trip into @SceneStorage |
advisory | flag | navigation-restoration.md |
| sr-07 | custom type in @AppStorage/@SceneStorage without RawRepresentable/Codable conformance |
warning | flag | wrapper-choice.md |
| sr-08 | oversized Data/array/image stored in @SceneStorage/@AppStorage (size-limited) β SwiftData/file |
warning | flag | wrapper-choice.md |
| sr-09 | declared CFBundleURLSchemes / custom URL scheme but no onOpenURL/onContinueUserActivity handler (dead deep link) |
warning | flag | deep-linking.md |
| sr-10 | restorationBehavior(_:) used/ungated under a deployment target < macOS 15 |
warning | flag | restoration-gating.md |
| sr-11 | focusedSceneValue(_:_:) β key-path form macOS 12 / object form macOS 14; verify floor + gate |
advisory | flag | restoration-gating.md |
One claim is FLOOR-SENSITIVE β never assert without VERIFY: focusedSceneValue has two floors (the
key-path overload is macOS 12, the object overload macOS 14, per the brief); confirm which overload the
code uses before quoting a floor (source: verify against Xcode 26 SDK until confirmed via swiftui-ctx +
Sosumi). restorationBehavior is macOS 15.
The real API, at a glance
Real (and their macOS floors β confirmed via swiftui-ctx introduced_macos + Sosumi): @AppStorage
(macOS 11), @SceneStorage (macOS 11), NavigationPath + NavigationPath(codable:) / .codable
(macOS 13), onOpenURL(perform:) (macOS 11), onContinueUserActivity(_:perform:) (macOS 11),
handlesExternalEvents(matching:) (macOS 11 β Mac, iOS, visionOS; no tvOS/watchOS), restorationBehavior(_:)
(macOS 15), focusedSceneValue(_:_:) (key-path macOS 12 / object macOS 14). Floor values live in
<swiftui-plugin-root>/references/_shared/floors-master.md β read, never restate.
Hallucinated / wrong-framework (never on SwiftUI macOS): .restorationIdentifier(...),
@StateRestoration, UIStateRestoring, restorationClass, encodeRestorableState/decodeRestorableState
(all UIKit UIViewController/NSResponder restoration, not SwiftUI). @FocusedDocument is not a real
symbol β use a custom FocusedValues key (<swiftui-plugin-root>/references/_shared/hallucination-blacklist.md).
The full βββ
rewrites: references/wrapper-choice.md + references/restoration-gating.md.
The 8-step audit workflow (execute verbatim)
- ORIENT.
tree/findthe SwiftUI sources. Read the deployment target (project.pbxprojMACOSX_DEPLOYMENT_TARGET, orPackage.swiftplatforms:). It is load-bearing: sr-10 fires only when the floor is below macOS 15, sr-11 only below the relevantfocusedSceneValuefloor. Also readInfo.plist/project.pbxprojfor any declaredCFBundleURLSchemes(needed for sr-09). Record the target and any declared scheme. - LOCATE. Run the shared hybrid lint runner:
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-state-restoration --dir <sources> --json /tmp/sr.json --sarif /tmp/sr.sarif. It runs this skill's tier-1 grep tells (lint/grep-tells.tsv) + the tier-2 structural ast-grep rule (lint/ast-grep/*.ymlβ therestorationBehavior-not-inside-a-#available(macOS 15)-gate rule grep can't express), 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. 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. The wrapper-choice defects (sr-02/03) are invisible to grep: only reading the value's meaning (app-wide vs per-window), its scope, and how many windows the app opens reveals the defect. Build a per-file inventory: each stored property + wrapper + value semantics + the wrapper test verdict.
- DETECT. Apply the index. Assign each candidate a confidence; report a finding only at 100%
certainty (e.g. a UIKit restoration symbol on SwiftUI, a custom type with no
RawRepresentable, arestorationBehaviorunder a <15 floor). - VERIFY. For anything β€ ~70% confidence (a floor you can't place β especially
focusedSceneValue's two overloads β a symbol you're unsure exists, a "does this persist" behavior claim), run both evidence sources. (a) Practice βbash <swiftui-plugin-root>/scripts/swiftui-ctx lookup <api> --json(andswiftui-ctx deprecated <api>for a currency rule): read itsconsensus(the canonical shape),deprecated+replacement,recommendedpermalink,introduced_macos, andco_occurs_with; alookupexit 3 (not-found, with a did-you-meansuggestion) corroborates a hallucination β no shipping Mac app uses the symbol. (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; this domain's parenthesized-symbol JSON 404s β Sosumi only). 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. Write the run's
_index.md. - FIX. All findings here are
fix_mode: flag-onlyβ leave eachopenwith the β in## Correct. The β is not a hand-written snippet: it is the swiftui-ctx consensus shape put in## Correct, backed by a real macOS example fetched withbash <swiftui-plugin-root>/scripts/swiftui-ctx file <recommended.id> --smartwhose GitHub permalink (plus the Sosumidoc:) goes in## Sourceas the canonical example. The fix-safety protocol (<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md) still governs: clean-tree gate, findings-first, never weaken a check. Because the wrapper swap turns on intent, never auto-apply it. - DOUBLE-CHECK. Re-read each flagged property to confirm the β
wrapper matches the value's semantics
(the wrapper test still holds). Re-confirm every citation still resolves and still says the floor you
quoted. If a suggested β
introduces a new tell (e.g. moving a blob to
@SceneStoragenow needsCodable), loop that file back to DETECT.
Confidence gating (load-bearing)
Report a finding only at 100% certainty. The wrapper-confusion defects (sr-02/03/04) are semantic β
treat them as β€70% until READ confirms the value's app-wide-vs-per-window meaning; route them through VERIFY
for the floor and through the wrapper test for the verdict. Never emit a speculative finding. Nothing here
auto-fixes β every finding 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/state-restoration/<context>/NN-slug.md(one finding per file, zero-padded, ordered). Per-run index:swiftui-audits/state-restoration/_index.md. domain: state-restoration.fix_modeisflag-onlyfor every defect. An optional descriptive additive fieldstorage_kind(app|scene|userdefaults|navigation|url) may record which store the finding concerns; emitcross_refon shared-seam findings (sr-08 βaudit-swiftui-swiftdata; sr-05/06 βaudit-swiftui-navigation-toolbars; sr-10 βaudit-swiftui-scenes-windows; sr-11 βaudit-swiftui-menus-commands).availabilityreads fromfloors-master.md.sourceis an Apple URL + access date (fetched via Sosumi) orverify against Xcode 26 SDK.
Starter <context> folders (file here whenβ¦):
<context> |
File a finding here when⦠|
|---|---|
hallucinated-api/ |
a UIKit/invented restoration symbol appears on SwiftUI (sr-01) |
wrapper-confusion/ |
the wrong wrapper holds the value β scene/app swapped, or hand-rolled UserDefaults (sr-02, sr-03, sr-04) |
relaunch-amnesia/ |
selection or navigation is not restored β plain @State selection, unpersisted NavigationPath (sr-05, sr-06) |
storage-correctness/ |
a non-conforming custom type or an oversized blob sits in key-value storage (sr-07, sr-08) |
deep-linking/ |
a declared URL scheme/activity has no handler (sr-09) |
restoration-gating/ |
restorationBehavior/focusedSceneValue is ungated under its floor (sr-10, sr-11) |
New-folder rule: if a finding does not fit any existing context folder, create a new one under
swiftui-audits/state-restoration/ 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/wrapper-choice.md |
the @AppStorageβ@SceneStorageβUserDefaults decision, the wrapper test, custom-type conformance, oversized-blob offload (sr-02/03/04/07/08) |
references/navigation-restoration.md |
restoring selection + NavigationPath across relaunch β @SceneStorage selection, NavigationPath(codable:) round-trip (sr-05/06) |
references/deep-linking.md |
URL scheme + onOpenURL/onContinueUserActivity/handlesExternalEvents wiring (sr-09) |
references/restoration-gating.md |
the hallucinated/UIKit symbols, restorationBehavior (macOS 15) + focusedSceneValue two-floor gating (sr-01/10/11) |
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 + the one tier-2 structural rule); 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 (incl. @FocusedDocument) |
<swiftui-plugin-root>/references/_shared/macos-arm-gating.md |
the macOS-arm gating rule + wrong-arm failure (sr-10/11) |
<swiftui-plugin-root>/references/_shared/finding-schema.md |
the unified finding schema + frontmatter keys |
<swiftui-plugin-root>/references/_shared/fix-safety-protocol.md |
the 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 |
Detection accelerator
bash <swiftui-plugin-root>/scripts/swiftui-lint.sh --skill audit-swiftui-state-restoration --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, sr-01/02/03/04/05/06/07/08/09/10/11) +
tier-2 ast-grep (lint/ast-grep/sr-10-restorationbehavior-ungated.yml β restorationBehavior NOT inside
an #available(macOS 15, *) gate, a gate-scope rule 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, exits
2 on any hard-fail (sr-01) for a CI gate, 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). It only LOCATES β
always READ each hit in full before reporting (step 3). The thin scripts/sr-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 State Restoration 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-state-restoration ~/.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