All skills
avdlee avatar

/swiftui-expert-skill

@1e522cf

Use when writing, reviewing, or refactoring SwiftUI code for iOS or macOS, including state and `@Observable` data flow, view composition, resizable layouts, safe areas, display scale, performance, lists, environment, localization, animation, Liquid Glass, and API migration. Also use for iPhone Duo, foldable, or large-display layouts (`NavigationSplitView` on large displays, two-column reflow, foldable grids, `ArrangementView`, `ReservedRegion`), hinge effects, vertical bars, `@State` initialization or synthesized-property diagnostics, `@ContentBuilder` ambiguity, `reorderable` drag/drop, custom `AsyncImage` `URLSession`, swipe actions outside List, item-bound `alert`/`confirmationDialog`, `ToolbarOverflowMenu`, `AnimatableValues`, Document APIs (`Document`/`DocumentReader`), and Instruments `.trace` capture or analysis.

Use this Skill: https://skilld.dev/gh/avdlee/swiftui-agent-skill/swiftui-expert-skill

This session only. Nothing lands on disk.

referencestrace-analysis.md

≈3.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Instruments Trace Analysis

Use this reference whenever the user references an Xcode Instruments .trace file. A target SwiftUI source file is optional — if provided, you can cite specific lines; without one, the trace still surfaces view names, hot symbols, and high-severity events that tell the user where to look.

The bundled parser reads five lanes for SwiftUI responsiveness (Time Profiler, Hangs, Animation Hitches, SwiftUI updates, and the SwiftUI cause graph) and exposes three discovery modes (--list-logs, --list-signposts, --fanin-for) plus a --window flag so the agent can focus analysis on a precise slice of the trace.

When to invoke

Any of these signals:

  • Message contains a path ending in .trace.
  • User mentions "hangs", "hitches", "jank", "slow view", or performance issues alongside an Instruments recording.
  • User asks to focus analysis "after / before / between / during" a log message or signpost.

Triggering does not require a SwiftUI source file. If one is present you'll ground recommendations in specific lines; if not, base them on the view names and symbols the trace reveals.

The three CLI modes

The scripts live alongside this skill at scripts/ and need only the Python 3 stdlib + xctrace (ships with Xcode at /usr/bin/xctrace).

1. Full analysis (default)

python3 "${SKILL_DIR}/scripts/analyze_trace.py" \
  --trace "/path/to/file.trace" \
  --top 10 --top-hitches 5 \
  [--window START_MS:END_MS] \
  --json-only
  • --json-only gives you structured data; omit for JSON + markdown summary; --markdown-only is for pasting a digest into the chat.
  • --output <path> writes <path>.json and <path>.md instead of stdout.
  • --window START_MS:END_MS (optional) restricts every lane and every correlation to that time slice.
  • --run N selects a specific run when the trace contains more than one recording session. Single-run traces don't need it; multi-run traces require it and will error with the available run numbers if omitted. Use --list-runs to dump per-run metadata (template, duration, start/end dates, schemas) before analyzing.

2. --list-logs — find os_log timestamps

python3 "${SKILL_DIR}/scripts/analyze_trace.py" --trace <path> --list-logs \
  [--log-subsystem com.myapp.net] \
  [--log-category "Network"] \
  [--log-type Fault] \
  [--log-message-contains "loaded feed"] \
  [--log-limit 10] \
  [--window START_MS:END_MS]

Returns JSON { "logs": [...], "count": N } where each log entry includes time_ms, type, subsystem, category, process, and the formatted message (with args substituted) + raw format_string. All filters are AND-combined; --log-message-contains is case-insensitive substring match.

3. --list-signposts — find signpost intervals

python3 "${SKILL_DIR}/scripts/analyze_trace.py" --trace <path> --list-signposts \
  [--signpost-name-contains "ImageDecode"] \
  [--signpost-subsystem com.myapp.feed] \
  [--signpost-category "Rendering"] \
  [--window START_MS:END_MS]

Returns JSON { "intervals": [...], "events": [...] }. Intervals are paired begin/end signposts with start_ms, end_ms, duration_ms, name, subsystem, category, process, signpost_id. Single-point events (and any unpaired begins) go into events. All filters are AND-combined; --signpost-name-contains is case-insensitive substring match.

4. --fanin-for — who keeps invalidating this view?

python3 "${SKILL_DIR}/scripts/analyze_trace.py" --trace <path> \
  --fanin-for "TextStyleModifier" \
  [--window START_MS:END_MS] \
  [--top 10]

Returns JSON { "matches": [...] }. Each match names a destination node whose fmt string contains the substring (case-insensitive) and lists its top incoming source nodes ranked by edge count. Use this after the swiftui lane names an expensive view and you want to know why it keeps being invalidated. For the example above, the top source is closure #1 in UserDefaultObserver.Target.GraphAttribute.send() — the canonical signature of an @AppStorage / UserDefaults feedback storm.

Composition pattern — scoping to a slice

When the user says something like "focus on X", "between A and B", or "during signpost Y", compose the three modes:

  1. Discover — call --list-logs or --list-signposts with filters that match the user's description. Pick the right entries.
  2. Build the window — take time_ms (logs) or start_ms/end_ms (intervals) and form --window START:END.
  3. Analyse — call the default mode with --window.

Examples:

  • "Focus on the section after the log saying 'loaded feed'." → --list-logs --log-message-contains "loaded feed", take the entry's time_ms, set window = [that_ms, end_of_trace_ms] (or use the trace duration_s × 1000).
  • "Between the 'begin-sync' log and the 'done-sync' log." → Two --list-logs calls (or one with a broader filter), pick the two timestamps, set window = [first, second].
  • "During the signpost 'ImageDecode'." → --list-signposts --signpost-name-contains "ImageDecode", pick the interval, set window = [start_ms, end_ms].

JSON shape

{
  "trace": "...",
  "xctrace_version": "26.4 (...)",
  "template": "SwiftUI",
  "duration_s": 14.83,
  "schemas_available": [...],
  "lanes": [
    { "lane": "time-profiler", "available": true, "schema_used": "time-profile",
      "metrics": { "total_samples": N, "total_weight_ms": ms, "processes": [...] },
      "top_offenders": [ { "symbol", "weight_ms", "percent", "samples", "thread" } ] },
    { "lane": "hangs", "available": true, "schema_used": "potential-hangs",
      "metrics": { "count", "total_duration_ms", "worst_duration_ms",
                   "severity_buckets": {"lt_250ms","250ms_1s","gt_1s"} },
      "top_offenders": [ { "start_ms", "duration_ms", "hang_type", "thread" } ] },
    { "lane": "hitches", "available": true, "schema_used": "hitches",
      "metrics": { "count", "total_hitch_ms", "worst_hitch_ms",
                   "narrative_breakdown": {...}, "system_hitches", "app_hitches" },
      "top_offenders": [ { "start_ms", "hitch_duration_ms", "narrative", "is_system" } ] },
    { "lane": "swiftui", "available": true, "schemas_used": [...],
      "metrics": { "total_events", "unique_views", "total_duration_ms",
                   "severity_breakdown": {"Very Low":N,"Moderate":N,"High":N},
                   "update_type_breakdown": {"View Body Updates":N, ...} },
      "top_offenders": [ { "view", "total_ms", "count", "avg_ms" } ],
      "high_severity_events": [ { "view", "severity", "duration_ms", "category",
                                   "update_type", "description" } ] },
    { "lane": "swiftui-causes", "available": true, "schema_used": "swiftui-causes",
      "metrics": { "total_edges", "unique_sources", "unique_destinations",
                   "top_labels": {...} },
      "top_sources":      [ { "source", "edges", "top_destinations": [...] } ],
      "top_destinations": [ { "destination", "edges", "top_sources":      [...] } ] }
  ],
  "correlations": [
    {
      "trigger": { "lane": "hangs"|"hitches", "start_ms", "end_ms", "duration_ms",
                   "hang_type"|"frame_duration_ms" },
      "time_profiler_main_thread": {
        "samples_in_window": N, "samples_on_main": M,
        "main_running_coverage_pct": 0–100,
        "hot_symbols": [ { "symbol", "samples", "weight_ms", "percent_of_main" } ]
      },
      "swiftui_overlapping_updates": [ { "view", "duration_ms", "start_ms" } ]
    }
  ]
}

Interpretation guide

main_running_coverage_pct is the key diagnostic

Time Profiler samples the main thread every ~1ms. For a correlation window of N ms, you'd expect ~N main-thread running samples if main were fully CPU-bound. Coverage is the ratio of observed main-thread samples to that expectation.

  • < 25% coverage → main thread was blocked (I/O, lock, sync XPC, Task.sleep, waiting on an actor-isolated call). The hot_symbols you do see are the moments main was executing — look there for the code that initiates the blocking work, not the work itself. Common fix: move to a background executor / nonisolated / Task.detached.
  • ≥ 75% coverage → main was CPU-bound the whole time. hot_symbols point directly at the expensive work. Common fixes: hoist computation out of view bodies, cache derived values, avoid per-frame allocation, debounce onChange.
  • 25–75% → mix. Usually computation plus intermittent I/O; show both hot symbols and note that main was partially blocked.

High-severity SwiftUI events → reference routing

When swiftui.high_severity_events[].description is one of:

description Likely cause Route to
onChange Expensive .onChange body references/performance-patterns.md, references/state-management.md
Gesture Heavy gesture handler references/performance-patterns.md
Action Callback Button/tap handler work references/performance-patterns.md
Update View body recomputation references/view-structure.md, references/performance-patterns.md
Creation View init cost references/view-structure.md
Layout GeometryReader churn references/layout-best-practices.md

Mapping trace findings to source code

If the user gave you a specific file, use it to confirm/cite. If they didn't, the trace itself tells you which views and symbols to look up.

  1. From swiftui.top_offenders and high_severity_events, use the view string as your search key. If a target file is open, grep it; if not, recommend the user grep their project for that type or the module name. A partial match (prefix / generic stripping) means it's probably a subview.
  2. From correlations[].time_profiler_main_thread.hot_symbols, treat symbols starting with the user's module name (or in Swift free-function form) as candidates. System frames (swift_, dyld, objc_, CA*, CF*, NS*, __open, pthread*) identify what the code was doing but the user-code caller one frame up is typically what to fix — say so and, if you can, suggest searching the project for callers of the equivalent Swift API (e.g. __open → FileHandle / Data(contentsOf:) / JSONDecoder.decode(from: Data) sites).
  3. From hitches[].narrative, Apple pre-attributes each hitch. The string "Potentially expensive app update(s)" means SwiftUI blamed the app (so user code is in scope); absence of narrative usually means it was a system hitch or below the threshold.
  4. Correlating hitches with SwiftUI updates: the swiftui_overlapping_updates list on each hitch names the views that were actively rendering when the frame dropped. Prioritise those.

Cause graph: finding why updates keep happening

The swiftui lane tells you what is expensive; the swiftui-causes lane tells you why it keeps being triggered. Each edge is "source node propagated to destination node" in SwiftUI's attribute graph.

Signatures to watch for in top_sources:

  • closure #1 in UserDefaultObserver.Target.GraphAttribute.send() — an @AppStorage / UserDefaults write is fanning out to every reader. If the destination list contains multiple @AppStorage <Type>.<prop> entries with thousands of edges each, you have a feedback storm. Fix by reading each key once at a high level and passing values down, or wrapping settings in a single @Observable so only genuine readers invalidate. Route to references/state-management.md and references/performance-patterns.md.
  • EnvironmentWriter: … with thousands of edges — a modifier (often .hoverEffect, custom environment keys) is applied too widely and being re-installed during every layout pass. Route to references/view-structure.md.
  • View Creation / Reuse as the #1 source — the hierarchy is replacing children rather than mutating in place. Look for ID instability (missing/unstable .id(…) on ForEach, type-erased AnyView wrappers, conditional structure swaps). Route to references/list-patterns.md and references/view-structure.md.

When a specific view in swiftui.high_severity_events keeps showing up, run --fanin-for "<view name>" to see the ranked list of sources invalidating it.

Picking targets from a full-trace analysis

Prioritise from most actionable to least:

  1. Any hangs with main_running_coverage_pct < 25% — these are blocking-I/O smells; nearly always fixable by moving work off-main.
  2. Any hangs with main_running_coverage_pct ≥ 75% — CPU-bound main-thread work; fix the top hot_symbols.
  3. swiftui-causes.top_sources with > ~1k edges — structural invalidation bugs (feedback storms, over-applied modifiers). These are often cheaper to fix than per-view optimisations and collapse many downstream high-severity updates at once.
  4. hitches with narrative == "Potentially expensive app update(s)" and overlapping swiftui_overlapping_updates — specific views to restructure.
  5. swiftui.high_severity_events — onChange, Gesture, or Action Callback with duration_ms > ~16 are frame-dropping handlers. For any that keep firing, run --fanin-for to find the source.
  6. swiftui.top_offenders — heaviest views by total body time, even without triggering hitches; candidates for view extraction or memoisation (equatable, @ViewBuilder extraction).

Recommended output format for the user

After running the parser, structure your response as:

  1. One-line summary — "Found N hangs, worst Wms; K hitches; J high-severity SwiftUI updates."
  2. Root-cause findings — per prioritised target (see above), one paragraph with the trace evidence (coverage %, hot symbol, overlapping view) and a citation from references/… for the fix pattern.
  3. Plan — numbered, file-specific edits. Cite line numbers in the user's Swift file when you know them. Don't edit the file unless the user asked for edits.

Source: SKILL.md on GitHub

No alerts2d5 checks · Risk SAFE
  • Gen Agent Trust Hub2d

    The skill is a professional-grade assistant for SwiftUI development and performance profiling. It includes Python scripts to interface with the Xcode xctrace CLI for recording and analyzing Instruments traces. All identified code and instructions are consistent with its stated purpose and follow security best practices.

  • Socket2d

    No alerts

  • Snyk2d

    Risk: LOW · No issues

  • Runlayer6mo

    19 files scanned · No issues

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 1e522cf. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub yesterday.

Activeupdated 3 days ago
  • Performance
  • swiftui
  • ios
  • macos
  • instruments
  • state-management
  • view-composition
  • accessibility
  • animations

README badge

README badge for avdlee/swiftui-agent-skill/swiftui-expert-skill

Provides guidance for writing, reviewing, and refactoring SwiftUI code for iOS and macOS, including state management, view composition, performance optimization, and Instruments trace recording and analysis. Covers deprecated API detection, animation patterns, accessibility, and Liquid Glass adoption.

Generated from the current SKILL.md.

Does this skill work with both iOS and macOS?
Yes. The skill covers SwiftUI for both iOS and macOS, with dedicated reference sections for macOS-specific patterns like scenes, window styling, and views (HSplitView, Table, PasteButton).
Can this skill help me record and analyze Instruments traces?
Yes. The skill includes workflows to record traces via `record_trace.py` (with template selection for real devices vs simulators) and analyze them via `analyze_trace.py` to identify hangs, hitches, CPU hotspots, and excessive view updates.
Does this skill enforce a specific architecture pattern?
No. It focuses on correctness and performance without mandating MVVM, VIPER, or other architectural styles, though it encourages separating business logic from views for testability.
What does the skill do about deprecated APIs?
It consults `references/latest-apis.md` at the start of every task to identify and replace deprecated APIs with modern equivalents across iOS 15+ through iOS 26+, and gates version-specific APIs with `#available`.
Does this skill handle Liquid Glass effects?
Yes, but only when explicitly requested by the user. It includes guidance in `references/liquid-glass.md` for iOS 26+ Liquid Glass adoption with sensible fallbacks for earlier versions.

Generated from the current SKILL.md. These answers refresh after source changes.