feat(skills): Import curated Apple workflow skills

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.
This commit is contained in:
sickn33
2026-03-25 11:53:08 +01:00
parent 0d425400d6
commit d2be634870
110 changed files with 7118 additions and 35 deletions

View File

@@ -0,0 +1,75 @@
---
name: app-store-changelog
description: Generate user-facing App Store release notes from git history since the last tag.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# App Store Changelog
## Overview
Generate a comprehensive, user-facing changelog from git history since the last tag, then translate commits into clear App Store release notes.
## When to Use
- When the user asks for App Store "What's New" text or release notes from git history.
- When you need to turn raw commits into concise, user-facing release bullets.
## Workflow
### 1) Collect changes
- Run `scripts/collect_release_changes.sh` from the repo root to gather commits and touched files.
- If needed, pass a specific tag or ref: `scripts/collect_release_changes.sh v1.2.3 HEAD`.
- If no tags exist, the script falls back to full history.
### 2) Triage for user impact
- Scan commits and files to identify user-visible changes.
- Group changes by theme (New, Improved, Fixed) and deduplicate overlaps.
- Drop internal-only work (build scripts, refactors, dependency bumps, CI).
### 3) Draft App Store notes
- Write short, benefit-focused bullets for each user-facing change.
- Use clear verbs and plain language; avoid internal jargon.
- Prefer 5 to 10 bullets unless the user requests a different length.
### 4) Validate
- Ensure every bullet maps back to a real change in the range.
- Check for duplicates and overly technical wording.
- Ask for clarification if any change is ambiguous or possibly internal-only.
## Commit-to-Bullet Examples
The following shows how raw commits are translated into App Store bullets:
| Raw commit message | App Store bullet |
|---|---|
| `fix(auth): resolve token refresh race condition on iOS 17` | • Fixed a login issue that could leave some users unexpectedly signed out. |
| `feat(search): add voice input to search bar` | • Search your library hands-free with the new voice input option. |
| `perf(timeline): lazy-load images to reduce scroll jank` | • Scrolling through your timeline is now smoother and faster. |
Internal-only commits that are **dropped** (no user impact):
- `chore: upgrade fastlane to 2.219`
- `refactor(network): extract URLSession wrapper into module`
- `ci: add nightly build job`
## Example Output
```
What's New in Version 3.4
• Search your library hands-free with the new voice input option.
• Scrolling through your timeline is now smoother and faster.
• Fixed a login issue that could leave some users unexpectedly signed out.
• Added dark-mode support to the settings screen.
• Improved load times when opening large photo albums.
```
## Output Format
- Title (optional): "What's New" or product name + version.
- Bullet list only; one sentence per bullet.
- Stick to storefront limits if the user provides one.
## Resources
- `scripts/collect_release_changes.sh`: Collect commits and touched files since last tag.
- `references/release-notes-guidelines.md`: Language, filtering, and QA rules for App Store notes.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "App Store Changelog"
short_description: "Generate App Store release notes"
default_prompt: "Use $app-store-changelog to draft App Store release notes from the changes since the last tag."

View File

@@ -0,0 +1,34 @@
# App Store Release Notes Guidelines
## Goals
- Produce user-facing release notes that describe visible changes since the last tag.
- Include all user-impacting changes; omit purely internal or refactor-only work.
- Keep language plain, short, and benefit-focused.
## Output Shape
- Prefer 5 to 10 bullets total for most releases.
- Group by theme if needed: New, Improved, Fixed.
- Each bullet should be one sentence and start with a verb.
- Avoid internal codenames, ticket IDs, or file paths.
## Filtering Rules
- Include: new features, UI changes, behavior changes, bug fixes users would notice, performance improvements with visible impact.
- Exclude: refactors, dependency bumps, CI changes, developer tooling, internal logging, analytics changes unless they affect user privacy or behavior.
- If a change is ambiguous, ask for clarification or describe it as a small improvement only if it is user-visible.
## Language Guidance
- Translate technical terms into user-facing descriptions.
- Avoid versions of "API", "refactor", "nil", "crash log", or "dependency".
- Prefer "Improved", "Added", "Fixed", "Updated" or action verbs like "Search", "Upload", "Sync".
- Keep tense present or past: "Added", "Improved", "Fixed".
## Examples
- "Added account switching from the profile menu."
- "Improved timeline loading speed on slow connections."
- "Fixed media attachments not opening in full screen."
## QA Checklist
- Every bullet ties to a real change in the range.
- No duplicate bullets that describe the same change.
- No internal jargon or file paths.
- Final list fits App Store text limits for the target storefront if provided.

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -euo pipefail
since_ref="${1:-}"
until_ref="${2:-HEAD}"
if [[ -z "${since_ref}" ]]; then
if git describe --tags --abbrev=0 >/dev/null 2>&1; then
since_ref="$(git describe --tags --abbrev=0)"
fi
fi
range=""
if [[ -n "${since_ref}" ]]; then
range="${since_ref}..${until_ref}"
else
range="${until_ref}"
fi
repo_root="$(git rev-parse --show-toplevel)"
printf "Repo: %s\n" "${repo_root}"
if [[ -n "${since_ref}" ]]; then
printf "Range: %s..%s\n" "${since_ref}" "${until_ref}"
else
printf "Range: start..%s (no tags found)\n" "${until_ref}"
fi
printf "\n== Commits ==\n"
git log --reverse --date=short --pretty=format:'%h|%ad|%s' ${range}
printf "\n\n== Files Touched ==\n"
git log --reverse --name-only --pretty=format:'--- %h %s' ${range} | sed '/^$/d'

76
skills/github/SKILL.md Normal file
View File

@@ -0,0 +1,76 @@
---
name: github
description: "Use the `gh` CLI for issues, pull requests, Actions runs, and GitHub API queries."
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# GitHub Skill
Use the `gh` CLI to interact with GitHub. Always specify `--repo owner/repo` when not in a git directory, or use URLs directly.
## When to Use
- When the user asks about GitHub issues, pull requests, workflow runs, or CI failures.
- When you need `gh issue`, `gh pr`, `gh run`, or `gh api` from the command line.
## Pull Requests
Check CI status on a PR:
```bash
gh pr checks 55 --repo owner/repo
```
List recent workflow runs:
```bash
gh run list --repo owner/repo --limit 10
```
View a run and see which steps failed:
```bash
gh run view <run-id> --repo owner/repo
```
View logs for failed steps only:
```bash
gh run view <run-id> --repo owner/repo --log-failed
```
### Debugging a CI Failure
Follow this sequence to investigate a failing CI run:
1. **Check PR status** — identify which checks are failing:
```bash
gh pr checks 55 --repo owner/repo
```
2. **List recent runs** — find the relevant run ID:
```bash
gh run list --repo owner/repo --limit 10
```
3. **View the failed run** — see which jobs and steps failed:
```bash
gh run view <run-id> --repo owner/repo
```
4. **Fetch failure logs** — get the detailed output for failed steps:
```bash
gh run view <run-id> --repo owner/repo --log-failed
```
## API for Advanced Queries
The `gh api` command is useful for accessing data not available through other subcommands.
Get PR with specific fields:
```bash
gh api repos/owner/repo/pulls/55 --jq '.title, .state, .user.login'
```
## JSON Output
Most commands support `--json` for structured output. You can use `--jq` to filter:
```bash
gh issue list --repo owner/repo --json number,title --jq '.[] | "\(.number): \(.title)"'
```

View File

@@ -0,0 +1,4 @@
interface:
display_name: "GitHub"
short_description: "Use gh for GitHub workflows"
default_prompt: "Use $github to inspect this repository's pull requests, issues, runs, or API data."

View File

@@ -0,0 +1,59 @@
---
name: ios-debugger-agent
description: Debug the current iOS project on a booted simulator with XcodeBuildMCP.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# iOS Debugger Agent
## Overview
Use XcodeBuildMCP to build and run the current project scheme on a booted iOS simulator, interact with the UI, and capture logs. Prefer the MCP tools for simulator control, logs, and view inspection.
## When to Use
- When the user asks to run, debug, or inspect an iOS app on a simulator.
- When you need simulator UI interaction, screenshots, or runtime logs via XcodeBuildMCP.
## Core Workflow
Follow this sequence unless the user asks for a narrower action.
### 1) Discover the booted simulator
- Call `mcp__XcodeBuildMCP__list_sims` and select the simulator with state `Booted`.
- If none are booted, ask the user to boot one (do not boot automatically unless asked).
### 2) Set session defaults
- Call `mcp__XcodeBuildMCP__session-set-defaults` with:
- `projectPath` or `workspacePath` (whichever the repo uses)
- `scheme` for the current app
- `simulatorId` from the booted device
- Optional: `configuration: "Debug"`, `useLatestOS: true`
### 3) Build + run (when requested)
- Call `mcp__XcodeBuildMCP__build_run_sim`.
- **If the build fails**, check the error output and retry (optionally with `preferXcodebuild: true`) or escalate to the user before attempting any UI interaction.
- **After a successful build**, verify the app launched by calling `mcp__XcodeBuildMCP__describe_ui` or `mcp__XcodeBuildMCP__screenshot` before proceeding to UI interaction.
- If the app is already built and only launch is requested, use `mcp__XcodeBuildMCP__launch_app_sim`.
- If bundle id is unknown:
1) `mcp__XcodeBuildMCP__get_sim_app_path`
2) `mcp__XcodeBuildMCP__get_app_bundle_id`
## UI Interaction & Debugging
Use these when asked to inspect or interact with the running app.
- **Describe UI**: `mcp__XcodeBuildMCP__describe_ui` before tapping or swiping.
- **Tap**: `mcp__XcodeBuildMCP__tap` (prefer `id` or `label`; use coordinates only if needed).
- **Type**: `mcp__XcodeBuildMCP__type_text` after focusing a field.
- **Gestures**: `mcp__XcodeBuildMCP__gesture` for common scrolls and edge swipes.
- **Screenshot**: `mcp__XcodeBuildMCP__screenshot` for visual confirmation.
## Logs & Console Output
- Start logs: `mcp__XcodeBuildMCP__start_sim_log_cap` with the app bundle id.
- Stop logs: `mcp__XcodeBuildMCP__stop_sim_log_cap` and summarize important lines.
- For console output, set `captureConsole: true` and relaunch if required.
## Troubleshooting
- If build fails, ask whether to retry with `preferXcodebuild: true`.
- If the wrong app launches, confirm the scheme and bundle id.
- If UI elements are not hittable, re-run `describe_ui` after layout changes.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "iOS Debugger Agent"
short_description: "Debug iOS apps on Simulator"
default_prompt: "Use $ios-debugger-agent to build, launch, and inspect the current iOS app on the booted simulator."

View File

@@ -0,0 +1,109 @@
---
name: macos-menubar-tuist-app
description: Build, refactor, or review SwiftUI macOS menubar apps that use Tuist.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# macos-menubar-tuist-app
Build and maintain macOS menubar apps with a Tuist-first workflow and stable launch scripts. Preserve strict architecture boundaries so networking, state, and UI remain testable and predictable.
## When to Use
- When working on LSUIElement menubar utilities built with Tuist and SwiftUI.
- When you need Tuist manifests, launch scripts, or architecture guidance for a menubar app.
## Core Rules
- Keep the app menubar-only unless explicitly told otherwise. Use `LSUIElement = true` by default.
- Keep transport and decoding logic outside views. Do not call networking from SwiftUI view bodies.
- Keep state transitions in a store layer (`@Observable` or equivalent), not in row/view presentation code.
- Keep model decoding resilient to API drift: optional fields, safe fallbacks, and defensive parsing.
- Treat Tuist manifests as the source of truth. Do not rely on hand-edited generated Xcode artifacts.
- Prefer script-based launch for local iteration when `tuist run` is unreliable for macOS target/device resolution.
- Prefer `tuist xcodebuild build` over raw `xcodebuild` in local run scripts when building generated projects.
## Expected File Shape
Use this placement by default:
- `Project.swift`: app target, settings, resources, `Info.plist` keys
- `Sources/*Model*.swift`: API/domain models and decoding
- `Sources/*Client*.swift`: requests, response mapping, transport concerns
- `Sources/*Store*.swift`: observable state, refresh policy, filtering, caching
- `Sources/*Menu*View*.swift`: menu composition and top-level UI state
- `Sources/*Row*View*.swift`: row rendering and lightweight interactions
- `run-menubar.sh`: canonical local restart/build/launch path
- `stop-menubar.sh`: explicit stop helper when needed
## Workflow
1. Confirm Tuist ownership
- Verify `Tuist.swift` and `Project.swift` (or workspace manifests) exist.
- Read existing run scripts before changing launch behavior.
2. Probe backend behavior before coding assumptions
- Use `curl` to verify endpoint shape, auth requirements, and pagination behavior.
- If endpoint ignores `limit/page`, implement full-list handling with local trimming in the store.
3. Implement layers from bottom to top
- Define/adjust models first.
- Add or update client request/decoding logic.
- Update store refresh, filtering, and cache policy.
- Wire views last.
4. Keep app wiring minimal
- Keep app entry focused on scene/menu wiring and dependency injection.
- Avoid embedding business logic in `App` or menu scene declarations.
5. Standardize launch ergonomics
- Ensure run script restarts an existing instance before relaunching.
- Ensure run script does not open Xcode as a side effect.
- Use `tuist generate --no-open` when generation is required.
- When the run script builds the generated project, prefer `TUIST_SKIP_UPDATE_CHECK=1 tuist xcodebuild build ...` instead of invoking raw `xcodebuild` directly.
## Validation Matrix
Run validations after edits:
```bash
TUIST_SKIP_UPDATE_CHECK=1 tuist xcodebuild build -scheme <TargetName> -configuration Debug
```
If launch workflow changed:
```bash
./run-menubar.sh
```
If shell scripts changed:
```bash
bash -n run-menubar.sh
bash -n stop-menubar.sh
./run-menubar.sh
```
## Failure Patterns and Fix Direction
- `tuist run` cannot resolve the macOS destination:
Use run/stop scripts as canonical local run path.
- Menu UI is laggy or inconsistent after refresh:
Move derived state and filtering into the store; keep views render-only.
- API payload changes break decode:
Relax model decoding with optional fields and defaults, then surface missing data safely in UI.
- Feature asks for quick UI patch:
Trace root cause in model/client/store before changing row/menu presentation.
## Completion Checklist
- Preserve menubar-only behavior unless explicitly changed.
- Keep network and state logic out of SwiftUI view bodies.
- Keep Tuist manifests and run scripts aligned with actual build/run flow.
- Run the validation matrix for touched areas.
- Report concrete commands run and outcomes.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "macOS Menubar Tuist App"
short_description: "Tuist-first macOS menubar app build and maintenance"
default_prompt: "Use $macos-menubar-tuist-app to scaffold or refactor a menubar-only macOS app with Tuist, layered state architecture, and script-based run validation."

View File

@@ -0,0 +1,105 @@
---
name: macos-spm-app-packaging
description: Scaffold, build, sign, and package SwiftPM macOS apps without Xcode projects.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# macOS SwiftPM App Packaging (No Xcode)
## Overview
Bootstrap a complete SwiftPM macOS app folder, then build, package, and run it without Xcode. Use `assets/templates/bootstrap/` for the starter layout and `references/packaging.md` + `references/release.md` for packaging and release details.
## When to Use
- When the user needs a SwiftPM-based macOS app without relying on an Xcode project.
- When you need packaging, signing, notarization, or appcast guidance for a SwiftPM app.
## Two-Step Workflow
1) Bootstrap the project folder
- Copy `assets/templates/bootstrap/` into a new repo.
- Rename `MyApp` in `Package.swift`, `Sources/MyApp/`, and `version.env`.
- Customize `APP_NAME`, `BUNDLE_ID`, and versions.
2) Build, package, and run the bootstrapped app
- Copy scripts from `assets/templates/` into your repo (for example, `Scripts/`).
- Build/tests: `swift build` and `swift test`.
- Package: `Scripts/package_app.sh`.
- Run: `Scripts/compile_and_run.sh` (preferred) or `Scripts/launch.sh`.
- Release (optional): `Scripts/sign-and-notarize.sh` and `Scripts/make_appcast.sh`.
- Tag + GitHub release (optional): create a git tag, upload the zip/appcast to the GitHub release, and publish.
## Minimum End-to-End Example
Shortest path from bootstrap to a running app:
```bash
# 1. Copy and rename the skeleton
cp -R assets/templates/bootstrap/ ~/Projects/MyApp
cd ~/Projects/MyApp
sed -i '' 's/MyApp/HelloApp/g' Package.swift version.env
# 2. Copy scripts
cp assets/templates/package_app.sh Scripts/
cp assets/templates/compile_and_run.sh Scripts/
chmod +x Scripts/*.sh
# 3. Build and launch
swift build
Scripts/compile_and_run.sh
```
## Validation Checkpoints
Run these after key steps to catch failures early before proceeding to the next stage.
**After packaging (`Scripts/package_app.sh`):**
```bash
# Confirm .app bundle structure is intact
ls -R build/HelloApp.app/Contents
# Check that the binary is present and executable
file build/HelloApp.app/Contents/MacOS/HelloApp
```
**After signing (`Scripts/sign-and-notarize.sh` or ad-hoc dev signing):**
```bash
# Inspect signature and entitlements
codesign -dv --verbose=4 build/HelloApp.app
# Verify the bundle passes Gatekeeper checks locally
spctl --assess --type execute --verbose build/HelloApp.app
```
**After notarization and stapling:**
```bash
# Confirm the staple ticket is attached
stapler validate build/HelloApp.app
# Re-run Gatekeeper to confirm notarization is recognised
spctl --assess --type execute --verbose build/HelloApp.app
```
## Common Notarization Failures
| Symptom | Likely Cause | Recovery |
|---|---|---|
| `The software asset has already been uploaded` | Duplicate submission for same version | Bump `BUILD_NUMBER` in `version.env` and repackage. |
| `Package Invalid: Invalid Code Signing Entitlements` | Entitlements in `.entitlements` file don't match provisioning | Audit entitlements against Apple's allowed set; remove unsupported keys. |
| `The executable does not have the hardened runtime enabled` | Missing `--options runtime` flag in `codesign` invocation | Edit `sign-and-notarize.sh` to add `--options runtime` to all `codesign` calls. |
| Notarization hangs / no status email | `xcrun notarytool` network or credential issue | Run `xcrun notarytool history` to check status; re-export App Store Connect API key if expired. |
| `stapler validate` fails after successful notarization | Ticket not yet propagated | Wait ~60 s, then re-run `xcrun stapler staple`. |
## Templates
- `assets/templates/package_app.sh`: Build binaries, create the .app bundle, copy resources, sign.
- `assets/templates/compile_and_run.sh`: Dev loop to kill running app, package, launch.
- `assets/templates/build_icon.sh`: Generate .icns from an Icon Composer file (requires Xcode install).
- `assets/templates/sign-and-notarize.sh`: Notarize, staple, and zip a release build.
- `assets/templates/make_appcast.sh`: Generate Sparkle appcast entries for updates.
- `assets/templates/setup_dev_signing.sh`: Create a stable dev code-signing identity.
- `assets/templates/launch.sh`: Simple launcher for a packaged .app.
- `assets/templates/version.env`: Example version file consumed by packaging scripts.
- `assets/templates/bootstrap/`: Minimal SwiftPM macOS app skeleton (Package.swift, Sources/, version.env).
## Notes
- Keep entitlements and signing configuration explicit; edit the template scripts instead of reimplementing.
- Remove Sparkle steps if you do not use Sparkle for updates.
- Sparkle relies on the bundle build number (`CFBundleVersion`), so `BUILD_NUMBER` in `version.env` must increase for each update.
- For menu bar apps, set `MENU_BAR_APP=1` when packaging to emit `LSUIElement` in Info.plist.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "macOS SwiftPM Packaging"
short_description: "Package SwiftPM macOS apps"
default_prompt: "Use $macos-spm-app-packaging to scaffold or package a SwiftPM-based macOS app without Xcode."

View File

@@ -0,0 +1,17 @@
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [
.macOS(.v14),
],
targets: [
.executableTarget(
name: "MyApp",
path: "Sources/MyApp",
resources: [
.process("Resources"),
])
]
)

View File

@@ -0,0 +1,11 @@
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
Text("Hello from MyApp")
.padding()
}
}
}

View File

@@ -0,0 +1,2 @@
MARKETING_VERSION=0.1.0
BUILD_NUMBER=1

View File

@@ -0,0 +1,49 @@
#!/usr/bin/env bash
set -euo pipefail
ICON_FILE=${1:-Icon.icon}
BASENAME=${2:-Icon}
OUT_ROOT=${3:-build/icon}
XCODE_APP=${XCODE_APP:-/Applications/Xcode.app}
ICTOOL="$XCODE_APP/Contents/Applications/Icon Composer.app/Contents/Executables/ictool"
if [[ ! -x "$ICTOOL" ]]; then
ICTOOL="$XCODE_APP/Contents/Applications/Icon Composer.app/Contents/Executables/icontool"
fi
if [[ ! -x "$ICTOOL" ]]; then
echo "ictool/icontool not found. Set XCODE_APP if Xcode is elsewhere." >&2
exit 1
fi
ICONSET_DIR="$OUT_ROOT/${BASENAME}.iconset"
TMP_DIR="$OUT_ROOT/tmp"
mkdir -p "$ICONSET_DIR" "$TMP_DIR"
MASTER_ART="$TMP_DIR/icon_art_824.png"
MASTER_1024="$TMP_DIR/icon_1024.png"
# Render inner art (no margin) with macOS Default appearance.
"$ICTOOL" "$ICON_FILE" \
--export-preview macOS Default 824 824 1 -45 "$MASTER_ART"
# Pad to 1024x1024 with transparent border.
sips --padToHeightWidth 1024 1024 "$MASTER_ART" --out "$MASTER_1024" >/dev/null
# Generate required sizes.
sizes=(16 32 64 128 256 512 1024)
for sz in "${sizes[@]}"; do
out="$ICONSET_DIR/icon_${sz}x${sz}.png"
sips -z "$sz" "$sz" "$MASTER_1024" --out "$out" >/dev/null
if [[ "$sz" -ne 1024 ]]; then
dbl=$((sz*2))
out2="$ICONSET_DIR/icon_${sz}x${sz}@2x.png"
sips -z "$dbl" "$dbl" "$MASTER_1024" --out "$out2" >/dev/null
fi
done
# 512x512@2x already covered by 1024; ensure it exists.
cp "$MASTER_1024" "$ICONSET_DIR/icon_512x512@2x.png"
iconutil -c icns "$ICONSET_DIR" -o Icon.icns
echo "Icon.icns generated at $(pwd)/Icon.icns"

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Kill running instances, package, relaunch, verify.
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
APP_NAME=${APP_NAME:-MyApp}
APP_BUNDLE="${ROOT_DIR}/${APP_NAME}.app"
APP_PROCESS_PATTERN="${APP_NAME}.app/Contents/MacOS/${APP_NAME}"
DEBUG_PROCESS_PATTERN="${ROOT_DIR}/.build/debug/${APP_NAME}"
RELEASE_PROCESS_PATTERN="${ROOT_DIR}/.build/release/${APP_NAME}"
RUN_TESTS=0
RELEASE_ARCHES=""
log() { printf '%s\n' "$*"; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
for arg in "$@"; do
case "${arg}" in
--test|-t) RUN_TESTS=1 ;;
--release-universal) RELEASE_ARCHES="arm64 x86_64" ;;
--release-arches=*) RELEASE_ARCHES="${arg#*=}" ;;
--help|-h)
log "Usage: $(basename "$0") [--test] [--release-universal] [--release-arches=\"arm64 x86_64\"]"
exit 0
;;
esac
done
log "==> Killing existing ${APP_NAME} instances"
pkill -f "${APP_PROCESS_PATTERN}" 2>/dev/null || true
pkill -f "${DEBUG_PROCESS_PATTERN}" 2>/dev/null || true
pkill -f "${RELEASE_PROCESS_PATTERN}" 2>/dev/null || true
pkill -x "${APP_NAME}" 2>/dev/null || true
if [[ "${RUN_TESTS}" == "1" ]]; then
log "==> swift test"
swift test -q
fi
HOST_ARCH="$(uname -m)"
ARCHES_VALUE="${HOST_ARCH}"
if [[ -n "${RELEASE_ARCHES}" ]]; then
ARCHES_VALUE="${RELEASE_ARCHES}"
fi
log "==> package app"
SIGNING_MODE=adhoc ARCHES="${ARCHES_VALUE}" "${ROOT_DIR}/Scripts/package_app.sh" release
log "==> launch app"
if ! open "${APP_BUNDLE}"; then
log "WARN: open failed; launching binary directly."
"${APP_BUNDLE}/Contents/MacOS/${APP_NAME}" >/dev/null 2>&1 &
disown
fi
for _ in {1..10}; do
if pgrep -f "${APP_PROCESS_PATTERN}" >/dev/null 2>&1; then
log "OK: ${APP_NAME} is running."
exit 0
fi
sleep 0.4
done
fail "App exited immediately. Check crash logs in Console.app (User Reports)."

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
APP_NAME=${APP_NAME:-MyApp}
APP_PATH="$PROJECT_ROOT/${APP_NAME}.app"
echo "==> Killing existing ${APP_NAME} instances"
pkill -x "$APP_NAME" || pkill -f "${APP_NAME}.app" || true
sleep 0.5
if [[ ! -d "$APP_PATH" ]]; then
echo "ERROR: ${APP_NAME}.app not found at $APP_PATH"
echo "Run ./Scripts/package_app.sh first to build the app"
exit 1
fi
echo "==> Launching ${APP_NAME} from $APP_PATH"
open -n "$APP_PATH"
sleep 1
if pgrep -x "$APP_NAME" > /dev/null; then
echo "OK: ${APP_NAME} is running."
else
echo "ERROR: App exited immediately. Check crash logs in Console.app (User Reports)."
exit 1
fi

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
ZIP=${1:?
"Usage: $0 MyApp-<ver>.zip"}
FEED_URL=${2:-"https://example.com/appcast.xml"}
PRIVATE_KEY_FILE=${SPARKLE_PRIVATE_KEY_FILE:-}
if [[ -z "$PRIVATE_KEY_FILE" ]]; then
echo "Set SPARKLE_PRIVATE_KEY_FILE to your ed25519 private key (Sparkle)." >&2
exit 1
fi
if [[ ! -f "$ZIP" ]]; then
echo "Zip not found: $ZIP" >&2
exit 1
fi
ZIP_DIR=$(cd "$(dirname "$ZIP")" && pwd)
ZIP_NAME=$(basename "$ZIP")
ZIP_BASE="${ZIP_NAME%.zip}"
VERSION=${SPARKLE_RELEASE_VERSION:-}
if [[ -z "$VERSION" ]]; then
if [[ "$ZIP_NAME" =~ ^[^-]+-([0-9]+(\.[0-9]+){1,2}([-.][^.]*)?)\.zip$ ]]; then
VERSION="${BASH_REMATCH[1]}"
else
echo "Could not infer version from $ZIP_NAME; set SPARKLE_RELEASE_VERSION." >&2
exit 1
fi
fi
NOTES_HTML="${ZIP_DIR}/${ZIP_BASE}.html"
KEEP_NOTES=${KEEP_SPARKLE_NOTES:-0}
if [[ -x "$ROOT/Scripts/changelog-to-html.sh" ]]; then
"$ROOT/Scripts/changelog-to-html.sh" "$VERSION" >"$NOTES_HTML"
else
cat >"$NOTES_HTML" <<HTML
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>${ZIP_BASE}</title>
<body>
<h2>${ZIP_BASE}</h2>
<p>Release notes not provided.</p>
</body>
</html>
HTML
fi
cleanup() {
if [[ -n "${WORK_DIR:-}" ]]; then
rm -rf "$WORK_DIR"
fi
if [[ "$KEEP_NOTES" != "1" ]]; then
rm -f "$NOTES_HTML"
fi
}
trap cleanup EXIT
DOWNLOAD_URL_PREFIX=${SPARKLE_DOWNLOAD_URL_PREFIX:-"https://example.com/downloads/v${VERSION}/"}
if ! command -v generate_appcast >/dev/null; then
echo "generate_appcast not found in PATH. Install Sparkle tools." >&2
exit 1
fi
WORK_DIR=$(mktemp -d /tmp/appcast.XXXXXX)
cp "$ROOT/appcast.xml" "$WORK_DIR/appcast.xml"
cp "$ZIP" "$WORK_DIR/$ZIP_NAME"
cp "$NOTES_HTML" "$WORK_DIR/$ZIP_BASE.html"
pushd "$WORK_DIR" >/dev/null
generate_appcast \
--ed-key-file "$PRIVATE_KEY_FILE" \
--download-url-prefix "$DOWNLOAD_URL_PREFIX" \
--embed-release-notes \
--link "$FEED_URL" \
"$WORK_DIR"
popd >/dev/null
cp "$WORK_DIR/appcast.xml" "$ROOT/appcast.xml"
echo "Appcast generated (appcast.xml). Upload alongside $ZIP at $FEED_URL"

View File

@@ -0,0 +1,206 @@
#!/usr/bin/env bash
set -euo pipefail
CONF=${1:-release}
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT"
APP_NAME=${APP_NAME:-MyApp}
BUNDLE_ID=${BUNDLE_ID:-com.example.myapp}
MACOS_MIN_VERSION=${MACOS_MIN_VERSION:-14.0}
MENU_BAR_APP=${MENU_BAR_APP:-0}
SIGNING_MODE=${SIGNING_MODE:-}
APP_IDENTITY=${APP_IDENTITY:-}
if [[ -f "$ROOT/version.env" ]]; then
source "$ROOT/version.env"
else
MARKETING_VERSION=${MARKETING_VERSION:-0.1.0}
BUILD_NUMBER=${BUILD_NUMBER:-1}
fi
ARCH_LIST=( ${ARCHES:-} )
if [[ ${#ARCH_LIST[@]} -eq 0 ]]; then
HOST_ARCH=$(uname -m)
ARCH_LIST=("$HOST_ARCH")
fi
for ARCH in "${ARCH_LIST[@]}"; do
swift build -c "$CONF" --arch "$ARCH"
done
APP="$ROOT/${APP_NAME}.app"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/Frameworks"
# Convert Icon.icon to Icon.icns if present (requires iconutil).
ICON_SOURCE="$ROOT/Icon.icon"
ICON_TARGET="$ROOT/Icon.icns"
if [[ -f "$ICON_SOURCE" ]]; then
iconutil --convert icns --output "$ICON_TARGET" "$ICON_SOURCE"
fi
LSUI_VALUE="false"
if [[ "$MENU_BAR_APP" == "1" ]]; then
LSUI_VALUE="true"
fi
BUILD_TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
cat > "$APP/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key><string>${APP_NAME}</string>
<key>CFBundleDisplayName</key><string>${APP_NAME}</string>
<key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>
<key>CFBundleExecutable</key><string>${APP_NAME}</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>${MARKETING_VERSION}</string>
<key>CFBundleVersion</key><string>${BUILD_NUMBER}</string>
<key>LSMinimumSystemVersion</key><string>${MACOS_MIN_VERSION}</string>
<key>LSUIElement</key><${LSUI_VALUE}/>
<key>CFBundleIconFile</key><string>Icon</string>
<key>BuildTimestamp</key><string>${BUILD_TIMESTAMP}</string>
<key>GitCommit</key><string>${GIT_COMMIT}</string>
</dict>
</plist>
PLIST
build_product_path() {
local name="$1"
local arch="$2"
case "$arch" in
arm64|x86_64) echo ".build/${arch}-apple-macosx/$CONF/$name" ;;
*) echo ".build/$CONF/$name" ;;
esac
}
verify_binary_arches() {
local binary="$1"; shift
local expected=("$@")
local actual
actual=$(lipo -archs "$binary")
local actual_count expected_count
actual_count=$(wc -w <<<"$actual" | tr -d ' ')
expected_count=${#expected[@]}
if [[ "$actual_count" -ne "$expected_count" ]]; then
echo "ERROR: $binary arch mismatch (expected: ${expected[*]}, actual: ${actual})" >&2
exit 1
fi
for arch in "${expected[@]}"; do
if [[ "$actual" != *"$arch"* ]]; then
echo "ERROR: $binary missing arch $arch (have: ${actual})" >&2
exit 1
fi
done
}
install_binary() {
local name="$1"
local dest="$2"
local binaries=()
for arch in "${ARCH_LIST[@]}"; do
local src
src=$(build_product_path "$name" "$arch")
if [[ ! -f "$src" ]]; then
echo "ERROR: Missing ${name} build for ${arch} at ${src}" >&2
exit 1
fi
binaries+=("$src")
done
if [[ ${#ARCH_LIST[@]} -gt 1 ]]; then
lipo -create "${binaries[@]}" -output "$dest"
else
cp "${binaries[0]}" "$dest"
fi
chmod +x "$dest"
verify_binary_arches "$dest" "${ARCH_LIST[@]}"
}
install_binary "$APP_NAME" "$APP/Contents/MacOS/$APP_NAME"
# Bundle app resources (if any).
APP_RESOURCES_DIR="$ROOT/Sources/$APP_NAME/Resources"
if [[ -d "$APP_RESOURCES_DIR" ]]; then
cp -R "$APP_RESOURCES_DIR/." "$APP/Contents/Resources/"
fi
# SwiftPM resource bundles are emitted next to the built binary.
PREFERRED_BUILD_DIR="$(dirname "$(build_product_path "$APP_NAME" "${ARCH_LIST[0]}")")"
shopt -s nullglob
SWIFTPM_BUNDLES=("${PREFERRED_BUILD_DIR}/"*.bundle)
shopt -u nullglob
if [[ ${#SWIFTPM_BUNDLES[@]} -gt 0 ]]; then
for bundle in "${SWIFTPM_BUNDLES[@]}"; do
cp -R "$bundle" "$APP/Contents/Resources/"
done
fi
# Embed frameworks if any exist in the build folder.
FRAMEWORK_DIRS=(".build/$CONF" ".build/${ARCH_LIST[0]}-apple-macosx/$CONF")
for dir in "${FRAMEWORK_DIRS[@]}"; do
if compgen -G "${dir}/*.framework" >/dev/null; then
cp -R "${dir}/"*.framework "$APP/Contents/Frameworks/"
chmod -R a+rX "$APP/Contents/Frameworks"
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP/Contents/MacOS/$APP_NAME"
break
fi
done
if [[ -f "$ICON_TARGET" ]]; then
cp "$ICON_TARGET" "$APP/Contents/Resources/Icon.icns"
fi
# Ensure contents are writable before stripping attributes and signing.
chmod -R u+w "$APP"
# Strip extended attributes to prevent AppleDouble files that break code sealing.
xattr -cr "$APP"
find "$APP" -name '._*' -delete
ENTITLEMENTS_DIR="$ROOT/.build/entitlements"
DEFAULT_ENTITLEMENTS="$ENTITLEMENTS_DIR/${APP_NAME}.entitlements"
mkdir -p "$ENTITLEMENTS_DIR"
APP_ENTITLEMENTS=${APP_ENTITLEMENTS:-$DEFAULT_ENTITLEMENTS}
if [[ ! -f "$APP_ENTITLEMENTS" ]]; then
cat > "$APP_ENTITLEMENTS" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Add entitlements here if needed. -->
</dict>
</plist>
PLIST
fi
if [[ "$SIGNING_MODE" == "adhoc" || -z "$APP_IDENTITY" ]]; then
CODESIGN_ARGS=(--force --sign "-")
else
CODESIGN_ARGS=(--force --timestamp --options runtime --sign "$APP_IDENTITY")
fi
# Sign embedded frameworks and their nested binaries before the app bundle.
sign_frameworks() {
local fw
for fw in "$APP/Contents/Frameworks/"*.framework; do
if [[ ! -d "$fw" ]]; then
continue
fi
while IFS= read -r -d '' bin; do
codesign "${CODESIGN_ARGS[@]}" "$bin"
done < <(find "$fw" -type f -perm -111 -print0)
codesign "${CODESIGN_ARGS[@]}" "$fw"
done
}
sign_frameworks
codesign "${CODESIGN_ARGS[@]}" \
--entitlements "$APP_ENTITLEMENTS" \
"$APP"
echo "Created $APP"

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Setup stable development code signing to reduce keychain prompts.
set -euo pipefail
APP_NAME=${APP_NAME:-MyApp}
CERT_NAME="${APP_NAME} Development"
if security find-certificate -c "$CERT_NAME" >/dev/null 2>&1; then
echo "Certificate '$CERT_NAME' already exists."
echo "Export this in your shell profile:"
echo " export APP_IDENTITY='$CERT_NAME'"
exit 0
fi
echo "Creating self-signed certificate '$CERT_NAME'..."
TEMP_CONFIG=$(mktemp)
trap "rm -f $TEMP_CONFIG" EXIT
cat > "$TEMP_CONFIG" <<EOFCONF
[ req ]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[ req_distinguished_name ]
CN = $CERT_NAME
O = ${APP_NAME} Development
C = US
[ v3_req ]
keyUsage = critical,digitalSignature
extendedKeyUsage = codeSigning
EOFCONF
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 \
-nodes -keyout /tmp/dev.key -out /tmp/dev.crt \
-config "$TEMP_CONFIG" 2>/dev/null
openssl pkcs12 -export -out /tmp/dev.p12 \
-inkey /tmp/dev.key -in /tmp/dev.crt \
-passout pass: 2>/dev/null
security import /tmp/dev.p12 -k ~/Library/Keychains/login.keychain-db \
-T /usr/bin/codesign -T /usr/bin/security
rm -f /tmp/dev.{key,crt,p12}
echo ""
echo "Trust this certificate for code signing in Keychain Access."
echo "Then export in your shell profile:"
echo " export APP_IDENTITY='$CERT_NAME'"

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
APP_NAME=${APP_NAME:-MyApp}
APP_IDENTITY=${APP_IDENTITY:-"Developer ID Application: Example (TEAMID)"}
APP_BUNDLE="${APP_NAME}.app"
ROOT=$(cd "$(dirname "$0")/.." && pwd)
source "$ROOT/version.env"
ZIP_NAME="${APP_NAME}-${MARKETING_VERSION}.zip"
if [[ -z "${APP_STORE_CONNECT_API_KEY_P8:-}" || -z "${APP_STORE_CONNECT_KEY_ID:-}" || -z "${APP_STORE_CONNECT_ISSUER_ID:-}" ]]; then
echo "Missing APP_STORE_CONNECT_* env vars (API key, key id, issuer id)." >&2
exit 1
fi
echo "$APP_STORE_CONNECT_API_KEY_P8" | sed 's/\\n/\n/g' > /tmp/app-store-connect-key.p8
trap 'rm -f /tmp/app-store-connect-key.p8 /tmp/${APP_NAME}Notarize.zip' EXIT
ARCHES_VALUE=${ARCHES:-"arm64 x86_64"}
ARCH_LIST=( ${ARCHES_VALUE} )
for ARCH in "${ARCH_LIST[@]}"; do
swift build -c release --arch "$ARCH"
done
ARCHES="${ARCHES_VALUE}" "$ROOT/Scripts/package_app.sh" release
ENTITLEMENTS_DIR="$ROOT/.build/entitlements"
APP_ENTITLEMENTS="${APP_ENTITLEMENTS:-${ENTITLEMENTS_DIR}/${APP_NAME}.entitlements}"
codesign --force --timestamp --options runtime --sign "$APP_IDENTITY" \
--entitlements "$APP_ENTITLEMENTS" \
"$APP_BUNDLE"
DITTO_BIN=${DITTO_BIN:-/usr/bin/ditto}
"$DITTO_BIN" --norsrc -c -k --keepParent "$APP_BUNDLE" "/tmp/${APP_NAME}Notarize.zip"
xcrun notarytool submit "/tmp/${APP_NAME}Notarize.zip" \
--key /tmp/app-store-connect-key.p8 \
--key-id "$APP_STORE_CONNECT_KEY_ID" \
--issuer "$APP_STORE_CONNECT_ISSUER_ID" \
--wait
xcrun stapler staple "$APP_BUNDLE"
xattr -cr "$APP_BUNDLE"
find "$APP_BUNDLE" -name '._*' -delete
"$DITTO_BIN" --norsrc -c -k --keepParent "$APP_BUNDLE" "$ZIP_NAME"
spctl -a -t exec -vv "$APP_BUNDLE"
stapler validate "$APP_BUNDLE"
echo "Done: $ZIP_NAME"

View File

@@ -0,0 +1,2 @@
MARKETING_VERSION=0.1.0
BUILD_NUMBER=1

View File

@@ -0,0 +1,17 @@
# Packaging notes
## Build output paths
SwiftPM places binaries under:
- `.build/<arch>-apple-macosx/<config>/<AppName>` for arch-specific builds
- `.build/<config>/<AppName>` for some products (frameworks/tools)
Use `ARCHES="arm64 x86_64"` with `swift build` to produce universal binaries.
## Common environment variables (used by templates)
- `APP_NAME`: App/binary name (for example, `MyApp`).
- `BUNDLE_ID`: Bundle identifier (for example, `com.example.myapp`).
- `ARCHES`: Space-separated architectures (default: host arch).
- `SIGNING_MODE`: `adhoc` to avoid keychain prompts in dev.
- `APP_IDENTITY`: Codesigning identity name for release builds.
- `MACOS_MIN_VERSION`: Minimum macOS version for Info.plist.
- `MENU_BAR_APP`: Set to `1` to add `LSUIElement` to Info.plist.

View File

@@ -0,0 +1,32 @@
# Release and notarization notes
## Notarization requirements
- Install Xcode Command Line Tools (for `xcrun` and `notarytool`).
- Provide App Store Connect API credentials:
- `APP_STORE_CONNECT_API_KEY_P8`
- `APP_STORE_CONNECT_KEY_ID`
- `APP_STORE_CONNECT_ISSUER_ID`
- Provide a Developer ID Application identity in `APP_IDENTITY`.
## Sparkle appcast (optional)
- Install Sparkle tools so `generate_appcast` is on PATH.
- Provide `SPARKLE_PRIVATE_KEY_FILE` (ed25519 key).
- The appcast script uses your zip artifact to create an updated `appcast.xml`.
- Sparkle compares `sparkle:version` (derived from `CFBundleVersion`), so bump `BUILD_NUMBER` for every release.
## Tag and GitHub release (optional)
Use a versioned git tag and publish a GitHub release with the notarized zip (and appcast if you host it on GitHub Releases).
Example flow:
```
git tag v<version>
git push origin v<version>
gh release create v<version> CodexBar-<version>.zip appcast.xml \
--title "AppName <version>" \
--notes-file CHANGELOG.md
```
Notes:
- If you serve appcast from GitHub Releases or raw URLs, ensure the release is published and assets are accessible (no 404s).
- Prefer using a curated release notes file rather than dumping the full changelog.

View File

@@ -0,0 +1,79 @@
# Scaffold a SwiftPM macOS app (no Xcode)
## Steps
1) Create a repo and initialize SwiftPM:
```
mkdir MyApp
cd MyApp
swift package init --type executable
```
2) Update `Package.swift` to target macOS and define an executable target for the app.
3) Create the app entry point under `Sources/MyApp/`.
- Use SwiftUI if you want a windowed app with minimal AppKit glue.
- Use AppKit if you want a menu bar or accessory-style app.
4) If you need app resources, add:
```
resources: [.process("Resources")]
```
and create `Sources/MyApp/Resources/`.
5) Add a `version.env` file (used by packaging templates):
```
MARKETING_VERSION=0.1.0
BUILD_NUMBER=1
```
6) Copy script templates from `assets/templates/` into your repo (for example, `Scripts/`).
## Minimal Package.swift (example)
```
// swift-tools-version: 6.2
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "MyApp",
path: "Sources/MyApp",
resources: [
.process("Resources")
])
]
)
```
## Minimal SwiftUI entry point (example)
```
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
Text("Hello")
}
}
}
```
## Minimal AppKit entry point (example)
```
import AppKit
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
// Initialize app state here.
}
}
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular)
app.run()
```

View File

@@ -0,0 +1,97 @@
---
name: "orchestrate-batch-refactor"
description: "Plan and execute large refactors with dependency-aware work packets and parallel analysis."
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# Orchestrate Batch Refactor
## Overview
Use this skill to run high-throughput refactors safely.
Analyze scope in parallel, synthesize a single plan, then execute independent work packets with sub-agents.
## When to Use
- When a refactor spans many files or subsystems and needs clear work partitioning.
- When you need dependency-aware planning before parallel implementation.
## Inputs
- Repo path and target scope (paths, modules, or feature area)
- Goal type: refactor, rewrite, or hybrid
- Constraints: behavior parity, API stability, deadlines, test requirements
## When to Use Parallelization
- Use this skill for medium/large scope touching many files or subsystems.
- Skip multi-agent execution for tiny edits or highly coupled single-file work.
## Core Workflow
1. Define scope and success criteria.
- List target paths/modules and non-goals.
- State behavior constraints (for example: preserve external behavior).
2. Run parallel analysis first.
- Split target scope into analysis lanes.
- Spawn `explorer` sub-agents in parallel to analyze each lane.
- Ask each agent for: intent map, coupling risks, candidate work packets, required validations.
3. Build one dependency-aware plan.
- Merge explorer output into a single work graph.
- Create work packets with clear file ownership and validation commands.
- Sequence packets by dependency level; run only independent packets in parallel.
4. Execute with worker agents.
- Spawn one `worker` per independent packet.
- Assign explicit ownership (files/responsibility).
- Instruct every worker that they are not alone in the codebase and must ignore unrelated edits.
5. Integrate and verify.
- Review packet outputs, resolve overlaps, and run validation gates.
- Run targeted tests per packet, then broader suite for integrated scope.
6. Report and close.
- Summarize packet outcomes, key refactors, conflicts resolved, and residual risks.
## Work Packet Rules
- One owner per file per execution wave.
- No parallel edits on overlapping file sets.
- Keep packet goals narrow and measurable.
- Include explicit done criteria and required checks.
- Prefer behavior-preserving refactors unless user explicitly requests behavior change.
## Planning Contract
Every packet must include:
1. Packet ID and objective.
2. Owned files.
3. Dependencies (none or packet IDs).
4. Risks and invariants to preserve.
5. Required checks.
6. Integration notes for main thread.
Use [`references/work-packet-template.md`](references/work-packet-template.md) for the exact shape.
## Agent Prompting Contract
- Use the prompt templates in [`references/agent-prompt-templates.md`](references/agent-prompt-templates.md).
- Explorer prompts focus on analysis and decomposition.
- Worker prompts focus on implementation and validation with strict ownership boundaries.
## Safety Guardrails
- Do not start worker execution before plan synthesis is complete.
- Do not parallelize across unresolved dependencies.
- Do not claim completion if any required packet check fails.
- Stop and re-plan when packet boundaries cause repeated merge conflicts.
## Validation Strategy
Run in this order:
1. Packet-level checks (fast and scoped).
2. Cross-packet integration checks.
3. Full project safety checks when scope is broad.
Prefer fast feedback loops, but never skip required behavior checks.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Batch Refactor Orchestrator"
short_description: "Plan and parallelize large refactors safely."
default_prompt: "Use $orchestrate-batch-refactor to analyze target scope, plan parallel workstreams, and execute multi-agent refactors safely."

View File

@@ -0,0 +1,53 @@
# Agent Prompt Templates
Use these templates when spawning sub-agents.
## Explorer Prompt Template
```
Analyze the target scope and return decomposition guidance only.
Scope:
- Paths/modules: <fill>
- Goal: <refactor|rewrite|hybrid>
- Constraints: <behavior/API/test constraints>
Return:
1. Intent map (what each area currently does)
2. Coupling and dependency risks
3. Candidate work packets with non-overlapping ownership
4. Validation commands per packet
5. Recommended execution order
```
## Worker Prompt Template
```
You own this packet and are not alone in the codebase.
Ignore unrelated edits by others and do not touch files outside ownership.
Packet:
- ID: <fill>
- Objective: <fill>
- Owned files: <fill>
- Dependencies already completed: <fill>
- Invariants to preserve: <fill>
- Required checks: <fill>
Execution requirements:
1. Implement only the packet objective.
2. Preserve specified invariants and external behavior.
3. Run required checks and report exact results.
4. Summarize changed files and any integration notes.
```
## Main Thread Synthesis Prompt Template
```
Merge explorer outputs into a single dependency-aware plan.
Produce:
1. Packet table with ownership and dependencies
2. Parallel execution waves (no overlap per wave)
3. Validation matrix by packet and integration stage
4. Risk list with mitigation actions
```

View File

@@ -0,0 +1,31 @@
# Work Packet Template
Use this template to define each packet before spawning workers.
## Packet
- `id`:
- `objective`:
- `mode`: `refactor` | `rewrite` | `hybrid`
- `owner_agent_type`: `worker`
- `owned_files`:
- `dependencies`:
- `invariants_to_preserve`:
- `out_of_scope`:
- `required_checks`:
- `integration_notes`:
- `done_criteria`:
## Example
- `id`: `P3`
- `objective`: "Extract duplicated parsing logic from thread reducers into shared helper"
- `mode`: `refactor`
- `owner_agent_type`: `worker`
- `owned_files`: `src/features/threads/hooks/threadReducer/*.ts`
- `dependencies`: `P1`
- `invariants_to_preserve`: "Thread ordering and hidden-thread filtering behavior"
- `out_of_scope`: "UI rendering components"
- `required_checks`: `npm run typecheck`, `npm run test -- src/features/threads/hooks`
- `integration_notes`: "Main thread verifies no overlapping helper names with existing util package"
- `done_criteria`: "No duplicated parsing block remains; all required checks pass"

View File

@@ -0,0 +1,190 @@
---
name: project-skill-audit
description: Audit a project and recommend the highest-value skills to add or update.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# Project Skill Audit
## Overview
Audit the project's real recurring workflows before recommending skills. Prefer evidence from memory, rollout summaries, existing skill folders, and current repo conventions over generic brainstorming.
Recommend updates before new skills when an existing project skill is already close to the needed behavior.
## When to Use
- When the user asks what skills a project needs or which existing skills should be updated.
- When recommendations should be grounded in project history, memory files, and local conventions.
## Workflow
1. Map the current project surface.
Identify the repo root and read the most relevant project guidance first, such as `AGENTS.md`, `README.md`, roadmap/ledger files, and local docs that define workflows or validation expectations.
2. Build the memory/session path first.
Resolve the memory base as `$CODEX_HOME` when set, otherwise default to `~/.codex`.
Use these locations:
- memory index: `$CODEX_HOME/memories/MEMORY.md` or `~/.codex/memories/MEMORY.md`
- rollout summaries: `$CODEX_HOME/memories/rollout_summaries/`
- raw sessions: `$CODEX_HOME/sessions/` or `~/.codex/sessions/`
3. Read project past sessions in this order.
If the runtime prompt already includes a memory summary, start there.
Then search `MEMORY.md` for:
- repo name
- repo basename
- current `cwd`
- important module or file names
Open only the 1-3 most relevant rollout summaries first.
Fall back to raw session JSONL only when the summaries are missing the exact evidence you need.
4. Scan existing project-local skills before suggesting anything new.
Check these locations relative to the current repo root:
- `.agents/skills`
- `.codex/skills`
- `skills`
Read both `SKILL.md` and `agents/openai.yaml` when present.
5. Compare project-local skills against recurring work.
Look for repeated patterns in past sessions:
- repeated validation sequences
- repeated failure shields
- recurring ownership boundaries
- repeated root-cause categories
- workflows that repeatedly require the same repo-specific context
If the pattern appears repeatedly and is not already well captured, it is a candidate skill.
6. Separate `new skill` from `update existing skill`.
Recommend an update when an existing skill is already the right bucket but has stale triggers, missing guardrails, outdated paths, weak validation instructions, or incomplete scope.
Recommend a new skill only when the workflow is distinct enough that stretching an existing skill would make it vague or confusing.
7. Check for overlap with global skills only after reviewing project-local skills.
Use `$CODEX_HOME/skills` and `$CODEX_HOME/skills/public` to avoid proposing project-local skills for workflows already solved well by a generic shared skill.
Do not reject a project-local skill just because a global skill exists; project-specific guardrails can still justify a local specialization.
## Session Analysis
### 1. Search memory index first
- Search `MEMORY.md` with `rg` using the repo name, basename, and `cwd`.
- Prefer entries that already cite rollout summaries with the same repo path.
- Capture:
- repeated workflows
- validation commands
- failure shields
- ownership boundaries
- milestone or roadmap coupling
### 2. Open targeted rollout summaries
- Open the most relevant summary files under `memories/rollout_summaries/`.
- Prefer summaries whose filenames, `cwd`, or `keywords` match the current project.
- Extract:
- what the user asked for repeatedly
- what steps kept recurring
- what broke repeatedly
- what commands proved correctness
- what project-specific context had to be rediscovered
### 3. Use raw sessions only as a fallback
- Only search `sessions/` JSONL files if rollout summaries are missing a concrete detail.
- Search by:
- exact `cwd`
- repo basename
- thread ID from a rollout summary
- specific file paths or commands
- Use raw sessions to recover exact prompts, command sequences, diffs, or failure text, not to replace the summary pass.
### 4. Turn session evidence into skill candidates
- A candidate `new skill` should correspond to a repeated workflow, not just a repeated topic.
- A candidate `skill update` should correspond to a workflow already covered by a local skill whose triggers, guardrails, or validation instructions no longer match the recorded sessions.
- Prefer concrete evidence such as:
- "this validation sequence appeared in 4 sessions"
- "this ownership confusion repeated across extractor and runtime fixes"
- "the same local script and telemetry probes had to be rediscovered repeatedly"
## Recommendation Rules
- Recommend a new skill when:
- the same repo-specific workflow or failure mode appears multiple times across sessions
- success depends on project-specific paths, scripts, ownership rules, or validation steps
- the workflow benefits from strong defaults or failure shields
- Recommend an update when:
- an existing project-local skill already covers most of the need
- `SKILL.md` and `agents/openai.yaml` drift from each other
- paths, scripts, validation commands, or milestone references are stale
- the skill body is too generic to reflect how the project is actually worked on
- Do not recommend a skill when:
- the pattern is a one-off bug rather than a reusable workflow
- a generic global skill already fits with no meaningful project-specific additions
- the workflow has not recurred enough to justify the maintenance cost
## What To Scan
- Past sessions and memory:
- memory summary already in context, if any
- `$CODEX_HOME/memories/MEMORY.md` or `~/.codex/memories/MEMORY.md`
- the 1-3 most relevant rollout summaries for the current repo
- raw `$CODEX_HOME/sessions` or `~/.codex/sessions` JSONL files only if summaries are insufficient
- Project-local skill surface:
- `./.agents/skills/*/SKILL.md`
- `./.agents/skills/*/agents/openai.yaml`
- `./.codex/skills/*/SKILL.md`
- `./skills/*/SKILL.md`
- Project conventions:
- `AGENTS.md`
- `README.md`
- roadmap, ledger, architecture, or validation docs
- current worktree or recent touched areas if needed for context
## Output Expectations
Return a compact audit with:
1. `Existing skills`
List the project-local skills found and the main workflow each one covers.
2. `Suggested updates`
For each update candidate, include:
- skill name
- why it is incomplete or stale
- the highest-value change to make
3. `Suggested new skills`
For each new skill, include:
- recommended skill name
- why it should exist
- what would trigger it
- the core workflow it should encode
4. `Priority order`
Rank the top recommendations by expected value.
## Naming Guidance
- Prefer short hyphen-case names.
- Use project prefixes for project-local skills when that improves clarity.
- Prefer verb-led or action-oriented names over vague nouns.
## Failure Shields
- Do not invent recurring patterns without session or repo evidence.
- Do not recommend duplicate skills when an update to an existing skill would suffice.
- Do not rely on a single memory note if the current repo clearly evolved since then.
- Do not bulk-load all rollout summaries; stay targeted.
- Do not skip rollout summaries and jump straight to raw sessions unless the summaries are insufficient.
- Do not recommend skills from themes alone; recommendations should come from repeated procedures, repeated validation flows, or repeated failure modes.
- Do not confuse a project's current implementation tasks with its reusable skill needs.
## Follow-up
If the user asks to actually create or update one of the recommended skills, switch to [$skill-creator](/Users/dimillian/.codex/skills/.system/skill-creator/SKILL.md) and implement the chosen skill rather than continuing the audit.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Project Skill Audit"
short_description: "Audit project sessions and skill coverage"
default_prompt: "Use $project-skill-audit to analyze this project and recommend new skills or updates to existing ones."

View File

@@ -0,0 +1,135 @@
---
name: react-component-performance
description: Diagnose slow React components and suggest targeted performance fixes.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# React Component Performance
## Overview
Identify render hotspots, isolate expensive updates, and apply targeted optimizations without changing UI behavior.
## When to Use
- When the user asks to profile or improve a slow React component.
- When you need to reduce re-renders, list lag, or expensive render work in React UI.
## Workflow
1. Reproduce or describe the slowdown.
2. Identify what triggers re-renders (state updates, props churn, effects).
3. Isolate fast-changing state from heavy subtrees.
4. Stabilize props and handlers; memoize where it pays off.
5. Reduce expensive work (computation, DOM size, list length).
6. **Validate**: open React DevTools Profiler → record the interaction → inspect the Flamegraph for components rendering longer than ~16 ms → compare against a pre-optimization baseline recording.
## Checklist
- Measure: use React DevTools Profiler or log renders; capture baseline.
- Find churn: identify state updated on a timer, scroll, input, or animation.
- Split: move ticking state into a child; keep heavy lists static.
- Memoize: wrap leaf rows with `memo` only when props are stable.
- Stabilize props: use `useCallback`/`useMemo` for handlers and derived values.
- Avoid derived work in render: precompute, or compute inside memoized helpers.
- Control list size: window/virtualize long lists; avoid rendering hidden items.
- Keys: ensure stable keys; avoid index when order can change.
- Effects: verify dependency arrays; avoid effects that re-run on every render.
- Style/layout: watch for expensive layout thrash or large Markdown/diff renders.
## Optimization Patterns
### Isolate ticking state
Move a timer or animation counter into a child so the parent list never re-renders on each tick.
```tsx
// ❌ Before entire parent (and list) re-renders every second
function Dashboard({ items }: { items: Item[] }) {
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<>
<Clock tick={tick} />
<ExpensiveList items={items} /> {/* re-renders every second */}
</>
);
}
// ✅ After only <Clock> re-renders; list is untouched
function Clock() {
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
return <span>{tick}s</span>;
}
function Dashboard({ items }: { items: Item[] }) {
return (
<>
<Clock />
<ExpensiveList items={items} />
</>
);
}
```
### Stabilize callbacks with `useCallback` + `memo`
```tsx
// ❌ Before new handler reference on every render busts Row memo
function List({ items }: { items: Item[] }) {
const handleClick = (id: string) => console.log(id); // new ref each render
return items.map(item => <Row key={item.id} item={item} onClick={handleClick} />);
}
// ✅ After stable handler; Row only re-renders when its own item changes
const Row = memo(({ item, onClick }: RowProps) => (
<li onClick={() => onClick(item.id)}>{item.name}</li>
));
function List({ items }: { items: Item[] }) {
const handleClick = useCallback((id: string) => console.log(id), []);
return items.map(item => <Row key={item.id} item={item} onClick={handleClick} />);
}
```
### Prefer derived data outside render
```tsx
// ❌ Before recomputes on every render
function Summary({ orders }: { orders: Order[] }) {
const total = orders.reduce((sum, o) => sum + o.amount, 0); // runs every render
return <p>Total: {total}</p>;
}
// ✅ After recomputes only when orders changes
function Summary({ orders }: { orders: Order[] }) {
const total = useMemo(() => orders.reduce((sum, o) => sum + o.amount, 0), [orders]);
return <p>Total: {total}</p>;
}
```
### Additional patterns
- **Split rows**: extract list rows into memoized components with narrow props.
- **Defer heavy rendering**: lazy-render or collapse expensive content until expanded.
## Profiling Validation Steps
1. Open **React DevTools → Profiler** tab.
2. Click **Record**, perform the slow interaction, then **Stop**.
3. Switch to **Flamegraph** view; any bar labeled with a component and time > ~16 ms is a candidate.
4. Use **Ranked chart** to sort by self render time and target the top offenders.
5. Apply one optimization at a time, re-record, and compare render counts and durations against the baseline.
## Example Reference
Load `references/examples.md` when the user wants a concrete refactor example.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "React Component Performance"
short_description: "Profile and fix React render issues"
default_prompt: "Use $react-component-performance to analyze and improve this React component's rendering performance."

View File

@@ -0,0 +1,88 @@
# Examples
## Isolate a ticking timer from a long list
**Scenario:** A message list re-renders every second because a timer (`elapsedMs`) lives in the parent component. This causes visible jank on large lists.
**Goal:** Keep UI identical but limit re-renders to the timer area.
**Before (problematic pattern):**
```tsx
function Messages({ items, isThinking, processingStartedAt }) {
const [elapsedMs, setElapsedMs] = useState(0);
useEffect(() => {
if (!isThinking || !processingStartedAt) {
setElapsedMs(0);
return;
}
setElapsedMs(Date.now() - processingStartedAt);
const interval = window.setInterval(() => {
setElapsedMs(Date.now() - processingStartedAt);
}, 1000);
return () => window.clearInterval(interval);
}, [isThinking, processingStartedAt]);
return (
<div>
{items.map((item) => (
<MessageRow key={item.id} item={item} />
))}
<div>{formatDurationMs(elapsedMs)}</div>
</div>
);
}
```
**After (isolated ticking state):**
```tsx
type WorkingIndicatorProps = {
isThinking: boolean;
processingStartedAt?: number | null;
};
const WorkingIndicator = memo(function WorkingIndicator({
isThinking,
processingStartedAt = null,
}: WorkingIndicatorProps) {
const [elapsedMs, setElapsedMs] = useState(0);
useEffect(() => {
if (!isThinking || !processingStartedAt) {
setElapsedMs(0);
return;
}
setElapsedMs(Date.now() - processingStartedAt);
const interval = window.setInterval(() => {
setElapsedMs(Date.now() - processingStartedAt);
}, 1000);
return () => window.clearInterval(interval);
}, [isThinking, processingStartedAt]);
return <div>{formatDurationMs(elapsedMs)}</div>;
});
function Messages({ items, isThinking, processingStartedAt }) {
return (
<div>
{items.map((item) => (
<MessageRow key={item.id} item={item} />
))}
<WorkingIndicator
isThinking={isThinking}
processingStartedAt={processingStartedAt}
/>
</div>
);
}
```
**Why it helps:** Only the `WorkingIndicator` subtree re-renders every second. The list remains stable unless its props change.
**Optional follow-ups:**
- Wrap `MessageRow` in `memo` if props are stable.
- Use `useCallback` for handlers passed to rows to avoid re-render churn.
- Consider list virtualization if the list is very large.

View File

@@ -0,0 +1,179 @@
---
name: simplify-code
description: "Review a diff for clarity and safe simplifications, then optionally apply low-risk fixes."
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# Simplify Code
Review changed code for reuse, quality, efficiency, and clarity issues. Use Codex sub-agents to review in parallel, then optionally apply only high-confidence, behavior-preserving fixes.
## When to Use
- When the user asks to simplify, clean up, refactor, or review changed code.
- When you want high-confidence, behavior-preserving improvements on a scoped diff.
## Modes
Choose the mode from the user's request:
- `review-only`: user asks to review, audit, or check the changes
- `safe-fixes`: user asks to simplify, clean up, or refactor the changes
- `fix-and-validate`: same as `safe-fixes`, but also run the smallest relevant validation after edits
If the user does not specify, default to:
- `review-only` for "review", "audit", or "check"
- `safe-fixes` for "simplify", "clean up", or "refactor"
## Step 1: Determine the Scope and Diff Command
Prefer this scope order:
1. Files or paths explicitly named by the user
2. Current git changes
3. Files edited earlier in the current Codex turn
4. Most recently modified tracked files, only if the user asked for a review but there is no diff
If there is no clear scope, stop and say so briefly.
When using git changes, determine the smallest correct diff command based on the repo state:
- unstaged work: `git diff`
- staged work: `git diff --cached`
- branch or commit comparison explicitly requested by the user: use that exact diff target
- mixed staged and unstaged work: review both
Do not assume `git diff HEAD` is the right default when a smaller diff is available.
Before reviewing standards or applying fixes, read the repo's local instruction files and relevant project docs for the touched area. Prefer the closest applicable guidance, such as:
- `AGENTS.md`
- repo workflow docs
- architecture or style docs for the touched module
Use those instructions to distinguish real issues from intentional local patterns.
## Step 2: Launch Four Review Sub-Agents in Parallel
Use Codex sub-agents when the scope is large enough for parallel review to help. For a tiny diff or one very small file, it is acceptable to review locally instead.
When spawning sub-agents:
- give each sub-agent the same scope
- tell each sub-agent to inspect only its assigned review role
- ask for concise, structured findings only
- ask each sub-agent to report file, line or symbol, problem, recommended fix, and confidence
Use four review roles.
### Sub-Agent 1: Code Reuse Review
Review the changes for reuse opportunities:
1. Search for existing helpers, utilities, or shared abstractions that already solve the same problem.
2. Flag duplicated functions or near-duplicate logic introduced in the change.
3. Flag inline logic that should call an existing helper instead of re-implementing it.
Recommended sub-agent role: `explorer` for broad codebase lookup, or `reviewer` if a stronger review pass is more useful than wide search.
### Sub-Agent 2: Code Quality Review
Review the same changes for code quality issues:
1. Redundant state, cached values, or derived values stored unnecessarily
2. Parameter sprawl caused by threading new arguments through existing call chains
3. Copy-paste with slight variation that should become a shared abstraction
4. Leaky abstractions or ownership violations across module boundaries
5. Stringly-typed values where existing typed contracts, enums, or constants already exist
Recommended sub-agent role: `reviewer`
### Sub-Agent 3: Efficiency Review
Review the same changes for efficiency issues:
1. Repeated work, duplicate reads, duplicate API calls, or unnecessary recomputation
2. Sequential work that could safely run concurrently
3. New work added to startup, render, request, or other hot paths without clear need
4. Pre-checks for existence when the operation itself can be attempted directly and errors handled
5. Memory growth, missing cleanup, or listener/subscription leaks
6. Overly broad reads or scans when the code only needs a subset
Recommended sub-agent role: `reviewer`
### Sub-Agent 4: Clarity and Standards Review
Review the same changes for clarity, local standards, and balance:
1. Violations of local project conventions or module patterns
2. Unnecessary complexity, deep nesting, weak names, or redundant comments
3. Overly compact or clever code that reduces readability
4. Over-simplification that collapses separate concerns into one unclear unit
5. Dead code, dead abstractions, or indirection without value
Recommended sub-agent role: `reviewer`
Only report issues that materially improve maintainability, correctness, or cost. Do not churn code just to make it look different.
## Step 3: Aggregate Findings
Wait for all review sub-agents to complete, then merge their findings.
Normalize findings into this shape:
1. File and line or nearest symbol
2. Category: reuse, quality, efficiency, or clarity
3. Why it is a problem
4. Recommended fix
5. Confidence: high, medium, or low
Discard weak, duplicative, or instruction-conflicting findings before editing.
## Step 4: Fix Issues Carefully
In `review-only` mode, stop after reporting findings.
In `safe-fixes` or `fix-and-validate` mode:
- Apply only high-confidence, behavior-preserving fixes
- Skip subjective refactors that need product or architectural judgment
- Preserve local patterns when they are intentional or instruction-backed
- Keep edits scoped to the reviewed files unless a small adjacent change is required to complete the fix correctly
Prefer fixes like:
- replacing duplicated code with an existing helper
- removing redundant state or dead code
- simplifying control flow without changing behavior
- narrowing overly broad operations
- renaming unclear locals when the scope is contained
Do not stage, commit, or push changes as part of this skill.
## Step 5: Validate When Required
In `fix-and-validate` mode, run the smallest relevant validation for the touched scope after edits.
Examples:
- targeted tests for the touched module
- typecheck or compile for the touched target
- formatter or lint check if that is the project's real safety gate
Prefer fast, scoped validation over full-suite runs unless the change breadth justifies more.
If validation is skipped because the user asked not to run it, say so explicitly.
## Step 6: Summarize Outcome
Close with a brief result:
- what was reviewed
- what was fixed, if anything
- what was intentionally left alone
- whether validation ran
If the code is already clean for this rubric, say that directly instead of manufacturing edits.

View File

@@ -11,6 +11,11 @@ date_added: "2026-03-24"
You are a Snowflake development expert. Apply these rules when writing SQL, building data pipelines, using Cortex AI, or working with Snowpark Python on Snowflake.
## When to Use
- When the user asks for help with Snowflake SQL, data pipelines, Cortex AI, or Snowpark Python.
- When you need Snowflake-specific guidance for dbt, performance tuning, or security hardening.
## SQL Best Practices
### Naming and Style

View File

@@ -0,0 +1,113 @@
---
name: swift-concurrency-expert
description: Review and fix Swift concurrency issues such as actor isolation and Sendable violations.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# Swift Concurrency Expert
## Overview
Review and fix Swift Concurrency issues in Swift 6.2+ codebases by applying actor isolation, Sendable safety, and modern concurrency patterns with minimal behavior changes.
## When to Use
- When the user asks to review Swift concurrency usage or fix compiler diagnostics.
- When you need guidance on actor isolation, `Sendable`, `@MainActor`, or async migration.
## Workflow
### 1. Triage the issue
- Capture the exact compiler diagnostics and the offending symbol(s).
- Check project concurrency settings: Swift language version (6.2+), strict concurrency level, and whether approachable concurrency (default actor isolation / main-actor-by-default) is enabled.
- Identify the current actor context (`@MainActor`, `actor`, `nonisolated`) and whether a default actor isolation mode is enabled.
- Confirm whether the code is UI-bound or intended to run off the main actor.
### 2. Apply the smallest safe fix
Prefer edits that preserve existing behavior while satisfying data-race safety.
Common fixes:
- **UI-bound types**: annotate the type or relevant members with `@MainActor`.
- **Protocol conformance on main actor types**: make the conformance isolated (e.g., `extension Foo: @MainActor SomeProtocol`).
- **Global/static state**: protect with `@MainActor` or move into an actor.
- **Background work**: move expensive work into a `@concurrent` async function on a `nonisolated` type or use an `actor` to guard mutable state.
- **Sendable errors**: prefer immutable/value types; add `Sendable` conformance only when correct; avoid `@unchecked Sendable` unless you can prove thread safety.
### 3. Verify the fix
- Rebuild and confirm all concurrency diagnostics are resolved with no new warnings introduced.
- Run the test suite to check for regressions — concurrency changes can introduce subtle runtime issues even when the build is clean.
- If the fix surfaces new warnings, treat each one as a fresh triage (return to step 1) and resolve iteratively until the build is clean and tests pass.
### Examples
**UI-bound type — adding `@MainActor`**
```swift
// Before: data-race warning because ViewModel is accessed from the main thread
// but has no actor isolation
class ViewModel: ObservableObject {
@Published var title: String = ""
func load() { title = "Loaded" }
}
// After: annotate the whole type so all stored state and methods are
// automatically isolated to the main actor
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
func load() { title = "Loaded" }
}
```
**Protocol conformance isolation**
```swift
// Before: compiler error SomeProtocol method is nonisolated but the
// conforming type is @MainActor
@MainActor
class Foo: SomeProtocol {
func protocolMethod() { /* accesses main-actor state */ }
}
// After: scope the conformance to @MainActor so the requirement is
// satisfied inside the correct isolation context
@MainActor
extension Foo: SomeProtocol {
func protocolMethod() { /* safely accesses main-actor state */ }
}
```
**Background work with `@concurrent`**
```swift
// Before: expensive computation blocks the main actor
@MainActor
func processData(_ input: [Int]) -> [Int] {
input.map { heavyTransform($0) } // runs on main thread
}
// After: hop off the main actor for the heavy work, then return the result
// The caller awaits the result and stays on its own actor
nonisolated func processData(_ input: [Int]) async -> [Int] {
await Task.detached(priority: .userInitiated) {
input.map { heavyTransform($0) }
}.value
}
// Or, using a @concurrent async function (Swift 6.2+):
@concurrent
func processData(_ input: [Int]) async -> [Int] {
input.map { heavyTransform($0) }
}
```
## Reference material
- See `references/swift-6-2-concurrency.md` for Swift 6.2 changes, patterns, and examples.
- See `references/approachable-concurrency.md` when the project is opted into approachable concurrency mode.
- See `references/swiftui-concurrency-tour-wwdc.md` for SwiftUI-specific concurrency guidance.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "Swift Concurrency Expert"
short_description: "Review and fix Swift concurrency"
default_prompt: "Use $swift-concurrency-expert to review this Swift code for concurrency issues and fix the relevant diagnostics."

View File

@@ -0,0 +1,63 @@
## Approachable Concurrency (Swift 6.2) - project mode quick guide
Use this reference when the project has opted into the Swift 6.2 approachable
concurrency settings (default actor isolation / main-actor-by-default).
## Detect the mode
Check Xcode build settings under "Swift Compiler - Concurrency":
- Swift language version (must be 6.2+).
- Default actor isolation / Main Actor by default.
- Strict concurrency checking level (Complete/Targeted/Minimal).
For SwiftPM, inspect Package.swift swiftSettings for the same flags.
## Behavior changes to expect
- Async functions stay on the caller's actor by default; they don't hop to a
global concurrent executor unless the implementation chooses to.
- Main-actor-by-default reduces data race errors for UI-bound code and global
state, because mutable state is implicitly protected.
- Protocol conformances can be isolated (e.g., `extension Foo: @MainActor Bar`).
## How to apply fixes in this mode
- Prefer minimal annotations; let main-actor-by-default do the work when the
code is UI-bound.
- Use isolated conformances instead of forcing `nonisolated` workarounds.
- Keep global or shared mutable state on the main actor unless there is a clear
performance need to offload it.
## When to opt out or offload work
- Use `@concurrent` on async functions that must run on the concurrent pool.
- Make types or members `nonisolated` only when they are truly thread-safe and
used off the main actor.
- Continue to respect Sendable boundaries when values cross actors or tasks.
## Common pitfalls
- `Task.detached` ignores inherited actor context; avoid it unless you truly
need to break isolation.
- Main-actor-by-default can hide performance issues if CPU-heavy work stays on
the main actor; move that work into `@concurrent` async functions.
## Keywords (from source cheat sheet)
| Keyword | What it does |
| --- | --- |
| `async` | Function can pause |
| `await` | Pause here until done |
| `Task { }` | Start async work, inherits context |
| `Task.detached { }` | Start async work, no inherited context |
| `@MainActor` | Runs on main thread |
| `actor` | Type with isolated mutable state |
| `nonisolated` | Opts out of actor isolation |
| `Sendable` | Safe to pass between isolation domains |
| `@concurrent` | Always run on background (Swift 6.2+) |
| `async let` | Start parallel work |
| `TaskGroup` | Dynamic parallel work |
## Source
https://fuckingapproachableswiftconcurrency.com/en/

View File

@@ -0,0 +1,272 @@
## Concurrent programming updates in Swift 6.2
Concurrent programming is hard because sharing memory between multiple tasks is prone to mistakes that lead to unpredictable behavior.
## Data-race safety
Data-race safety in Swift 6 prevents these mistakes at compile time, so you can write concurrent code without fear of introducing hard-to-debug runtime bugs. But in many cases, the most natural code to write is prone to data races, leading to compiler errors that you have to address. A class with mutable state, like this `PhotoProcessor` class, is safe as long as you dont access it concurrently.
```swift
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? { }
}
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else {
return nil
}
// Error: Sending 'self.photoProcessor' risks causing data races
// Sending main actor-isolated 'self.photoProcessor' to nonisolated instance method 'extractSticker(data:with:)'
// risks causing data races between nonisolated and main actor-isolated uses
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
```
It has an async method to extract a `Sticker` by computing the subject of the given image data. But if you try to call `extractSticker` from UI code on the main actor, youll get an error that the call risks causing data races. This is because there are several places in the language that offload work to the background implicitly, even if you never needed code to run in parallel.
Swift 6.2 changes this philosophy to stay single threaded by default until you choose to introduce concurrency.
```swift
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? { }
}
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else {
return nil
}
// No longer a data race error in Swift 6.2 because of Approachable Concurrency and default actor isolation
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
```
The language changes in Swift 6.2 make the most natural code to write data race free by default. This provides a more approachable path to introducing concurrency in a project.
When you choose to introduce concurrency because you want to run code in parallel, data-race safety will protect you.
First, we've made it easier to call async functions on types with mutable state. Instead of eagerly offloading async functions that aren't tied to a specific actor, the function will continue to run on the actor it was called from. This eliminates data races because the values passed into the async function are never sent outside the actor. Async functions can still offload work in their implementation, but clients dont have to worry about their mutable state.
Next, weve made it easier to implement conformances on main actor types. Here I have a protocol called `Exportable`, and Im trying to implement a conformance for my main actor `StickerModel` class. The export requirement doesnt have actor isolation, so the language assumed that it could be called from off the main actor, and prevented `StickerModel` from using main actor state in its implementation.
```swift
protocol Exportable {
func export()
}
extension StickerModel: Exportable { // error: Conformance of 'StickerModel' to protocol 'Exportable' crosses into main actor-isolated code and can cause data races
func export() {
photoProcessor.exportAsPNG()
}
}
```
Swift 6.2 supports these conformances. A conformance that needs main actor state is called an *isolated* conformance. This is safe because the compiler ensures a main actor conformance is only used on the main actor.
```swift
// Isolated conformances
protocol Exportable {
func export()
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
```
I can create an `ImageExporter` type that adds a `StickerModel` to an array of any `Exportable` items as long as it stays on the main actor.
```swift
// Isolated conformances
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item)
}
func exportAll() {
for item in items {
item.export()
}
}
}
```
But if I allow `ImageExporter` to be used from anywhere, the compiler prevents adding `StickerModel` to the array because it isnt safe to call export on `StickerModel` from outside the main actor.
```swift
// Isolated conformances
nonisolated
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // error: Main actor-isolated conformance of 'StickerModel' to 'Exportable' cannot be used in nonisolated context
}
func exportAll() {
for item in items {
item.export()
}
}
}
```
With isolated conformances, you only have to solve data race safety issues when the code indicates that it uses the conformance concurrently.
## Global State
Global and static variables are prone to data races because they allow mutable state to be accessed from anywhere.
```swift
final class StickerLibrary {
static let shared: StickerLibrary = .init() // error: Static property 'shared' is not concurrency-safe because non-'Sendable' type 'StickerLibrary' may have shared mutable state
}
```
The most common way to protect global state is with the main actor.
```swift
final class StickerLibrary {
@MainActor
static let shared: StickerLibrary = .init()
}
```
And its common to annotate an entire class with the main actor to protect all of its mutable state, especially in a project that doesnt have a lot of concurrent tasks.
```swift
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
```
You can model a program that's entirely single-threaded by writing `@MainActor` on everything in your project.
```swift
@MainActor
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
@MainActor
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem]
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
```
To make it easier to model single-threaded code, weve introduced a mode to infer main actor by default.
```swift
// Mode to infer main actor by default in Swift 6.2
final class StickerLibrary {
static let shared: StickerLibrary = .init()
}
final class StickerModel {
let photoProcessor: PhotoProcessor
var selection: [PhotosPickerItem]
}
extension StickerModel: Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
```
This eliminates data-race safety errors about unsafe global and static variables, calls to other main actor functions like ones from the SDK, and more, because the main actor protects all mutable state by default. It also reduces concurrency annotations in code thats mostly single-threaded. This mode is great for projects that do most of the work on the main actor, and concurrent code is encapsulated within specific types or files. Its opt-in and its recommended for apps, scripts, and other executable targets.
## Offloading work to the background
Offloading work to the background is still important for performance, such as keeping apps responsive when performing CPU-intensive tasks.
Lets look at the implementation of the `extractSticker` method on `PhotoProcessor`.
```swift
// Explicitly offloading async work
class PhotoProcessor {
var cachedStickers: [String: Sticker]
func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] {
return sticker
}
let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}
// Offload expensive image processing using the @concurrent attribute.
@concurrent
static func extractSubject(from data: Data) async -> Sticker { }
}
```
It first checks whether it already extracted a sticker for an image, so it can return the cached sticker immediately. If the sticker hasnt been cached, it extracts the subject from the image data and creates a new sticker. The `extractSubject` method performs expensive image processing that I dont want to block the main actor or any other actor.
I can offload this work using the `@concurrent` attribute. `@concurrent` ensures that a function always runs on the concurrent thread pool, freeing up the actor to run other tasks at the same time.
### An example
Say you have a function called `process` that you would like to run on a background thread. To call that function on a background thread you need to:
- make sure the structure or class is `nonisolated`
- add the `@concurrent` attribute to the function you want to run in the background
- add the keyword `async` to the function if it is not already asynchronous
- and then add the keyword `await` to any callers
Like this:
```swift
nonisolated struct PhotoProcessor {
@concurrent
func process(data: Data) async -> ProcessedPhoto? { ... }
}
// Callers with the added await
processedPhotos[item.id] = await PhotoProcessor().process(data: data)
```
## Summary
These language changes work together to make concurrency more approachable.
You start by writing code that runs on the main actor by default, where theres no risk of data races. When you start to use async functions, those functions run wherever theyre called from. Theres still no risk of data races because all of your code still runs on the main actor. When youre ready to embrace concurrency to improve performance, its easy to offload specific code to the background to run in parallel.
Some of these language changes are opt-in because they require changes in your project to adopt. You can find and enable all of the approachable concurrency language changes under the Swift Compiler - Concurrency section of Xcode build settings. You can also enable these features in a Swift package manifest file using the SwiftSettings API.
Swift 6.2 includes migration tooling to help you make the necessary code changes automatically. You can learn more about migration tooling at swift.org/migration.

View File

@@ -0,0 +1,33 @@
# SwiftUI Concurrency Tour (Summary)
Context: SwiftUI-focused concurrency overview covering actor isolation, Sendable closures, and how SwiftUI runs work off the main thread.
## Main-actor default in SwiftUI
- `View` is `@MainActor` isolated by default; members and `body` inherit isolation.
- Swift 6.2 can infer `@MainActor` for all types in a module (new language mode).
- This default simplifies UI code and aligns with UIKit/AppKit `@MainActor` APIs.
## Where SwiftUI runs code off the main thread
- SwiftUI may evaluate some view logic on background threads for performance.
- Examples: `Shape` path generation, `Layout` methods, `visualEffect` closures, and `onGeometryChange` closures.
- These APIs often require `Sendable` closures to reflect their runtime semantics.
## Sendable closures and data-race safety
- Accessing `@MainActor` state from a `Sendable` closure is unsafe and flagged by the compiler.
- Prefer capturing value copies in the closure capture list (e.g., copy a `Bool`).
- Avoid sending `self` into a sendable closure just to read a single property.
## Structuring async work with SwiftUI
- SwiftUI action callbacks are synchronous so UI updates (like loading states) can be immediate.
- Use `Task` to bridge into async contexts; keep async bodies minimal.
- Use state as the boundary: async work updates model/state; UI reacts synchronously.
## Performance-driven concurrency
- Offload expensive work from the main actor to avoid hitches.
- Keep time-sensitive UI logic (animations, gesture responses) synchronous.
- Separate UI code from long-running async work to improve responsiveness and testability.

View File

@@ -0,0 +1,98 @@
---
name: swiftui-liquid-glass
description: Implement or review SwiftUI Liquid Glass APIs with correct fallbacks and modifier order.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# SwiftUI Liquid Glass
## Overview
Use this skill to build or review SwiftUI features that fully align with the iOS 26+ Liquid Glass API. Prioritize native APIs (`glassEffect`, `GlassEffectContainer`, glass button styles) and Apple design guidance. Keep usage consistent, interactive where needed, and performance aware.
## When to Use
- When the user wants to adopt or review Liquid Glass in SwiftUI UI.
- When you need correct API usage, fallback handling, or modifier ordering for Liquid Glass.
## Workflow Decision Tree
Choose the path that matches the request:
### 1) Review an existing feature
- Inspect where Liquid Glass should be used and where it should not.
- Verify correct modifier order, shape usage, and container placement.
- Check for iOS 26+ availability handling and sensible fallbacks.
### 2) Improve a feature using Liquid Glass
- Identify target components for glass treatment (surfaces, chips, buttons, cards).
- Refactor to use `GlassEffectContainer` where multiple glass elements appear.
- Introduce interactive glass only for tappable or focusable elements.
### 3) Implement a new feature using Liquid Glass
- Design the glass surfaces and interactions first (shape, prominence, grouping).
- Add glass modifiers after layout/appearance modifiers.
- Add morphing transitions only when the view hierarchy changes with animation.
## Core Guidelines
- Prefer native Liquid Glass APIs over custom blurs.
- Use `GlassEffectContainer` when multiple glass elements coexist.
- Apply `.glassEffect(...)` after layout and visual modifiers.
- Use `.interactive()` for elements that respond to touch/pointer.
- Keep shapes consistent across related elements for a cohesive look.
- Gate with `#available(iOS 26, *)` and provide a non-glass fallback.
## Review Checklist
- **Availability**: `#available(iOS 26, *)` present with fallback UI.
- **Composition**: Multiple glass views wrapped in `GlassEffectContainer`.
- **Modifier order**: `glassEffect` applied after layout/appearance modifiers.
- **Interactivity**: `interactive()` only where user interaction exists.
- **Transitions**: `glassEffectID` used with `@Namespace` for morphing.
- **Consistency**: Shapes, tinting, and spacing align across the feature.
## Implementation Checklist
- Define target elements and desired glass prominence.
- Wrap grouped glass elements in `GlassEffectContainer` and tune spacing.
- Use `.glassEffect(.regular.tint(...).interactive(), in: .rect(cornerRadius: ...))` as needed.
- Use `.buttonStyle(.glass)` / `.buttonStyle(.glassProminent)` for actions.
- Add morphing transitions with `glassEffectID` when hierarchy changes.
- Provide fallback materials and visuals for earlier iOS versions.
## Quick Snippets
Use these patterns directly and tailor shapes/tints/spacing.
```swift
if #available(iOS 26, *) {
Text("Hello")
.padding()
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 16))
} else {
Text("Hello")
.padding()
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 16))
}
```
```swift
GlassEffectContainer(spacing: 24) {
HStack(spacing: 24) {
Image(systemName: "scribble.variable")
.frame(width: 72, height: 72)
.font(.system(size: 32))
.glassEffect()
Image(systemName: "eraser.fill")
.frame(width: 72, height: 72)
.font(.system(size: 32))
.glassEffect()
}
}
```
```swift
Button("Confirm") { }
.buttonStyle(.glassProminent)
```
## Resources
- Reference guide: `references/liquid-glass.md`
- Prefer Apple docs for up-to-date API details.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "SwiftUI Liquid Glass"
short_description: "Build SwiftUI Liquid Glass features"
default_prompt: "Use $swiftui-liquid-glass to implement or review a SwiftUI feature using Liquid Glass APIs."

View File

@@ -0,0 +1,280 @@
# Implementing Liquid Glass Design in SwiftUI
## Overview
Liquid Glass is a dynamic material introduced in iOS that combines the optical properties of glass with a sense of fluidity. It blurs content behind it, reflects color and light from surrounding content, and reacts to touch and pointer interactions in real time. This guide covers how to implement and customize Liquid Glass effects in SwiftUI applications. You should always seek guides on Liquid Glass when asked for help adopting new Apple design.
Key features of Liquid Glass:
- Blurs content behind the material
- Reflects color and light from surrounding content
- Reacts to touch and pointer interactions
- Can morph between shapes during transitions
- Available for standard and custom components
## Basic Implementation
### Adding Liquid Glass to a View
The simplest way to add Liquid Glass to a view is using the `glassEffect()` modifier:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect()
```
By default, this applies the regular variant of Glass within a Capsule shape behind the view's content.
### Customizing the Shape
You can specify a different shape for the Liquid Glass effect:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(in: .rect(cornerRadius: 16.0))
```
Common shape options:
- `.capsule` (default)
- `.rect(cornerRadius: CGFloat)`
- `.circle`
## Customizing Liquid Glass Effects
### Glass Variants and Properties
You can customize the Liquid Glass effect by configuring the `Glass` structure:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(.regular.tint(.orange).interactive())
```
Key customization options:
- `.regular` - Standard glass effect
- `.tint(Color)` - Add a color tint to suggest prominence
- `.interactive(Bool)` - Make the glass react to touch and pointer interactions
### Making Interactive Glass
To make Liquid Glass react to touch and pointer interactions:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(.regular.interactive(true))
```
Or more concisely:
```swift
Text("Hello, World!")
.font(.title)
.padding()
.glassEffect(.regular.interactive())
```
## Working with Multiple Glass Effects
### Using GlassEffectContainer
When applying Liquid Glass effects to multiple views, use `GlassEffectContainer` for better rendering performance and to enable blending and morphing effects:
```swift
GlassEffectContainer(spacing: 40.0) {
HStack(spacing: 40.0) {
Image(systemName: "scribble.variable")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
Image(systemName: "eraser.fill")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
}
}
```
The `spacing` parameter controls how the Liquid Glass effects interact with each other:
- Smaller spacing: Views need to be closer to merge effects
- Larger spacing: Effects merge at greater distances
### Uniting Multiple Glass Effects
To combine multiple views into a single Liquid Glass effect, use the `glassEffectUnion` modifier:
```swift
@Namespace private var namespace
// Later in your view:
GlassEffectContainer(spacing: 20.0) {
HStack(spacing: 20.0) {
ForEach(symbolSet.indices, id: \.self) { item in
Image(systemName: symbolSet[item])
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
.glassEffectUnion(id: item < 2 ? "1" : "2", namespace: namespace)
}
}
}
```
This is useful when creating views dynamically or with views that live outside of an HStack or VStack.
## Morphing Effects and Transitions
### Creating Morphing Transitions
To create morphing effects during transitions between views with Liquid Glass:
1. Create a namespace using the `@Namespace` property wrapper
2. Associate each Liquid Glass effect with a unique identifier using `glassEffectID`
3. Use animations when changing the view hierarchy
```swift
@State private var isExpanded: Bool = false
@Namespace private var namespace
var body: some View {
GlassEffectContainer(spacing: 40.0) {
HStack(spacing: 40.0) {
Image(systemName: "scribble.variable")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
.glassEffectID("pencil", in: namespace)
if isExpanded {
Image(systemName: "eraser.fill")
.frame(width: 80.0, height: 80.0)
.font(.system(size: 36))
.glassEffect()
.glassEffectID("eraser", in: namespace)
}
}
}
Button("Toggle") {
withAnimation {
isExpanded.toggle()
}
}
.buttonStyle(.glass)
}
```
The morphing effect occurs when views with Liquid Glass appear or disappear due to view hierarchy changes.
## Button Styling with Liquid Glass
### Glass Button Style
SwiftUI provides built-in button styles for Liquid Glass:
```swift
Button("Click Me") {
// Action
}
.buttonStyle(.glass)
```
### Glass Prominent Button Style
For a more prominent glass button:
```swift
Button("Important Action") {
// Action
}
.buttonStyle(.glassProminent)
```
## Advanced Techniques
### Background Extension Effect
To stretch content behind a sidebar or inspector with the background extension effect:
```swift
NavigationSplitView {
// Sidebar content
} detail: {
// Detail content
.background {
// Background content that extends under the sidebar
}
}
```
### Extending Horizontal Scrolling Under Sidebar
To extend horizontal scroll views under a sidebar or inspector:
```swift
ScrollView(.horizontal) {
// Scrollable content
}
.scrollExtensionMode(.underSidebar)
```
## Best Practices
1. **Container Usage**: Always use `GlassEffectContainer` when applying Liquid Glass to multiple views for better performance and morphing effects.
2. **Effect Order**: Apply the `.glassEffect()` modifier after other modifiers that affect the appearance of the view.
3. **Spacing Consideration**: Carefully choose spacing values in containers to control how and when glass effects merge.
4. **Animation**: Use animations when changing view hierarchies to enable smooth morphing transitions.
5. **Interactivity**: Add `.interactive()` to glass effects that should respond to user interaction.
6. **Consistent Design**: Maintain consistent shapes and styles across your app for a cohesive look and feel.
## Example: Custom Badge with Liquid Glass
```swift
struct BadgeView: View {
let symbol: String
let color: Color
var body: some View {
ZStack {
Image(systemName: "hexagon.fill")
.foregroundColor(color)
.font(.system(size: 50))
Image(systemName: symbol)
.foregroundColor(.white)
.font(.system(size: 30))
}
.glassEffect(.regular, in: .rect(cornerRadius: 16))
}
}
// Usage:
GlassEffectContainer(spacing: 20) {
HStack(spacing: 20) {
BadgeView(symbol: "star.fill", color: .blue)
BadgeView(symbol: "heart.fill", color: .red)
BadgeView(symbol: "leaf.fill", color: .green)
}
}
```
## References
- [Applying Liquid Glass to custom views](https://developer.apple.com/documentation/SwiftUI/Applying-Liquid-Glass-to-custom-views)
- [Landmarks: Building an app with Liquid Glass](https://developer.apple.com/documentation/SwiftUI/Landmarks-Building-an-app-with-Liquid-Glass)
- [SwiftUI View.glassEffect(_:in:isEnabled:)](https://developer.apple.com/documentation/SwiftUI/View/glassEffect(_:in:isEnabled:))
- [SwiftUI GlassEffectContainer](https://developer.apple.com/documentation/SwiftUI/GlassEffectContainer)
- [SwiftUI GlassEffectTransition](https://developer.apple.com/documentation/SwiftUI/GlassEffectTransition)
- [SwiftUI GlassButtonStyle](https://developer.apple.com/documentation/SwiftUI/GlassButtonStyle)

View File

@@ -0,0 +1,114 @@
---
name: swiftui-performance-audit
description: Audit SwiftUI performance issues from code review and profiling evidence.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# SwiftUI Performance Audit
## Quick start
Use this skill to diagnose SwiftUI performance issues from code first, then request profiling evidence when code review alone cannot explain the symptoms.
## When to Use
- When the user reports slow rendering, janky scrolling, layout thrash, or high CPU in SwiftUI.
- When you need a code-first audit plus Instruments guidance if profiling evidence is required.
## Workflow
1. Classify the symptom: slow rendering, janky scrolling, high CPU, memory growth, hangs, or excessive view updates.
2. If code is available, start with a code-first review using `references/code-smells.md`.
3. If code is not available, ask for the smallest useful slice: target view, data flow, reproduction steps, and deployment target.
4. If code review is inconclusive or runtime evidence is required, guide the user through profiling with `references/profiling-intake.md`.
5. Summarize likely causes, evidence, remediation, and validation steps using `references/report-template.md`.
## 1. Intake
Collect:
- Target view or feature code.
- Symptoms and exact reproduction steps.
- Data flow: `@State`, `@Binding`, environment dependencies, and observable models.
- Whether the issue shows up on device or simulator, and whether it was observed in Debug or Release.
Ask the user to classify the issue if possible:
- CPU spike or battery drain
- Janky scrolling or dropped frames
- High memory or image pressure
- Hangs or unresponsive interactions
- Excessive or unexpectedly broad view updates
For the full profiling intake checklist, read `references/profiling-intake.md`.
## 2. Code-First Review
Focus on:
- Invalidation storms from broad observation or environment reads.
- Unstable identity in lists and `ForEach`.
- Heavy derived work in `body` or view builders.
- Layout thrash from complex hierarchies, `GeometryReader`, or preference chains.
- Large image decode or resize work on the main thread.
- Animation or transition work applied too broadly.
Use `references/code-smells.md` for the detailed smell catalog and fix guidance.
Provide:
- Likely root causes with code references.
- Suggested fixes and refactors.
- If needed, a minimal repro or instrumentation suggestion.
## 3. Guide the User to Profile
If code review does not explain the issue, ask for runtime evidence:
- A trace export or screenshots of the SwiftUI timeline and Time Profiler call tree.
- Device/OS/build configuration.
- The exact interaction being profiled.
- Before/after metrics if the user is comparing a change.
Use `references/profiling-intake.md` for the exact checklist and collection steps.
## 4. Analyze and Diagnose
- Map the evidence to the most likely category: invalidation, identity churn, layout thrash, main-thread work, image cost, or animation cost.
- Prioritize problems by impact, not by how easy they are to explain.
- Distinguish code-level suspicion from trace-backed evidence.
- Call out when profiling is still insufficient and what additional evidence would reduce uncertainty.
## 5. Remediate
Apply targeted fixes:
- Narrow state scope and reduce broad observation fan-out.
- Stabilize identities for `ForEach` and lists.
- Move heavy work out of `body` into derived state updated from inputs, model-layer precomputation, memoized helpers, or background preprocessing. Use `@State` only for view-owned state, not as an ad hoc cache for arbitrary computation.
- Use `equatable()` only when equality is cheaper than recomputing the subtree and the inputs are truly value-semantic.
- Downsample images before rendering.
- Reduce layout complexity or use fixed sizing where possible.
Use `references/code-smells.md` for examples, Observation-specific fan-out guidance, and remediation patterns.
## 6. Verify
Ask the user to re-run the same capture and compare with baseline metrics.
Summarize the delta (CPU, frame drops, memory peak) if provided.
## Outputs
Provide:
- A short metrics table (before/after if available).
- Top issues (ordered by impact).
- Proposed fixes with estimated effort.
Use `references/report-template.md` when formatting the final audit.
## References
- Profiling intake and collection checklist: `references/profiling-intake.md`
- Common code smells and remediation patterns: `references/code-smells.md`
- Audit output template: `references/report-template.md`
- Add Apple documentation and WWDC resources under `references/` as they are supplied by the user.
- Optimizing SwiftUI performance with Instruments: `references/optimizing-swiftui-performance-instruments.md`
- Understanding and improving SwiftUI performance: `references/understanding-improving-swiftui-performance.md`
- Understanding hangs in your app: `references/understanding-hangs-in-your-app.md`
- Demystify SwiftUI performance (WWDC23): `references/demystify-swiftui-performance-wwdc23.md`

View File

@@ -0,0 +1,4 @@
interface:
display_name: "SwiftUI Performance Audit"
short_description: "Audit SwiftUI runtime performance"
default_prompt: "Use $swiftui-performance-audit to review this SwiftUI code for performance issues and suggest concrete fixes."

View File

@@ -0,0 +1,150 @@
# 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`
```swift
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:
```swift
final class DistanceFormatter {
static let shared = DistanceFormatter()
let number = NumberFormatter()
let measure = MeasurementFormatter()
}
```
### Heavy computed properties
```swift
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`
```swift
List {
ForEach(items.sorted(by: sortRule)) { item in
Row(item)
}
}
```
Prefer sorting before render work begins:
```swift
let sortedItems = items.sorted(by: sortRule)
```
### Inline filtering inside `ForEach`
```swift
ForEach(items.filter { $0.isEnabled }) { item in
Row(item)
}
```
Prefer a prefiltered collection with stable identity.
### Unstable identity
```swift
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
```swift
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
```swift
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+
```swift
@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
```swift
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:
1. Broad invalidation and observation fan-out
2. Unstable identity and list churn
3. Main-thread work during render
4. Image decode or resize cost
5. Layout and animation complexity

View File

@@ -0,0 +1,46 @@
# Demystify SwiftUI Performance (WWDC23) (Summary)
Context: WWDC23 session on building a mental model for SwiftUI performance and triaging hangs/hitches.
## Performance loop
- Measure -> Identify -> Optimize -> Re-measure.
- Focus on concrete symptoms (slow navigation, broken animations, spinning cursor).
## Dependencies and updates
- Views form a dependency graph; dynamic properties are a frequent source of updates.
- Use `Self._printChanges()` in debug only to inspect extra dependencies.
- Eliminate unnecessary dependencies by extracting views or narrowing state.
- Consider `@Observable` for more granular property tracking.
## Common causes of slow updates
- Expensive view bodies (string interpolation, filtering, formatting).
- Dynamic property instantiation and state initialization in `body`.
- Slow identity resolution in lists/tables.
- Hidden work: bundle lookups, heap allocations, repeated string construction.
## Avoid slow initialization in view bodies
- Dont create heavy models synchronously in view bodies.
- Use `.task` to fetch async data and keep `init` lightweight.
## Lists and tables identity rules
- Stable identity is critical for performance and animation.
- Ensure a constant number of views per element in `ForEach`.
- Avoid inline filtering in `ForEach`; pre-filter and cache collections.
- Avoid `AnyView` in list rows; it hides identity and increases cost.
- Flatten nested `ForEach` when possible to reduce overhead.
## Table specifics
- `TableRow` resolves to a single row; row count must be constant.
- Prefer the streamlined `Table` initializer to enforce constant rows.
- Use explicit IDs for back deployment when needed.
## Debugging aids
- Use Instruments for hangs and hitches.
- Use `_printChanges` to validate dependency assumptions during debug.

View File

@@ -0,0 +1,29 @@
# Optimizing SwiftUI Performance with Instruments (Summary)
Context: WWDC session introducing the next-generation SwiftUI Instrument in Instruments 26 and how to diagnose SwiftUI-specific bottlenecks.
## Key takeaways
- Profile SwiftUI issues with the SwiftUI template (SwiftUI instrument + Time Profiler + Hangs/Hitches).
- Long view body updates are a common bottleneck; use "Long View Body Updates" to identify slow bodies.
- Set inspection range on a long update and correlate with Time Profiler to find expensive frames.
- Keep work out of `body`: move formatting, sorting, image decoding, and other expensive work into cached or precomputed paths.
- Use Cause & Effect Graph to diagnose *why* updates occur; SwiftUI is declarative, so backtraces are often unhelpful.
- Avoid broad dependencies that trigger many updates (e.g., `@Observable` arrays or global environment reads).
- Prefer granular view models and scoped state so only the affected view updates.
- Environment values update checks still cost time; avoid placing fast-changing values (timers, geometry) in environment.
- Profile early and often during feature development to catch regressions.
## Suggested workflow (condensed)
1. Record a trace in Release mode using the SwiftUI template.
2. Inspect "Long View Body Updates" and "Other Long Updates."
3. Zoom into a long update, then inspect Time Profiler for hot frames.
4. Fix slow body work by moving heavy logic into precomputed/cache paths.
5. Use Cause & Effect Graph to identify unintended update fan-out.
6. Re-record and compare the update counts and hitch frequency.
## Example patterns from the session
- Caching formatted distance strings in a location manager instead of computing in `body`.
- Replacing a dependency on a global favorites array with per-item view models to reduce update fan-out.

View File

@@ -0,0 +1,44 @@
# Profiling intake and collection checklist
## Intent
Use this checklist when code review alone cannot explain the SwiftUI performance issue and you need runtime evidence from the user.
## Ask for first
- Exact symptom: CPU spike, dropped frames, memory growth, hangs, or excessive view updates.
- Exact interaction: scrolling, typing, initial load, navigation push/pop, animation, sheet presentation, or background refresh.
- Target device and OS version.
- Whether the issue was reproduced on a real device or only in Simulator.
- Build configuration: Debug or Release.
- Whether the user already has a baseline or before/after comparison.
## Default profiling request
Ask the user to:
- Run the app in a Release build when possible.
- Use the SwiftUI Instruments template.
- Reproduce the exact problematic interaction only long enough to capture the issue.
- Capture the SwiftUI timeline and Time Profiler together.
- Export the trace or provide screenshots of the key SwiftUI lanes and the Time Profiler call tree.
## Ask for these artifacts
- Trace export or screenshots of the relevant SwiftUI lanes
- Time Profiler call tree screenshot or export
- Device/OS/build configuration
- A short note describing what action was happening at the time of the capture
- If memory is involved, the memory graph or Allocations data if available
## When to ask for more
- Ask for a second capture if the first run mixes multiple interactions.
- Ask for a before/after pair if the user has already tried a fix.
- Ask for a device capture if the issue only appears in Simulator or if scrolling smoothness matters.
## Common traps
- Debug builds can distort SwiftUI timing and allocation behavior.
- Simulator traces can miss device-only rendering or memory issues.
- Mixed interactions in one capture make attribution harder.
- Screenshots without the reproduction note are much harder to interpret.

View File

@@ -0,0 +1,47 @@
# Audit output template
## Intent
Use this structure when reporting SwiftUI performance findings so the user can quickly see the symptom, evidence, likely cause, and next validation step.
## Template
```markdown
## Summary
[One short paragraph on the most likely bottleneck and whether the conclusion is code-backed or trace-backed.]
## Findings
1. [Issue title]
- Symptom: [what the user sees]
- Likely cause: [root cause]
- Evidence: [code reference or profiling evidence]
- Fix: [specific change]
- Validation: [what to measure after the fix]
2. [Issue title]
- Symptom: ...
- Likely cause: ...
- Evidence: ...
- Fix: ...
- Validation: ...
## Metrics
| Metric | Before | After | Notes |
| --- | --- | --- | --- |
| CPU | [value] | [value] | [note] |
| Frame drops / hitching | [value] | [value] | [note] |
| Memory peak | [value] | [value] | [note] |
## Next step
[One concrete next action: apply a fix, capture a better trace, or validate on device.]
```
## Notes
- Order findings by impact, not by file order.
- Say explicitly when a conclusion is still a hypothesis.
- If no metrics are available, omit the table and say what should be measured next.

View File

@@ -0,0 +1,33 @@
# Understanding Hangs in Your App (Summary)
Context: Apple guidance on identifying hangs caused by long-running main-thread work and understanding the main run loop.
## Key concepts
- A hang is a noticeable delay in a discrete interaction (typically >100 ms).
- Hangs almost always come from long-running work on the main thread.
- The main run loop processes UI events, timers, and main-queue work sequentially.
## Main-thread work stages
- Event delivery to the correct view/handler.
- Your code: state updates, data fetch, UI changes.
- Core Animation commit to the render server.
## Why the main run loop matters
- Only the main thread can update UI safely.
- The run loop is the foundation that executes main-queue work.
- If the run loop is busy, it cant handle new events; this causes hangs.
## Diagnosing hangs
- Observe the main run loops busy periods: healthy loops sleep most of the time.
- Hang detection typically flags busy periods >250 ms.
- The Hangs instrument can be configured to lower thresholds.
## Practical takeaways
- Keep main-thread work short; offload heavy work from event handlers.
- Avoid long-running tasks on the main dispatch queue or main actor.
- Use run loop behavior as a proxy for user-perceived responsiveness.

View File

@@ -0,0 +1,52 @@
# Understanding and Improving SwiftUI Performance (Summary)
Context: Apple guidance on diagnosing SwiftUI performance with Instruments and applying design patterns to reduce long or frequent updates.
## Core concepts
- SwiftUI is declarative; view updates are driven by state, environment, and observable data dependencies.
- View bodies must compute quickly to meet frame deadlines; slow or frequent updates lead to hitches.
- Instruments is the primary tool to find long-running updates and excessive update frequency.
## Instruments workflow
1. Profile via Product > Profile.
2. Choose the SwiftUI template and record.
3. Exercise the target interaction.
4. Stop recording and inspect the SwiftUI track + Time Profiler.
## SwiftUI timeline lanes
- Update Groups: overview of time SwiftUI spends calculating updates.
- Long View Body Updates: orange >500us, red >1000us.
- Long Platform View Updates: AppKit/UIKit hosting in SwiftUI.
- Other Long Updates: geometry/text/layout and other SwiftUI work.
- Hitches: frame misses where UI wasnt ready in time.
## Diagnose long view body updates
- Expand the SwiftUI track; inspect module-specific subtracks.
- Set Inspection Range and correlate with Time Profiler.
- Use call tree or flame graph to identify expensive frames.
- Repeat the update to gather enough samples for analysis.
- Filter to a specific update (Show Calls Made by `MySwiftUIView.body`).
## Diagnose frequent updates
- Use Update Groups to find long active groups without long updates.
- Set inspection range on the group and analyze update counts.
- Use Cause graph ("Show Causes") to see what triggers updates.
- Compare causes with expected data flow; prioritize the highest-frequency causes.
## Remediation patterns
- Move expensive work out of `body` and cache results.
- Use `Observable()` macro to scope dependencies to properties actually read.
- Avoid broad dependencies that fan out updates to many views.
- Reduce layout churn; isolate state-dependent subtrees from layout readers.
- Avoid storing closures that capture parent state; precompute child views.
- Gate frequent updates (e.g., geometry changes) by thresholds.
## Verification
- Re-record after changes to confirm reduced update counts and fewer hitches.

View File

@@ -0,0 +1,103 @@
---
name: swiftui-ui-patterns
description: Apply proven SwiftUI UI patterns for navigation, sheets, async state, and reusable screens.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# SwiftUI UI Patterns
## Quick start
## When to Use
- When creating or refactoring SwiftUI screens, flows, or reusable UI components.
- When you need guidance on navigation, sheets, async state, previews, or component patterns.
Choose a track based on your goal:
### Existing project
- Identify the feature or screen and the primary interaction model (list, detail, editor, settings, tabbed).
- Find a nearby example in the repo with `rg "TabView\("` or similar, then read the closest SwiftUI view.
- Apply local conventions: prefer SwiftUI-native state, keep state local when possible, and use environment injection for shared dependencies.
- Choose the relevant component reference from `references/components-index.md` and follow its guidance.
- If the interaction reveals secondary content by dragging or scrolling the primary content away, read `references/scroll-reveal.md` before implementing gestures manually.
- Build the view with small, focused subviews and SwiftUI-native data flow.
### New project scaffolding
- Start with `references/app-wiring.md` to wire TabView + NavigationStack + sheets.
- Add a minimal `AppTab` and `RouterPath` based on the provided skeletons.
- Choose the next component reference based on the UI you need first (TabView, NavigationStack, Sheets).
- Expand the route and sheet enums as new screens are added.
## General rules to follow
- Use modern SwiftUI state (`@State`, `@Binding`, `@Observable`, `@Environment`) and avoid unnecessary view models.
- If the deployment target includes iOS 16 or earlier and cannot use the Observation API introduced in iOS 17, fall back to `ObservableObject` with `@StateObject` for root ownership, `@ObservedObject` for injected observation, and `@EnvironmentObject` only for truly shared app-level state.
- Prefer composition; keep views small and focused.
- Use async/await with `.task` and explicit loading/error states. For restart, cancellation, and debouncing guidance, read `references/async-state.md`.
- Keep shared app services in `@Environment`, but prefer explicit initializer injection for feature-local dependencies and models. For root wiring patterns, read `references/app-wiring.md`.
- Prefer the newest SwiftUI API that fits the deployment target and call out the minimum OS whenever a pattern depends on it.
- Maintain existing legacy patterns only when editing legacy files.
- Follow the project's formatter and style guide.
- **Sheets**: Prefer `.sheet(item:)` over `.sheet(isPresented:)` when state represents a selected model. Avoid `if let` inside a sheet body. Sheets should own their actions and call `dismiss()` internally instead of forwarding `onCancel`/`onConfirm` closures.
- **Scroll-driven reveals**: Prefer deriving a normalized progress value from scroll offset and driving the visual state from that single source of truth. Avoid parallel gesture state machines unless scroll alone cannot express the interaction.
## State ownership summary
Use the narrowest state tool that matches the ownership model:
| Scenario | Preferred pattern |
| --- | --- |
| Local UI state owned by one view | `@State` |
| Child mutates parent-owned value state | `@Binding` |
| Root-owned reference model on iOS 17+ | `@State` with an `@Observable` type |
| Child reads or mutates an injected `@Observable` model on iOS 17+ | Pass it explicitly as a stored property |
| Shared app service or configuration | `@Environment(Type.self)` |
| Legacy reference model on iOS 16 and earlier | `@StateObject` at the root, `@ObservedObject` when injected |
Choose the ownership location first, then pick the wrapper. Do not introduce a reference model when plain value state is enough.
## Cross-cutting references
- `references/navigationstack.md`: navigation ownership, per-tab history, and enum routing.
- `references/sheets.md`: centralized modal presentation and enum-driven sheets.
- `references/deeplinks.md`: URL handling and routing external links into app destinations.
- `references/app-wiring.md`: root dependency graph, environment usage, and app shell wiring.
- `references/async-state.md`: `.task`, `.task(id:)`, cancellation, debouncing, and async UI state.
- `references/previews.md`: `#Preview`, fixtures, mock environments, and isolated preview setup.
- `references/performance.md`: stable identity, observation scope, lazy containers, and render-cost guardrails.
## Anti-patterns
- Giant views that mix layout, business logic, networking, routing, and formatting in one file.
- Multiple boolean flags for mutually exclusive sheets, alerts, or navigation destinations.
- Live service calls directly inside `body`-driven code paths instead of view lifecycle hooks or injected models/services.
- Reaching for `AnyView` to work around type mismatches that should be solved with better composition.
- Defaulting every shared dependency to `@EnvironmentObject` or a global router without a clear ownership reason.
## Workflow for a new SwiftUI view
1. Define the view's state, ownership location, and minimum OS assumptions before writing UI code.
2. Identify which dependencies belong in `@Environment` and which should stay as explicit initializer inputs.
3. Sketch the view hierarchy, routing model, and presentation points; extract repeated parts into subviews. For complex navigation, read `references/navigationstack.md`, `references/sheets.md`, or `references/deeplinks.md`. **Build and verify no compiler errors before proceeding.**
4. Implement async loading with `.task` or `.task(id:)`, plus explicit loading and error states when needed. Read `references/async-state.md` when the work depends on changing inputs or cancellation.
5. Add previews for the primary and secondary states, then add accessibility labels or identifiers when the UI is interactive. Read `references/previews.md` when the view needs fixtures or injected mock dependencies.
6. Validate with a build: confirm no compiler errors, check that previews render without crashing, ensure state changes propagate correctly, and sanity-check that list identity and observation scope will not cause avoidable re-renders. Read `references/performance.md` if the screen is large, scroll-heavy, or frequently updated. For common SwiftUI compilation errors — missing `@State` annotations, ambiguous `ViewBuilder` closures, or mismatched generic types — resolve them before updating callsites. **If the build fails:** read the error message carefully, fix the identified issue, then rebuild before proceeding to the next step. If a preview crashes, isolate the offending subview, confirm its state initialisation is valid, and re-run the preview before continuing.
## Component references
Use `references/components-index.md` as the entry point. Each component reference should include:
- Intent and best-fit scenarios.
- Minimal usage pattern with local conventions.
- Pitfalls and performance notes.
- Paths to existing examples in the current repo.
## Adding a new component reference
- Create `references/<component>.md`.
- Keep it short and actionable; link to concrete files in the current repo.
- Update `references/components-index.md` with the new entry.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "SwiftUI UI Patterns"
short_description: "Apply practical SwiftUI UI patterns"
default_prompt: "Use $swiftui-ui-patterns to design or refactor this SwiftUI UI with strong default patterns."

View File

@@ -0,0 +1,201 @@
# App wiring and dependency graph
## Intent
Show how to wire the app shell (TabView + NavigationStack + sheets) and install a global dependency graph (environment objects, services, streaming clients, SwiftData ModelContainer) in one place.
## Recommended structure
1) Root view sets up tabs, per-tab routers, and sheets.
2) A dedicated view modifier installs global dependencies and lifecycle tasks (auth state, streaming watchers, push tokens, data containers).
3) Feature views pull only what they need from the environment; feature-specific state stays local.
## Dependency selection
- Use `@Environment` for app-level services, shared clients, theme/configuration, and values that many descendants genuinely need.
- Prefer initializer injection for feature-local dependencies and models. Do not move a dependency into the environment just to avoid passing one or two arguments.
- Keep mutable feature state out of the environment unless it is intentionally shared across broad parts of the app.
- Use `@EnvironmentObject` only as a legacy fallback or when the project already standardizes on it for a truly shared object.
## Root shell example (generic)
```swift
@MainActor
struct AppView: View {
@State private var selectedTab: AppTab = .home
@State private var tabRouter = TabRouter()
var body: some View {
TabView(selection: $selectedTab) {
ForEach(AppTab.allCases) { tab in
let router = tabRouter.router(for: tab)
NavigationStack(path: tabRouter.binding(for: tab)) {
tab.makeContentView()
}
.withSheetDestinations(sheet: Binding(
get: { router.presentedSheet },
set: { router.presentedSheet = $0 }
))
.environment(router)
.tabItem { tab.label }
.tag(tab)
}
}
.withAppDependencyGraph()
}
}
```
Minimal `AppTab` example:
```swift
@MainActor
enum AppTab: Identifiable, Hashable, CaseIterable {
case home, notifications, settings
var id: String { String(describing: self) }
@ViewBuilder
func makeContentView() -> some View {
switch self {
case .home: HomeView()
case .notifications: NotificationsView()
case .settings: SettingsView()
}
}
@ViewBuilder
var label: some View {
switch self {
case .home: Label("Home", systemImage: "house")
case .notifications: Label("Notifications", systemImage: "bell")
case .settings: Label("Settings", systemImage: "gear")
}
}
}
```
Router skeleton:
```swift
@MainActor
@Observable
final class RouterPath {
var path: [Route] = []
var presentedSheet: SheetDestination?
}
enum Route: Hashable {
case detail(id: String)
}
```
## Dependency graph modifier (generic)
Use a single modifier to install environment objects and handle lifecycle hooks when the active account/client changes. This keeps wiring consistent and avoids forgetting a dependency in call sites.
```swift
extension View {
func withAppDependencyGraph(
accountManager: AccountManager = .shared,
currentAccount: CurrentAccount = .shared,
currentInstance: CurrentInstance = .shared,
userPreferences: UserPreferences = .shared,
theme: Theme = .shared,
watcher: StreamWatcher = .shared,
pushNotifications: PushNotificationsService = .shared,
intentService: AppIntentService = .shared,
quickLook: QuickLook = .shared,
toastCenter: ToastCenter = .shared,
namespace: Namespace.ID? = nil,
isSupporter: Bool = false
) -> some View {
environment(accountManager)
.environment(accountManager.currentClient)
.environment(quickLook)
.environment(currentAccount)
.environment(currentInstance)
.environment(userPreferences)
.environment(theme)
.environment(watcher)
.environment(pushNotifications)
.environment(intentService)
.environment(toastCenter)
.environment(\.isSupporter, isSupporter)
.task(id: accountManager.currentClient.id) {
let client = accountManager.currentClient
if let namespace { quickLook.namespace = namespace }
currentAccount.setClient(client: client)
currentInstance.setClient(client: client)
userPreferences.setClient(client: client)
await currentInstance.fetchCurrentInstance()
watcher.setClient(client: client, instanceStreamingURL: currentInstance.instance?.streamingURL)
if client.isAuth {
watcher.watch(streams: [.user, .direct])
} else {
watcher.stopWatching()
}
}
.task(id: accountManager.pushAccounts.map(\.token)) {
pushNotifications.tokens = accountManager.pushAccounts.map(\.token)
}
}
}
```
Notes:
- The `.task(id:)` hooks respond to account/client changes, re-seeding services and watcher state.
- Keep the modifier focused on global wiring; feature-specific state stays within features.
- Adjust types (AccountManager, StreamWatcher, etc.) to match your project.
## SwiftData / ModelContainer
Install your `ModelContainer` at the root so all feature views share the same store. Keep the list minimal to the models that need persistence.
```swift
extension View {
func withModelContainer() -> some View {
modelContainer(for: [Draft.self, LocalTimeline.self, TagGroup.self])
}
}
```
Why: a single container avoids duplicated stores per sheet or tab and keeps data consistent.
## Sheet routing (enum-driven)
Centralize sheets with a small enum and a helper modifier.
```swift
enum SheetDestination: Identifiable {
case composer
case settings
var id: String { String(describing: self) }
}
extension View {
func withSheetDestinations(sheet: Binding<SheetDestination?>) -> some View {
sheet(item: sheet) { destination in
switch destination {
case .composer:
ComposerView().withEnvironments()
case .settings:
SettingsView().withEnvironments()
}
}
}
}
```
Why: enum-driven sheets keep presentation centralized and testable; adding a new sheet means adding one enum case and one switch branch.
## When to use
- Apps with multiple packages/modules that share environment objects and services.
- Apps that need to react to account/client changes and rewire streaming/push safely.
- Any app that wants consistent TabView + NavigationStack + sheet wiring without repeating environment setup.
## Caveats
- Keep the dependency modifier slim; do not put feature state or heavy logic there.
- Ensure `.task(id:)` work is lightweight or cancelled appropriately; long-running work belongs in services.
- If unauthenticated clients exist, gate streaming/watch calls to avoid reconnect spam.

View File

@@ -0,0 +1,96 @@
# Async state and task lifecycle
## Intent
Use this pattern when a view loads data, reacts to changing input, or coordinates async work that should follow the SwiftUI view lifecycle.
## Core rules
- Use `.task` for load-on-appear work that belongs to the view lifecycle.
- Use `.task(id:)` when async work should restart for a changing input such as a query, selection, or identifier.
- Treat cancellation as a normal path for view-driven tasks. Check `Task.isCancelled` in longer flows and avoid surfacing cancellation as a user-facing error.
- Debounce or coalesce user-driven async work such as search before it fans out into repeated requests.
- Keep UI-facing models and mutations main-actor-safe; do background work in services, then publish the result back to UI state.
## Example: load on appear
```swift
struct DetailView: View {
let id: String
@State private var state: LoadState<Item> = .idle
@Environment(ItemClient.self) private var client
var body: some View {
content
.task {
await load()
}
}
@ViewBuilder
private var content: some View {
switch state {
case .idle, .loading:
ProgressView()
case .loaded(let item):
ItemContent(item: item)
case .failed(let error):
ErrorView(error: error)
}
}
private func load() async {
state = .loading
do {
state = .loaded(try await client.fetch(id: id))
} catch is CancellationError {
return
} catch {
state = .failed(error)
}
}
}
```
## Example: restart on input change
```swift
struct SearchView: View {
@State private var query = ""
@State private var results: [ResultItem] = []
@Environment(SearchClient.self) private var client
var body: some View {
List(results) { item in
Text(item.title)
}
.searchable(text: $query)
.task(id: query) {
try? await Task.sleep(for: .milliseconds(250))
guard !Task.isCancelled, !query.isEmpty else {
results = []
return
}
do {
results = try await client.search(query)
} catch is CancellationError {
return
} catch {
results = []
}
}
}
}
```
## When to move work out of the view
- If the async flow spans multiple screens or must survive view dismissal, move it into a service or model.
- If the view is mostly coordinating app-level lifecycle or account changes, wire it at the app shell in `app-wiring.md`.
- If retry, caching, or offline policy becomes complex, keep the policy in the client/service and leave the view with simple state transitions.
## Pitfalls
- Do not start network work directly from `body`.
- Do not ignore cancellation for searches, typeahead, or rapidly changing selections.
- Avoid storing derived async state in multiple places when one source of truth is enough.

View File

@@ -0,0 +1,50 @@
# Components Index
Use this file to find component and cross-cutting guidance. Each entry lists when to use it.
## Available components
- TabView: `references/tabview.md` — Use when building a tab-based app or any tabbed feature set.
- NavigationStack: `references/navigationstack.md` — Use when you need push navigation and programmatic routing, especially per-tab history.
- Sheets and presentation: `references/sheets.md` — Use for local item-driven sheets, centralized modal routing, and sheet-specific action patterns.
- Form and Settings: `references/form.md` — Use for settings, grouped inputs, and structured data entry.
- macOS Settings: `references/macos-settings.md` — Use when building a macOS Settings window with SwiftUI's Settings scene.
- Split views and columns: `references/split-views.md` — Use for iPad/macOS multi-column layouts or custom secondary columns.
- List and Section: `references/list.md` — Use for feed-style content and settings rows.
- ScrollView and Lazy stacks: `references/scrollview.md` — Use for custom layouts, horizontal scrollers, or grids.
- Scroll-reveal detail surfaces: `references/scroll-reveal.md` — Use when a detail screen reveals secondary content or actions as the user scrolls or swipes between full-screen sections.
- Grids: `references/grids.md` — Use for icon pickers, media galleries, and tiled layouts.
- Theming and dynamic type: `references/theming.md` — Use for app-wide theme tokens, colors, and type scaling.
- Controls (toggles, pickers, sliders): `references/controls.md` — Use for settings controls and input selection.
- Input toolbar (bottom anchored): `references/input-toolbar.md` — Use for chat/composer screens with a sticky input bar.
- Top bar overlays (iOS 26+ and fallback): `references/top-bar.md` — Use for pinned selectors or pills above scroll content.
- Overlay and toasts: `references/overlay.md` — Use for transient UI like banners or toasts.
- Focus handling: `references/focus.md` — Use for chaining fields and keyboard focus management.
- Searchable: `references/searchable.md` — Use for native search UI with scopes and async results.
- Async images and media: `references/media.md` — Use for remote media, previews, and media viewers.
- Haptics: `references/haptics.md` — Use for tactile feedback tied to key actions.
- Matched transitions: `references/matched-transitions.md` — Use for smooth source-to-destination animations.
- Deep links and URL routing: `references/deeplinks.md` — Use for in-app navigation from URLs.
- Title menus: `references/title-menus.md` — Use for filter or context menus in the navigation title.
- Menu bar commands: `references/menu-bar.md` — Use when adding or customizing macOS/iPadOS menu bar commands.
- Loading & placeholders: `references/loading-placeholders.md` — Use for redacted skeletons, empty states, and loading UX.
- Lightweight clients: `references/lightweight-clients.md` — Use for small, closure-based API clients injected into stores.
## Cross-cutting references
- App wiring and dependency graph: `references/app-wiring.md` — Use to wire the app shell, install shared dependencies, and decide what belongs in the environment.
- Async state and task lifecycle: `references/async-state.md` — Use when a view loads data, reacts to changing input, or needs cancellation/debouncing guidance.
- Previews: `references/previews.md` — Use when adding `#Preview`, fixtures, mock environments, or isolated preview setup.
- Performance guardrails: `references/performance.md` — Use when a screen is large, scroll-heavy, frequently updated, or showing signs of avoidable re-renders.
## Planned components (create files as needed)
- Web content: create `references/webview.md` — Use for embedded web content or in-app browsing.
- Status composer patterns: create `references/composer.md` — Use for composition or editor workflows.
- Text input and validation: create `references/text-input.md` — Use for forms, validation, and text-heavy input.
- Design system usage: create `references/design-system.md` — Use when applying shared styling rules.
## Adding entries
- Add the component file and link it here with a short “when to use” description.
- Keep each component reference short and actionable.

View File

@@ -0,0 +1,57 @@
# Controls (Toggle, Slider, Picker)
## Intent
Use native controls for settings and configuration screens, keeping labels accessible and state bindings clear.
## Core patterns
- Bind controls directly to `@State`, `@Binding`, or `@AppStorage`.
- Prefer `Toggle` for boolean preferences.
- Use `Slider` for numeric ranges and show the current value in a label.
- Use `Picker` for discrete choices; use `.pickerStyle(.segmented)` only for 24 options.
- Keep labels visible and descriptive; avoid embedding buttons inside controls.
## Example: toggles with sections
```swift
Form {
Section("Notifications") {
Toggle("Mentions", isOn: $preferences.notificationsMentionsEnabled)
Toggle("Follows", isOn: $preferences.notificationsFollowsEnabled)
Toggle("Boosts", isOn: $preferences.notificationsBoostsEnabled)
}
}
```
## Example: slider with value text
```swift
Section("Font Size") {
Slider(value: $fontSizeScale, in: 0.5...1.5, step: 0.1)
Text("Scale: \(String(format: \"%.1f\", fontSizeScale))")
.font(.scaledBody)
}
```
## Example: picker for enums
```swift
Picker("Default Visibility", selection: $visibility) {
ForEach(Visibility.allCases, id: \.self) { option in
Text(option.title).tag(option)
}
}
```
## Design choices to keep
- Group related controls in a `Form` section.
- Use `.disabled(...)` to reflect locked or inherited settings.
- Use `Label` inside toggles to combine icon + text when it adds clarity.
## Pitfalls
- Avoid `.pickerStyle(.segmented)` for large sets; use menu or inline styles instead.
- Dont hide labels for sliders; always show context.
- Avoid hard-coding colors for controls; use theme tint sparingly.

View File

@@ -0,0 +1,66 @@
# Deep links and navigation
## Intent
Route external URLs into in-app destinations while falling back to system handling when needed.
## Core patterns
- Centralize URL handling in the router (`handle(url:)`, `handleDeepLink(url:)`).
- Inject an `OpenURLAction` handler that delegates to the router.
- Use `.onOpenURL` for app scheme links and convert them to web URLs if needed.
- Let the router decide whether to navigate or open externally.
## Example: router entry points
```swift
@MainActor
final class RouterPath {
var path: [Route] = []
var urlHandler: ((URL) -> OpenURLAction.Result)?
func handle(url: URL) -> OpenURLAction.Result {
if isInternal(url) {
navigate(to: .status(id: url.lastPathComponent))
return .handled
}
return urlHandler?(url) ?? .systemAction
}
func handleDeepLink(url: URL) -> OpenURLAction.Result {
// Resolve federated URLs, then navigate.
navigate(to: .status(id: url.lastPathComponent))
return .handled
}
}
```
## Example: attach to a root view
```swift
extension View {
func withLinkRouter(_ router: RouterPath) -> some View {
self
.environment(
\.openURL,
OpenURLAction { url in
router.handle(url: url)
}
)
.onOpenURL { url in
router.handleDeepLink(url: url)
}
}
}
```
## Design choices to keep
- Keep URL parsing and decision logic inside the router.
- Avoid handling deep links in multiple places; one entry point is enough.
- Always provide a fallback to `OpenURLAction` or `UIApplication.shared.open`.
## Pitfalls
- Dont assume the URL is internal; validate first.
- Avoid blocking UI while resolving remote links; use `Task`.

View File

@@ -0,0 +1,90 @@
# Focus handling and field chaining
## Intent
Use `@FocusState` to control keyboard focus, chain fields, and coordinate focus across complex forms.
## Core patterns
- Use an enum to represent focusable fields.
- Set initial focus in `onAppear`.
- Use `.onSubmit` to move focus to the next field.
- For dynamic lists of fields, use an enum with associated values (e.g., `.option(Int)`).
## Example: single field focus
```swift
struct AddServerView: View {
@State private var server = ""
@FocusState private var isServerFieldFocused: Bool
var body: some View {
Form {
TextField("Server", text: $server)
.focused($isServerFieldFocused)
}
.onAppear { isServerFieldFocused = true }
}
}
```
## Example: chained focus with enum
```swift
struct EditTagView: View {
enum FocusField { case title, symbol, newTag }
@FocusState private var focusedField: FocusField?
var body: some View {
Form {
TextField("Title", text: $title)
.focused($focusedField, equals: .title)
.onSubmit { focusedField = .symbol }
TextField("Symbol", text: $symbol)
.focused($focusedField, equals: .symbol)
.onSubmit { focusedField = .newTag }
}
.onAppear { focusedField = .title }
}
}
```
## Example: dynamic focus for variable fields
```swift
struct PollView: View {
enum FocusField: Hashable { case option(Int) }
@FocusState private var focused: FocusField?
@State private var options: [String] = ["", ""]
@State private var currentIndex = 0
var body: some View {
ForEach(options.indices, id: \.self) { index in
TextField("Option \(index + 1)", text: $options[index])
.focused($focused, equals: .option(index))
.onSubmit { addOption(at: index) }
}
.onAppear { focused = .option(0) }
}
private func addOption(at index: Int) {
options.append("")
currentIndex = index + 1
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
focused = .option(currentIndex)
}
}
}
```
## Design choices to keep
- Keep focus state local to the view that owns the fields.
- Use focus changes to drive UX (validation messages, helper UI).
- Pair with `.scrollDismissesKeyboard(...)` when using ScrollView/Form.
## Pitfalls
- Dont store focus state in shared objects; it is view-local.
- Avoid aggressive focus changes during animation; delay if needed.

View File

@@ -0,0 +1,97 @@
# Form
## Intent
Use `Form` for structured settings, grouped inputs, and action rows. This pattern keeps layout, spacing, and accessibility consistent for data entry screens.
## Core patterns
- Wrap the form in a `NavigationStack` only when it is presented in a sheet or standalone view without an existing navigation context.
- Group related controls into `Section` blocks.
- Use `.scrollContentBackground(.hidden)` plus a custom background color when you need design-system colors.
- Apply `.formStyle(.grouped)` for grouped styling when appropriate.
- Use `@FocusState` to manage keyboard focus in input-heavy forms.
## Example: settings-style form
```swift
@MainActor
struct SettingsView: View {
@Environment(Theme.self) private var theme
var body: some View {
NavigationStack {
Form {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Edit profile") { /* open sheet */ }
.buttonStyle(.plain)
}
.listRowBackground(theme.primaryBackgroundColor)
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
}
}
}
```
## Example: modal form with validation
```swift
@MainActor
struct AddRemoteServerView: View {
@Environment(\.dismiss) private var dismiss
@Environment(Theme.self) private var theme
@State private var server: String = ""
@State private var isValid = false
@FocusState private var isServerFieldFocused: Bool
var body: some View {
NavigationStack {
Form {
TextField("Server URL", text: $server)
.keyboardType(.URL)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($isServerFieldFocused)
.listRowBackground(theme.primaryBackgroundColor)
Button("Add") {
guard isValid else { return }
dismiss()
}
.disabled(!isValid)
.listRowBackground(theme.primaryBackgroundColor)
}
.formStyle(.grouped)
.navigationTitle("Add Server")
.navigationBarTitleDisplayMode(.inline)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
.scrollDismissesKeyboard(.immediately)
.toolbar { CancelToolbarItem() }
.onAppear { isServerFieldFocused = true }
}
}
}
```
## Design choices to keep
- Prefer `Form` over custom stacks for settings and input screens.
- Keep rows tappable by using `.contentShape(Rectangle())` and `.buttonStyle(.plain)` on row buttons.
- Use list row backgrounds to keep section styling consistent with your theme.
## Pitfalls
- Avoid heavy custom layouts inside a `Form`; it can lead to spacing issues.
- If you need highly custom layouts, prefer `ScrollView` + `VStack`.
- Dont mix multiple background strategies; pick either default Form styling or custom colors.

View File

@@ -0,0 +1,71 @@
# Grids
## Intent
Use `LazyVGrid` for icon pickers, media galleries, and dense visual selections where items align in columns.
## Core patterns
- Use `.adaptive` columns for layouts that should scale across device sizes.
- Use multiple `.flexible` columns when you want a fixed column count.
- Keep spacing consistent and small to avoid uneven gutters.
- Use `GeometryReader` inside grid cells when you need square thumbnails.
## Example: adaptive icon grid
```swift
let columns = [GridItem(.adaptive(minimum: 120, maximum: 1024))]
LazyVGrid(columns: columns, spacing: 6) {
ForEach(icons) { icon in
Button {
select(icon)
} label: {
ZStack(alignment: .bottomTrailing) {
Image(icon.previewName)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(6)
if icon.isSelected {
Image(systemName: "checkmark.seal.fill")
.padding(4)
.tint(.green)
}
}
}
.buttonStyle(.plain)
}
}
```
## Example: fixed 3-column media grid
```swift
LazyVGrid(
columns: [
.init(.flexible(minimum: 100), spacing: 4),
.init(.flexible(minimum: 100), spacing: 4),
.init(.flexible(minimum: 100), spacing: 4),
],
spacing: 4
) {
ForEach(items) { item in
GeometryReader { proxy in
ThumbnailView(item: item)
.frame(width: proxy.size.width, height: proxy.size.width)
}
.aspectRatio(1, contentMode: .fit)
}
}
```
## Design choices to keep
- Use `LazyVGrid` for large collections; avoid non-lazy grids for big sets.
- Keep tap targets full-bleed using `.contentShape(Rectangle())` when needed.
- Prefer adaptive grids for settings pickers and flexible layouts.
## Pitfalls
- Avoid heavy overlays in every grid cell; it can be expensive.
- Dont nest grids inside other grids without a clear reason.

View File

@@ -0,0 +1,71 @@
# Haptics
## Intent
Use haptics sparingly to reinforce user actions (tab selection, refresh, success/error) and respect user preferences.
## Core patterns
- Centralize haptic triggers in a `HapticManager` or similar utility.
- Gate haptics behind user preferences and hardware support.
- Use distinct types for different UX moments (selection vs. notification vs. refresh).
## Example: simple haptic manager
```swift
@MainActor
final class HapticManager {
static let shared = HapticManager()
enum HapticType {
case buttonPress
case tabSelection
case dataRefresh(intensity: CGFloat)
case notification(UINotificationFeedbackGenerator.FeedbackType)
}
private let selectionGenerator = UISelectionFeedbackGenerator()
private let impactGenerator = UIImpactFeedbackGenerator(style: .heavy)
private let notificationGenerator = UINotificationFeedbackGenerator()
private init() { selectionGenerator.prepare() }
func fire(_ type: HapticType, isEnabled: Bool) {
guard isEnabled else { return }
switch type {
case .buttonPress:
impactGenerator.impactOccurred()
case .tabSelection:
selectionGenerator.selectionChanged()
case let .dataRefresh(intensity):
impactGenerator.impactOccurred(intensity: intensity)
case let .notification(style):
notificationGenerator.notificationOccurred(style)
}
}
}
```
## Example: usage
```swift
Button("Save") {
HapticManager.shared.fire(.notification(.success), isEnabled: preferences.hapticsEnabled)
}
TabView(selection: $selectedTab) { /* tabs */ }
.onChange(of: selectedTab) { _, _ in
HapticManager.shared.fire(.tabSelection, isEnabled: preferences.hapticTabSelectionEnabled)
}
```
## Design choices to keep
- Haptics should be subtle and not fire on every tiny interaction.
- Respect user preferences (toggle to disable).
- Keep haptic triggers close to the user action, not deep in data layers.
## Pitfalls
- Avoid firing multiple haptics in quick succession.
- Do not assume haptics are available; check support.

View File

@@ -0,0 +1,51 @@
# Input toolbar (bottom anchored)
## Intent
Use a bottom-anchored input bar for chat, composer, or quick actions without fighting the keyboard.
## Core patterns
- Use `.safeAreaInset(edge: .bottom)` to anchor the toolbar above the keyboard.
- Keep the main content in a `ScrollView` or `List`.
- Drive focus with `@FocusState` and set initial focus when needed.
- Avoid embedding the input bar inside the scroll content; keep it separate.
## Example: scroll view + bottom input
```swift
@MainActor
struct ConversationView: View {
@FocusState private var isInputFocused: Bool
var body: some View {
ScrollViewReader { _ in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message)
}
}
.padding(.horizontal, .layoutPadding)
}
.safeAreaInset(edge: .bottom) {
InputBar(text: $draft)
.focused($isInputFocused)
}
.scrollDismissesKeyboard(.interactively)
.onAppear { isInputFocused = true }
}
}
}
```
## Design choices to keep
- Keep the input bar visually separated from the scrollable content.
- Use `.scrollDismissesKeyboard(.interactively)` for chat-like screens.
- Ensure send actions are reachable via keyboard return or a clear button.
## Pitfalls
- Avoid placing the input view inside the scroll stack; it will jump with content.
- Avoid nested scroll views that fight for drag gestures.

View File

@@ -0,0 +1,93 @@
# Lightweight Clients (Closure-Based)
Use this pattern to keep networking or service dependencies simple and testable without introducing a full view model or heavy DI framework. It works well for SwiftUI apps where you want a small, composable API surface that can be swapped in previews/tests.
## Intent
- Provide a tiny "client" type made of async closures.
- Keep business logic in a store or feature layer, not the view.
- Enable easy stubbing in previews/tests.
## Minimal shape
```swift
struct SomeClient {
var fetchItems: (_ limit: Int) async throws -> [Item]
var search: (_ query: String, _ limit: Int) async throws -> [Item]
}
extension SomeClient {
static func live(baseURL: URL = URL(string: "https://example.com")!) -> SomeClient {
let session = URLSession.shared
return SomeClient(
fetchItems: { limit in
// build URL, call session, decode
},
search: { query, limit in
// build URL, call session, decode
}
)
}
}
```
## Usage pattern
```swift
@MainActor
@Observable final class ItemsStore {
enum LoadState { case idle, loading, loaded, failed(String) }
var items: [Item] = []
var state: LoadState = .idle
private let client: SomeClient
init(client: SomeClient) {
self.client = client
}
func load(limit: Int = 20) async {
state = .loading
do {
items = try await client.fetchItems(limit)
state = .loaded
} catch {
state = .failed(error.localizedDescription)
}
}
}
```
```swift
struct ContentView: View {
@Environment(ItemsStore.self) private var store
var body: some View {
List(store.items) { item in
Text(item.title)
}
.task { await store.load() }
}
}
```
```swift
@main
struct MyApp: App {
@State private var store = ItemsStore(client: .live())
var body: some Scene {
WindowGroup {
ContentView()
.environment(store)
}
}
}
```
## Guidance
- Keep decoding and URL-building in the client; keep state changes in the store.
- Make the store accept the client in `init` and keep it private.
- Avoid global singletons; use `.environment` for store injection.
- If you need multiple variants (mock/stub), add `static func mock(...)`.
## Pitfalls
- Dont put UI state in the client; keep state in the store.
- Dont capture `self` or view state in the client closures.

View File

@@ -0,0 +1,86 @@
# List and Section
## Intent
Use `List` for feed-style content and settings-style rows where built-in row reuse, selection, and accessibility matter.
## Core patterns
- Prefer `List` for long, vertically scrolling content with repeated rows.
- Use `Section` headers to group related rows.
- Pair with `ScrollViewReader` when you need scroll-to-top or jump-to-id.
- Use `.listStyle(.plain)` for modern feed layouts.
- Use `.listStyle(.grouped)` for multi-section discovery/search pages where section grouping helps.
- Apply `.scrollContentBackground(.hidden)` + a custom background when you need a themed surface.
- Use `.listRowInsets(...)` and `.listRowSeparator(.hidden)` to tune row spacing and separators.
- Use `.environment(\\.defaultMinListRowHeight, ...)` to control dense list layouts.
## Example: feed list with scroll-to-top
```swift
@MainActor
struct TimelineListView: View {
@Environment(\.selectedTabScrollToTop) private var selectedTabScrollToTop
@State private var scrollToId: String?
var body: some View {
ScrollViewReader { proxy in
List {
ForEach(items) { item in
TimelineRow(item: item)
.id(item.id)
.listRowInsets(.init(top: 12, leading: 16, bottom: 6, trailing: 16))
.listRowSeparator(.hidden)
}
}
.listStyle(.plain)
.environment(\\.defaultMinListRowHeight, 1)
.onChange(of: scrollToId) { _, newValue in
if let newValue {
proxy.scrollTo(newValue, anchor: .top)
scrollToId = nil
}
}
.onChange(of: selectedTabScrollToTop) { _, newValue in
if newValue == 0 {
withAnimation {
proxy.scrollTo(ScrollToView.Constants.scrollToTop, anchor: .top)
}
}
}
}
}
}
```
## Example: settings-style list
```swift
@MainActor
struct SettingsView: View {
var body: some View {
List {
Section("General") {
NavigationLink("Display") { DisplaySettingsView() }
NavigationLink("Haptics") { HapticsSettingsView() }
}
Section("Account") {
Button("Sign Out", role: .destructive) {}
}
}
.listStyle(.insetGrouped)
}
}
```
## Design choices to keep
- Use `List` for dynamic feeds, settings, and any UI where row semantics help.
- Use stable IDs for rows to keep animations and scroll positioning reliable.
- Prefer `.contentShape(Rectangle())` on rows that should be tappable end-to-end.
- Use `.refreshable` for pull-to-refresh feeds when the data source supports it.
## Pitfalls
- Avoid heavy custom layouts inside a `List` row; use `ScrollView` + `LazyVStack` instead.
- Be careful mixing `List` and nested `ScrollView`; it can cause gesture conflicts.

View File

@@ -0,0 +1,38 @@
# Loading & Placeholders
Use this when a view needs a consistent loading state (skeletons, redaction, empty state) without blocking interaction.
## Patterns to prefer
- **Redacted placeholders** for list/detail content to preserve layout while loading.
- **ContentUnavailableView** for empty or error states after loading completes.
- **ProgressView** only for short, global operations (use sparingly in content-heavy screens).
## Recommended approach
1. Keep the real layout, render placeholder data, then apply `.redacted(reason: .placeholder)`.
2. For lists, show a fixed number of placeholder rows (avoid infinite spinners).
3. Switch to `ContentUnavailableView` when load finishes but data is empty.
## Pitfalls
- Dont animate layout shifts during redaction; keep frames stable.
- Avoid nesting multiple spinners; use one loading indicator per section.
- Keep placeholder count small (36) to reduce jank on low-end devices.
## Minimal usage
```swift
VStack {
if isLoading {
ForEach(0..<3, id: \.self) { _ in
RowView(model: .placeholder())
}
.redacted(reason: .placeholder)
} else if items.isEmpty {
ContentUnavailableView("No items", systemImage: "tray")
} else {
ForEach(items) { item in RowView(model: item) }
}
}
```

View File

@@ -0,0 +1,71 @@
# macOS Settings
## Intent
Use this when building a macOS Settings window backed by SwiftUI's `Settings` scene.
## Core patterns
- Declare the Settings scene in the `App` and compile it only for macOS.
- Keep settings content in a dedicated root view (`SettingsView`) and drive values with `@AppStorage`.
- Use `TabView` to group settings sections when you have more than one category.
- Use `Form` inside each tab to keep controls aligned and accessible.
- Use `OpenSettingsAction` or `SettingsLink` for in-app entry points to the Settings window.
## Example: settings scene
```swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
#if os(macOS)
Settings {
SettingsView()
}
#endif
}
}
```
## Example: tabbed settings view
```swift
@MainActor
struct SettingsView: View {
@AppStorage("showPreviews") private var showPreviews = true
@AppStorage("fontSize") private var fontSize = 12.0
var body: some View {
TabView {
Form {
Toggle("Show Previews", isOn: $showPreviews)
Slider(value: $fontSize, in: 9...96) {
Text("Font Size (\(fontSize, specifier: "%.0f") pts)")
}
}
.tabItem { Label("General", systemImage: "gear") }
Form {
Toggle("Enable Advanced Mode", isOn: .constant(false))
}
.tabItem { Label("Advanced", systemImage: "star") }
}
.scenePadding()
.frame(maxWidth: 420, minHeight: 240)
}
}
```
## Skip navigation
- Avoid wrapping `SettingsView` in a `NavigationStack` unless you truly need deep push navigation.
- Prefer tabs or sections; Settings is already presented as a separate window and should feel flat.
- If you must show hierarchical settings, use a single `NavigationSplitView` with a sidebar list of categories.
## Pitfalls
- Dont reuse iOS-only settings layouts (full-screen stacks, toolbar-heavy flows).
- Avoid large custom view hierarchies inside `Form`; keep rows focused and accessible.

View File

@@ -0,0 +1,59 @@
# Matched transitions
## Intent
Use matched transitions to create smooth continuity between a source view (thumbnail, avatar) and a destination view (sheet, detail, viewer).
## Core patterns
- Use a shared `Namespace` and a stable ID for the source.
- Use `matchedTransitionSource` + `navigationTransition(.zoom(...))` on iOS 26+.
- Use `matchedGeometryEffect` for in-place transitions within a view hierarchy.
- Keep IDs stable across view updates (avoid random UUIDs).
## Example: media preview to full-screen viewer (iOS 26+)
```swift
struct MediaPreview: View {
@Namespace private var namespace
@State private var selected: MediaAttachment?
var body: some View {
ThumbnailView()
.matchedTransitionSource(id: selected?.id ?? "", in: namespace)
.sheet(item: $selected) { item in
MediaViewer(item: item)
.navigationTransition(.zoom(sourceID: item.id, in: namespace))
}
}
}
```
## Example: matched geometry within a view
```swift
struct ToggleBadge: View {
@Namespace private var space
@State private var isOn = false
var body: some View {
Button {
withAnimation(.spring) { isOn.toggle() }
} label: {
Image(systemName: isOn ? "eye" : "eye.slash")
.matchedGeometryEffect(id: "icon", in: space)
}
}
}
```
## Design choices to keep
- Prefer `matchedTransitionSource` for cross-screen transitions.
- Keep source and destination sizes reasonable to avoid jarring scale changes.
- Use `withAnimation` for state-driven transitions.
## Pitfalls
- Dont use unstable IDs; it breaks the transition.
- Avoid mismatched shapes (e.g., square to circle) unless the design expects it.

View File

@@ -0,0 +1,73 @@
# Media (images, video, viewer)
## Intent
Use consistent patterns for loading images, previewing media, and presenting a full-screen viewer.
## Core patterns
- Use `LazyImage` (or `AsyncImage`) for remote images with loading states.
- Prefer a lightweight preview component for inline media.
- Use a shared viewer state (e.g., `QuickLook`) to present a full-screen media viewer.
- Use `openWindow` for desktop/visionOS and a sheet for iOS.
## Example: inline media preview
```swift
struct MediaPreviewRow: View {
@Environment(QuickLook.self) private var quickLook
let attachments: [MediaAttachment]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(attachments) { attachment in
LazyImage(url: attachment.previewURL) { state in
if let image = state.image {
image.resizable().aspectRatio(contentMode: .fill)
} else {
ProgressView()
}
}
.frame(width: 120, height: 120)
.clipped()
.onTapGesture {
quickLook.prepareFor(
selectedMediaAttachment: attachment,
mediaAttachments: attachments
)
}
}
}
}
}
}
```
## Example: global media viewer sheet
```swift
struct AppRoot: View {
@State private var quickLook = QuickLook.shared
var body: some View {
content
.environment(quickLook)
.sheet(item: $quickLook.selectedMediaAttachment) { selected in
MediaUIView(selectedAttachment: selected, attachments: quickLook.mediaAttachments)
}
}
}
```
## Design choices to keep
- Keep previews lightweight; load full media in the viewer.
- Use shared viewer state so any view can open media without prop-drilling.
- Use a single entry point for the viewer (sheet/window) to avoid duplicates.
## Pitfalls
- Avoid loading full-size images in list rows; use resized previews.
- Dont present multiple viewer sheets at once; keep a single source of truth.

View File

@@ -0,0 +1,101 @@
# Menu Bar
## Intent
Use this when adding or customizing the macOS/iPadOS menu bar with SwiftUI commands.
## Core patterns
- Add commands at the `Scene` level with `.commands { ... }`.
- Use `SidebarCommands()` when your UI includes a navigation sidebar.
- Use `CommandMenu` for app-specific menus and group related actions.
- Use `CommandGroup` to insert items before/after system groups or replace them.
- Use `FocusedValue` for context-sensitive menu items that depend on the active scene.
## Example: basic command menu
```swift
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
CommandMenu("Actions") {
Button("Run", action: run)
.keyboardShortcut("R")
Button("Stop", action: stop)
.keyboardShortcut(".")
}
}
}
private func run() {}
private func stop() {}
}
```
## Example: insert and replace groups
```swift
WindowGroup {
ContentView()
}
.commands {
CommandGroup(before: .systemServices) {
Button("Check for Updates") { /* open updater */ }
}
CommandGroup(after: .newItem) {
Button("New from Clipboard") { /* create item */ }
}
CommandGroup(replacing: .help) {
Button("User Manual") { /* open docs */ }
}
}
```
## Example: focused menu state
```swift
@Observable
final class DataModel {
var items: [String] = []
}
struct ContentView: View {
@State private var model = DataModel()
var body: some View {
List(model.items, id: \.self) { item in
Text(item)
}
.focusedSceneValue(model)
}
}
struct ItemCommands: Commands {
@FocusedValue(DataModel.self) private var model: DataModel?
var body: some Commands {
CommandGroup(after: .newItem) {
Button("New Item") {
model?.items.append("Untitled")
}
.disabled(model == nil)
}
}
}
```
## Menu bar and Settings
- Defining a `Settings` scene adds the Settings menu item on macOS automatically.
- If you need a custom entry point inside the app, use `OpenSettingsAction` or `SettingsLink`.
## Pitfalls
- Avoid registering the same keyboard shortcut in multiple command groups.
- Dont use menu items as the only discoverable entry point for critical features.

View File

@@ -0,0 +1,159 @@
# NavigationStack
## Intent
Use this pattern for programmatic navigation and deep links, especially when each tab needs an independent navigation history. The key idea is one `NavigationStack` per tab, each with its own path binding and router object.
## Core architecture
- Define a route enum that is `Hashable` and represents all destinations.
- Create a lightweight router (or use a library such as `https://github.com/Dimillian/AppRouter`) that owns the `path` and any sheet state.
- Each tab owns its own router instance and binds `NavigationStack(path:)` to it.
- Inject the router into the environment so child views can navigate programmatically.
- Centralize destination mapping with a single `navigationDestination(for:)` block (or a `withAppRouter()` modifier).
## Example: custom router with per-tab stack
```swift
@MainActor
@Observable
final class RouterPath {
var path: [Route] = []
var presentedSheet: SheetDestination?
func navigate(to route: Route) {
path.append(route)
}
func reset() {
path = []
}
}
enum Route: Hashable {
case account(id: String)
case status(id: String)
}
@MainActor
struct TimelineTab: View {
@State private var routerPath = RouterPath()
var body: some View {
NavigationStack(path: $routerPath.path) {
TimelineView()
.navigationDestination(for: Route.self) { route in
switch route {
case .account(let id): AccountView(id: id)
case .status(let id): StatusView(id: id)
}
}
}
.environment(routerPath)
}
}
```
## Example: centralized destination mapping
Use a shared view modifier to avoid duplicating route switches across screens.
```swift
extension View {
func withAppRouter() -> some View {
navigationDestination(for: Route.self) { route in
switch route {
case .account(let id):
AccountView(id: id)
case .status(let id):
StatusView(id: id)
}
}
}
}
```
Then apply it once per stack:
```swift
NavigationStack(path: $routerPath.path) {
TimelineView()
.withAppRouter()
}
```
## Example: binding per tab (tabs with independent history)
```swift
@MainActor
struct TabsView: View {
@State private var timelineRouter = RouterPath()
@State private var notificationsRouter = RouterPath()
var body: some View {
TabView {
TimelineTab(router: timelineRouter)
NotificationsTab(router: notificationsRouter)
}
}
}
```
## Example: generic tabs with per-tab NavigationStack
Use this when tabs are built from data and each needs its own path without hard-coded names.
```swift
@MainActor
struct TabsView: View {
@State private var selectedTab: AppTab = .timeline
@State private var tabRouter = TabRouter()
var body: some View {
TabView(selection: $selectedTab) {
ForEach(AppTab.allCases) { tab in
NavigationStack(path: tabRouter.binding(for: tab)) {
tab.makeContentView()
}
.environment(tabRouter.router(for: tab))
.tabItem { tab.label }
.tag(tab)
}
}
}
}
```
@MainActor
@Observable
final class TabRouter {
private var routers: [AppTab: RouterPath] = [:]
func router(for tab: AppTab) -> RouterPath {
if let router = routers[tab] { return router }
let router = RouterPath()
routers[tab] = router
return router
}
func binding(for tab: AppTab) -> Binding<[Route]> {
let router = router(for: tab)
return Binding(get: { router.path }, set: { router.path = $0 })
}
}
## Design choices to keep
- One `NavigationStack` per tab to preserve independent history.
- A single source of truth for navigation state (`RouterPath` or library router).
- Use `navigationDestination(for:)` to map routes to views.
- Reset the path when app context changes (account switch, logout, etc.).
- Inject the router into the environment so child views can navigate and present sheets without prop-drilling.
- Keep sheet presentation state on the router if you want a single place to manage modals.
## Pitfalls
- Do not share one path across all tabs unless you want global history.
- Ensure route identifiers are stable and `Hashable`.
- Avoid storing view instances in the path; store lightweight route data instead.
- If using a router object, keep it outside other `@Observable` objects to avoid nested observation.

View File

@@ -0,0 +1,45 @@
# Overlay and toasts
## Intent
Use overlays for transient UI (toasts, banners, loaders) without affecting layout.
## Core patterns
- Use `.overlay(alignment:)` to place global UI without changing the underlying layout.
- Keep overlays lightweight and dismissible.
- Use a dedicated `ToastCenter` (or similar) for global state if multiple features trigger toasts.
## Example: toast overlay
```swift
struct AppRootView: View {
@State private var toast: Toast?
var body: some View {
content
.overlay(alignment: .top) {
if let toast {
ToastView(toast: toast)
.transition(.move(edge: .top).combined(with: .opacity))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation { self.toast = nil }
}
}
}
}
}
}
```
## Design choices to keep
- Prefer overlays for transient UI rather than embedding in layout stacks.
- Use transitions and short auto-dismiss timers.
- Keep the overlay aligned to a clear edge (`.top` or `.bottom`).
## Pitfalls
- Avoid overlays that block all interaction unless explicitly needed.
- Dont stack many overlays; use a queue or replace the current toast.

View File

@@ -0,0 +1,62 @@
# Performance guardrails
## Intent
Use these rules when a SwiftUI screen is large, scroll-heavy, frequently updated, or at risk of unnecessary recomputation.
## Core rules
- Give `ForEach` and list content stable identity. Do not use unstable indices as identity when the collection can reorder or mutate.
- Keep expensive filtering, sorting, and formatting out of `body`; precompute or move it into a model/helper when it is not trivial.
- Narrow observation scope so only the views that read changing state need to update.
- Prefer lazy containers for larger scrolling content and extract subviews when only part of a screen changes frequently.
- Avoid swapping entire top-level view trees for small state changes; keep a stable root view and vary localized sections or modifiers.
## Example: stable identity
```swift
ForEach(items) { item in
Row(item: item)
}
```
Prefer that over index-based identity when the collection can change order:
```swift
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
Row(item: item)
}
```
## Example: move expensive work out of body
```swift
struct FeedView: View {
let items: [FeedItem]
private var sortedItems: [FeedItem] {
items.sorted(using: KeyPathComparator(\.createdAt, order: .reverse))
}
var body: some View {
List(sortedItems) { item in
FeedRow(item: item)
}
}
}
```
If the work is more expensive than a small derived property, move it into a model, store, or helper that updates less often.
## When to investigate further
- Janky scrolling in long feeds or grids
- Typing lag from search or form validation
- Overly broad view updates when one small piece of state changes
- Large screens with many conditionals or repeated formatting work
## Pitfalls
- Recomputing heavy transforms every render
- Observing a large object from many descendants when only one field matters
- Building custom scroll containers when `List`, `LazyVStack`, or `LazyHGrid` would already solve the problem

View File

@@ -0,0 +1,48 @@
# Previews
## Intent
Use previews to validate layout, state wiring, and injected dependencies without relying on a running app or live services.
## Core rules
- Add `#Preview` coverage for the primary state plus important secondary states such as loading, empty, and error.
- Use deterministic fixtures, mocks, and sample data. Do not make previews depend on live network calls, real databases, or global singletons.
- Install required environment dependencies directly in the preview so the view can render in isolation.
- Keep preview setup close to the view until it becomes noisy; then extract lightweight preview helpers or fixtures.
- If a preview crashes, fix the state initialization or dependency wiring before expanding the feature further.
## Example: simple preview states
```swift
#Preview("Loaded") {
ProfileView(profile: .fixture)
}
#Preview("Empty") {
ProfileView(profile: nil)
}
```
## Example: preview with injected dependencies
```swift
#Preview("Search results") {
SearchView()
.environment(SearchClient.preview(results: [.fixture, .fixture2]))
.environment(Theme.preview)
}
```
## Preview checklist
- Does the preview install every required environment dependency?
- Does it cover at least one success path and one non-happy path?
- Are fixtures stable and small enough to be read quickly?
- Can the preview render without network, auth, or app-global initialization?
## Pitfalls
- Do not hide preview crashes by making dependencies optional if the production view requires them.
- Avoid huge inline fixtures when a named sample is easier to read.
- Do not couple previews to global shared singletons unless the project has no alternative.

View File

@@ -0,0 +1,133 @@
# Scroll-reveal detail surfaces
## Intent
Use this pattern when a detail screen has a primary surface first and secondary content behind it, and you want the user to reveal that secondary layer by scrolling or swiping instead of tapping a separate button.
Typical fits:
- media detail screens that reveal actions or metadata
- maps, cards, or canvases that transition into structured detail
- full-screen viewers with a second "actions" or "insights" page
## Core pattern
Build the interaction as a paged vertical `ScrollView` with two sections:
1. a primary section sized to the viewport
2. a secondary section below it
Derive a normalized `progress` value from the vertical content offset and drive all visual changes from that one value.
Avoid treating the reveal as a separate gesture system unless scroll alone cannot express it.
## Minimal structure
```swift
private enum DetailSection: Hashable {
case primary
case secondary
}
struct DetailSurface: View {
@State private var revealProgress: CGFloat = 0
@State private var secondaryHeight: CGFloat = 1
var body: some View {
GeometryReader { geometry in
ScrollViewReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
PrimaryContent(progress: revealProgress)
.frame(height: geometry.size.height)
.id(DetailSection.primary)
SecondaryContent(progress: revealProgress)
.id(DetailSection.secondary)
.onGeometryChange(for: CGFloat.self) { geo in
geo.size.height
} action: { newHeight in
secondaryHeight = max(newHeight, 1)
}
}
.scrollTargetLayout()
}
.scrollTargetBehavior(.paging)
.onScrollGeometryChange(for: CGFloat.self, of: { scroll in
scroll.contentOffset.y + scroll.contentInsets.top
}) { _, offset in
revealProgress = (offset / secondaryHeight).clamped(to: 0...1)
}
.safeAreaInset(edge: .bottom) {
ChevronAffordance(progress: revealProgress) {
withAnimation(.smooth) {
let target: DetailSection = revealProgress < 0.5 ? .secondary : .primary
proxy.scrollTo(target, anchor: .top)
}
}
}
}
}
}
}
```
## Design choices to keep
- Make the primary section exactly viewport-sized when the interaction should feel like paging between states.
- Compute `progress` from real scroll offset, not from duplicated booleans like `isExpanded`, `isShowingSecondary`, and `isSnapped`.
- Use `progress` to drive `offset`, `opacity`, `blur`, `scaleEffect`, and toolbar state so the whole surface stays synchronized.
- Use `ScrollViewReader` for programmatic snapping from taps on the primary content or chevron affordances.
- Use `onScrollTargetVisibilityChange` when you need a settled section state for haptics, tooltip dismissal, analytics, or accessibility announcements.
## Morphing a shared control
If a control appears to move from the primary surface into the secondary content, do not render two fully visible copies.
Instead:
- expose a source anchor in the primary area
- expose a destination anchor in the secondary area
- render one overlay that interpolates position and size using `progress`
```swift
Color.clear
.anchorPreference(key: ControlAnchorKey.self, value: .bounds) { anchor in
["source": anchor]
}
Color.clear
.anchorPreference(key: ControlAnchorKey.self, value: .bounds) { anchor in
["destination": anchor]
}
.overlayPreferenceValue(ControlAnchorKey.self) { anchors in
MorphingControlOverlay(anchors: anchors, progress: revealProgress)
}
```
This keeps the motion coherent and avoids duplicate-hit-target bugs.
## Haptics and affordances
- Use light threshold haptics when the reveal begins and stronger haptics near the committed state.
- Keep a visible affordance like a chevron or pill while `progress` is near zero.
- Flip, fade, or blur the affordance as the secondary section becomes active.
## Interaction guards
- Disable vertical scrolling when a conflicting mode is active, such as pinch-to-zoom, crop, or full-screen media manipulation.
- Disable hit testing on overlays that should disappear once the secondary content is revealed.
- Avoid same-axis nested scroll views unless the inner view is effectively static or disabled during the reveal.
## Pitfalls
- Do not hard-code the progress divisor. Measure the secondary section height or another real reveal distance.
- Do not mix multiple animation sources for the same property. If `progress` drives it, keep other animations off that property.
- Do not store derived state like `isSecondaryVisible` unless another API requires it. Prefer deriving it from `progress` or visible scroll targets.
- Beware of layout feedback loops when measuring heights. Clamp zero values and update only when the measured height actually changes.
## Concrete example
- Pool iOS tile detail reveal: `/Users/dimillian/Documents/Dev/Pool/pool-ios/Pool/Sources/Features/Tile/Detail/TileDetailView.swift`
- Secondary content anchor example: `/Users/dimillian/Documents/Dev/Pool/pool-ios/Pool/Sources/Features/Tile/Detail/TileDetailIntentListView.swift`

View File

@@ -0,0 +1,87 @@
# ScrollView and Lazy stacks
## Intent
Use `ScrollView` with `LazyVStack`, `LazyHStack`, or `LazyVGrid` when you need custom layout, mixed content, or horizontal/ grid-based scrolling.
## Core patterns
- Prefer `ScrollView` + `LazyVStack` for chat-like or custom feed layouts.
- Use `ScrollView(.horizontal)` + `LazyHStack` for chips, tags, avatars, and media strips.
- Use `LazyVGrid` for icon/media grids; prefer adaptive columns when possible.
- Use `ScrollViewReader` for scroll-to-top/bottom and anchor-based jumps.
- Use `safeAreaInset(edge:)` for input bars that should stick above the keyboard.
## Example: vertical custom feed
```swift
@MainActor
struct ConversationView: View {
private enum Constants { static let bottomAnchor = "bottom" }
@State private var scrollProxy: ScrollViewProxy?
var body: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messages) { message in
MessageRow(message: message)
.id(message.id)
}
Color.clear.frame(height: 1).id(Constants.bottomAnchor)
}
.padding(.horizontal, .layoutPadding)
}
.safeAreaInset(edge: .bottom) {
MessageInputBar()
}
.onAppear {
scrollProxy = proxy
withAnimation {
proxy.scrollTo(Constants.bottomAnchor, anchor: .bottom)
}
}
}
}
}
```
## Example: horizontal chips
```swift
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(chips) { chip in
ChipView(chip: chip)
}
}
}
```
## Example: adaptive grid
```swift
let columns = [GridItem(.adaptive(minimum: 120))]
ScrollView {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(items) { item in
GridItemView(item: item)
}
}
.padding(8)
}
```
## Design choices to keep
- Use `Lazy*` stacks when item counts are large or unknown.
- Use non-lazy stacks for small, fixed-size content to avoid lazy overhead.
- Keep IDs stable when using `ScrollViewReader`.
- Prefer explicit animations (`withAnimation`) when scrolling to an ID.
## Pitfalls
- Avoid nesting scroll views of the same axis; it causes gesture conflicts.
- Dont combine `List` and `ScrollView` in the same hierarchy without a clear reason.
- Overuse of `LazyVStack` for tiny content can add unnecessary complexity.

View File

@@ -0,0 +1,71 @@
# Searchable
## Intent
Use `searchable` to add native search UI with optional scopes and async results.
## Core patterns
- Bind `searchable(text:)` to local state.
- Use `.searchScopes` for multiple search modes.
- Use `.task(id: searchQuery)` or debounced tasks to avoid overfetching.
- Show placeholders or progress states while results load.
## Example: searchable with scopes
```swift
@MainActor
struct ExploreView: View {
@State private var searchQuery = ""
@State private var searchScope: SearchScope = .all
@State private var isSearching = false
@State private var results: [SearchResult] = []
var body: some View {
List {
if isSearching {
ProgressView()
} else {
ForEach(results) { result in
SearchRow(result: result)
}
}
}
.searchable(
text: $searchQuery,
placement: .navigationBarDrawer(displayMode: .always),
prompt: Text("Search")
)
.searchScopes($searchScope) {
ForEach(SearchScope.allCases, id: \.self) { scope in
Text(scope.title)
}
}
.task(id: searchQuery) {
await runSearch()
}
}
private func runSearch() async {
guard !searchQuery.isEmpty else {
results = []
return
}
isSearching = true
defer { isSearching = false }
try? await Task.sleep(for: .milliseconds(250))
results = await fetchResults(query: searchQuery, scope: searchScope)
}
}
```
## Design choices to keep
- Show a placeholder when search is empty or has no results.
- Debounce input to avoid spamming the network.
- Keep search state local to the view.
## Pitfalls
- Avoid running searches for empty strings.
- Dont block the main thread during fetch.

View File

@@ -0,0 +1,155 @@
# Sheets
## Intent
Use a centralized sheet routing pattern so any view can present modals without prop-drilling. This keeps sheet state in one place and scales as the app grows.
## Core architecture
- Define a `SheetDestination` enum that describes every modal and is `Identifiable`.
- Store the current sheet in a router object (`presentedSheet: SheetDestination?`).
- Create a view modifier like `withSheetDestinations(...)` that maps the enum to concrete sheet views.
- Inject the router into the environment so child views can set `presentedSheet` directly.
## Example: item-driven local sheet
Use this when sheet state is local to one screen and does not need centralized routing.
```swift
@State private var selectedItem: Item?
.sheet(item: $selectedItem) { item in
EditItemSheet(item: item)
}
```
## Example: SheetDestination enum
```swift
enum SheetDestination: Identifiable, Hashable {
case composer
case editProfile
case settings
case report(itemID: String)
var id: String {
switch self {
case .composer, .editProfile:
// Use the same id to ensure only one editor-like sheet is active at a time.
return "editor"
case .settings:
return "settings"
case .report:
return "report"
}
}
}
```
## Example: withSheetDestinations modifier
```swift
extension View {
func withSheetDestinations(
sheet: Binding<SheetDestination?>
) -> some View {
sheet(item: sheet) { destination in
Group {
switch destination {
case .composer:
ComposerView()
case .editProfile:
EditProfileView()
case .settings:
SettingsView()
case .report(let itemID):
ReportView(itemID: itemID)
}
}
}
}
}
```
## Example: presenting from a child view
```swift
struct StatusRow: View {
@Environment(RouterPath.self) private var router
var body: some View {
Button("Report") {
router.presentedSheet = .report(itemID: "123")
}
}
}
```
## Required wiring
For the child view to work, a parent view must:
- own the router instance,
- attach `withSheetDestinations(sheet: $router.presentedSheet)` (or an equivalent `sheet(item:)` handler), and
- inject it with `.environment(router)` after the sheet modifier so the modal content inherits it.
This makes the child assignment to `router.presentedSheet` drive presentation at the root.
## Example: sheets that need their own navigation
Wrap sheet content in a `NavigationStack` so it can push within the modal.
```swift
struct NavigationSheet<Content: View>: View {
var content: () -> Content
var body: some View {
NavigationStack {
content()
.toolbar { CloseToolbarItem() }
}
}
}
```
## Example: sheet owns its actions
Keep dismissal and confirmation logic inside the sheet when the actions belong to the modal itself.
```swift
struct EditItemSheet: View {
@Environment(\.dismiss) private var dismiss
@Environment(Store.self) private var store
let item: Item
@State private var isSaving = false
var body: some View {
VStack {
Button(isSaving ? "Saving..." : "Save") {
Task { await save() }
}
}
}
private func save() async {
isSaving = true
await store.save(item)
dismiss()
}
}
```
## Design choices to keep
- Centralize sheet routing so features can present modals without wiring bindings through many layers.
- Use `sheet(item:)` to guarantee a single sheet is active and to drive presentation from the enum.
- Group related sheets under the same `id` when they are mutually exclusive (e.g., editor flows).
- Keep sheet views lightweight and composed from smaller views; avoid large monoliths.
- Let sheets own their actions and call `dismiss()` internally instead of forwarding `onCancel` or `onConfirm` closures through many layers.
## Pitfalls
- Avoid mixing `sheet(isPresented:)` and `sheet(item:)` for the same concern; prefer a single enum.
- Avoid `if let` inside a sheet body when the presentation state already carries the selected model; prefer `sheet(item:)`.
- Do not store heavy state inside `SheetDestination`; pass lightweight identifiers or models.
- If multiple sheets can appear from the same screen, give them distinct `id` values.

View File

@@ -0,0 +1,72 @@
# Split views and columns
## Intent
Provide a lightweight, customizable multi-column layout for iPad/macOS without relying on `NavigationSplitView`.
## Custom split column pattern (manual HStack)
Use this when you want full control over column sizing, behavior, and environment tweaks.
```swift
@MainActor
struct AppView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@AppStorage("showSecondaryColumn") private var showSecondaryColumn = true
var body: some View {
HStack(spacing: 0) {
primaryColumn
if shouldShowSecondaryColumn {
Divider().edgesIgnoringSafeArea(.all)
secondaryColumn
}
}
}
private var shouldShowSecondaryColumn: Bool {
horizontalSizeClass == .regular
&& showSecondaryColumn
}
private var primaryColumn: some View {
TabView { /* tabs */ }
}
private var secondaryColumn: some View {
NotificationsTab()
.environment(\.isSecondaryColumn, true)
.frame(maxWidth: .secondaryColumnWidth)
}
}
```
## Notes on the custom approach
- Use a shared preference or setting to toggle the secondary column.
- Inject an environment flag (e.g., `isSecondaryColumn`) so child views can adapt behavior.
- Prefer a fixed or capped width for the secondary column to avoid layout thrash.
## Alternative: NavigationSplitView
`NavigationSplitView` can handle sidebar + detail + supplementary columns for you, but is harder to customize in cases like:\n- a dedicated notification column independent of selection,\n- custom sizing, or\n- different toolbar behaviors per column.
```swift
@MainActor
struct AppView: View {
var body: some View {
NavigationSplitView {
SidebarView()
} content: {
MainContentView()
} detail: {
NotificationsView()
}
}
}
```
## When to choose which
- Use the manual HStack split when you need full control or a non-standard secondary column.
- Use `NavigationSplitView` when you want a standard system layout with minimal customization.

View File

@@ -0,0 +1,114 @@
# TabView
## Intent
Use this pattern for a scalable, multi-platform tab architecture with:
- a single source of truth for tab identity and content,
- platform-specific tab sets and sidebar sections,
- dynamic tabs sourced from data,
- an interception hook for special tabs (e.g., compose).
## Core architecture
- `AppTab` enum defines identity, labels, icons, and content builder.
- `SidebarSections` enum groups tabs for sidebar sections.
- `AppView` owns the `TabView` and selection binding, and routes tab changes through `updateTab`.
## Example: custom binding with side effects
Use this when tab selection needs side effects, like intercepting a special tab to perform an action instead of changing selection.
```swift
@MainActor
struct AppView: View {
@Binding var selectedTab: AppTab
var body: some View {
TabView(selection: .init(
get: { selectedTab },
set: { updateTab(with: $0) }
)) {
ForEach(availableSections) { section in
TabSection(section.title) {
ForEach(section.tabs) { tab in
Tab(value: tab) {
tab.makeContentView(
homeTimeline: $timeline,
selectedTab: $selectedTab,
pinnedFilters: $pinnedFilters
)
} label: {
tab.label
}
.tabPlacement(tab.tabPlacement)
}
}
.tabPlacement(.sidebarOnly)
}
}
}
private func updateTab(with newTab: AppTab) {
if newTab == .post {
// Intercept special tabs (compose) instead of changing selection.
presentComposer()
return
}
selectedTab = newTab
}
}
```
## Example: direct binding without side effects
Use this when selection is purely state-driven.
```swift
@MainActor
struct AppView: View {
@Binding var selectedTab: AppTab
var body: some View {
TabView(selection: $selectedTab) {
ForEach(availableSections) { section in
TabSection(section.title) {
ForEach(section.tabs) { tab in
Tab(value: tab) {
tab.makeContentView(
homeTimeline: $timeline,
selectedTab: $selectedTab,
pinnedFilters: $pinnedFilters
)
} label: {
tab.label
}
.tabPlacement(tab.tabPlacement)
}
}
.tabPlacement(.sidebarOnly)
}
}
}
}
```
## Design choices to keep
- Centralize tab identity and content in `AppTab` with `makeContentView(...)`.
- Use `Tab(value:)` with `selection` binding for state-driven tab selection.
- Route selection changes through `updateTab` to handle special tabs and scroll-to-top behavior.
- Use `TabSection` + `.tabPlacement(.sidebarOnly)` for sidebar structure.
- Use `.tabPlacement(.pinned)` in `AppTab.tabPlacement` for a single pinned tab; this is commonly used for iOS 26 `.searchable` tab content, but can be used for any tab.
## Dynamic tabs pattern
- `SidebarSections` handles dynamic data tabs.
- `AppTab.anyTimelineFilter(filter:)` wraps dynamic tabs in a single enum case.
- The enum provides label/icon/title for dynamic tabs via the filter type.
## Pitfalls
- Avoid adding ViewModels for tabs; keep state local or in `@Observable` services.
- Do not nest `@Observable` objects inside other `@Observable` objects.
- Ensure `AppTab.id` values are stable; dynamic cases should hash on stable IDs.
- Special tabs (compose) should not change selection.

View File

@@ -0,0 +1,71 @@
# Theming and dynamic type
## Intent
Provide a clean, scalable theming approach that keeps view code semantic and consistent.
## Core patterns
- Use a single `Theme` object as the source of truth (colors, fonts, spacing).
- Inject theme at the app root and read it via `@Environment(Theme.self)` in views.
- Prefer semantic colors (`primaryBackground`, `secondaryBackground`, `label`, `tint`) instead of raw colors.
- Keep user-facing theme controls in a dedicated settings screen.
- Apply Dynamic Type scaling through custom fonts or `.font(.scaled...)`.
## Example: Theme object
```swift
@MainActor
@Observable
final class Theme {
var tintColor: Color = .blue
var primaryBackground: Color = .white
var secondaryBackground: Color = .gray.opacity(0.1)
var labelColor: Color = .primary
var fontSizeScale: Double = 1.0
}
```
## Example: inject at app root
```swift
@main
struct MyApp: App {
@State private var theme = Theme()
var body: some Scene {
WindowGroup {
AppView()
.environment(theme)
}
}
}
```
## Example: view usage
```swift
struct ProfileView: View {
@Environment(Theme.self) private var theme
var body: some View {
VStack {
Text("Profile")
.foregroundStyle(theme.labelColor)
}
.background(theme.primaryBackground)
}
}
```
## Design choices to keep
- Keep theme values semantic and minimal; avoid duplicating system colors.
- Store user-selected theme values in persistent storage if needed.
- Ensure contrast between text and backgrounds.
## Pitfalls
- Avoid sprinkling raw `Color` values in views; it breaks consistency.
- Do not tie theme to a single views local state.
- Avoid using `@Environment(\\.colorScheme)` as the only theme control; it should complement your theme.

View File

@@ -0,0 +1,93 @@
# Title menus
## Intent
Use a title menu in the navigation bar to provide contextspecific filtering or quick actions without adding extra chrome.
## Core patterns
- Use `ToolbarTitleMenu` to attach a menu to the navigation title.
- Keep the menu content compact and grouped with dividers.
## Example: title menu for filters
```swift
@ToolbarContentBuilder
private var toolbarView: some ToolbarContent {
ToolbarTitleMenu {
Button("Latest") { timeline = .latest }
Button("Resume") { timeline = .resume }
Divider()
Button("Local") { timeline = .local }
Button("Federated") { timeline = .federated }
}
}
```
## Example: attach to a view
```swift
NavigationStack {
TimelineView()
.toolbar {
toolbarView
}
}
```
## Example: title + menu together
```swift
struct TimelineScreen: View {
@State private var timeline: TimelineFilter = .home
var body: some View {
NavigationStack {
TimelineView()
.toolbar {
ToolbarItem(placement: .principal) {
VStack(spacing: 2) {
Text(timeline.title)
.font(.headline)
Text(timeline.subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
ToolbarTitleMenu {
Button("Home") { timeline = .home }
Button("Local") { timeline = .local }
Button("Federated") { timeline = .federated }
}
}
.navigationBarTitleDisplayMode(.inline)
}
}
}
```
## Example: title + subtitle with menu
```swift
ToolbarItem(placement: .principal) {
VStack(spacing: 2) {
Text(title)
.font(.headline)
Text(subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
}
```
## Design choices to keep
- Only show the title menu when filtering or context switching is available.
- Keep the title readable; avoid long labels that truncate.
- Use secondary text below the title if extra context is needed.
## Pitfalls
- Dont overload the menu with too many options.
- Avoid using title menus for destructive actions.

View File

@@ -0,0 +1,49 @@
# Top bar overlays (iOS 26+ and fallback)
## Intent
Provide a custom top selector or pill row that sits above scroll content, using `safeAreaBar(.top)` on iOS 26 and a compatible fallback on earlier OS versions.
## iOS 26+ approach
Use `safeAreaBar(edge: .top)` to attach the view to the safe area bar.
```swift
if #available(iOS 26.0, *) {
content
.safeAreaBar(edge: .top) {
TopSelectorView()
.padding(.horizontal, .layoutPadding)
}
}
```
## Fallback for earlier iOS
Use `.safeAreaInset(edge: .top)` and hide the toolbar background to avoid double layers.
```swift
content
.toolbarBackground(.hidden, for: .navigationBar)
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
TopSelectorView()
.padding(.vertical, 8)
.padding(.horizontal, .layoutPadding)
.background(Color.primary.opacity(0.06))
.background(Material.ultraThin)
Divider()
}
}
```
## Design choices to keep
- Use `safeAreaBar` when available; it integrates better with the navigation bar.
- Use a subtle background + divider in the fallback to keep separation from content.
- Keep the selector height compact to avoid pushing content too far down.
## Pitfalls
- Dont stack multiple top insets; it can create extra padding.
- Avoid heavy, opaque backgrounds that fight the navigation bar.

View File

@@ -0,0 +1,210 @@
---
name: swiftui-view-refactor
description: Refactor SwiftUI views into smaller components with stable, explicit data flow.
risk: safe
source: "Dimillian/Skills (MIT)"
date_added: "2026-03-25"
---
# SwiftUI View Refactor
## Overview
Refactor SwiftUI views toward small, explicit, stable view types. Default to vanilla SwiftUI: local state in the view, shared dependencies in the environment, business logic in services/models, and view models only when the request or existing code clearly requires one.
## When to Use
- When cleaning up a large SwiftUI view or splitting long `body` implementations.
- When you need smaller subviews, explicit dependency injection, or better Observation usage.
## Core Guidelines
### 1) View ordering (top → bottom)
- Enforce this ordering unless the existing file has a stronger local convention you must preserve.
- Environment
- `private`/`public` `let`
- `@State` / other stored properties
- computed `var` (non-view)
- `init`
- `body`
- computed view builders / other view helpers
- helper / async functions
### 2) Default to MV, not MVVM
- Views should be lightweight state expressions and orchestration points, not containers for business logic.
- Favor `@State`, `@Environment`, `@Query`, `.task`, `.task(id:)`, and `onChange` before reaching for a view model.
- Inject services and shared models via `@Environment`; keep domain logic in services/models, not in the view body.
- Do not introduce a view model just to mirror local view state or wrap environment dependencies.
- If a screen is getting large, split the UI into subviews before inventing a new view model layer.
### 3) Strongly prefer dedicated subview types over computed `some View` helpers
- Flag `body` properties that are longer than roughly one screen or contain multiple logical sections.
- Prefer extracting dedicated `View` types for non-trivial sections, especially when they have state, async work, branching, or deserve their own preview.
- Keep computed `some View` helpers rare and small. Do not build an entire screen out of `private var header: some View`-style fragments.
- Pass small, explicit inputs (data, bindings, callbacks) into extracted subviews instead of handing down the entire parent state.
- If an extracted subview becomes reusable or independently meaningful, move it to its own file.
Prefer:
```swift
var body: some View {
List {
HeaderSection(title: title, subtitle: subtitle)
FilterSection(
filterOptions: filterOptions,
selectedFilter: $selectedFilter
)
ResultsSection(items: filteredItems)
FooterSection()
}
}
private struct HeaderSection: View {
let title: String
let subtitle: String
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.title2)
Text(subtitle).font(.subheadline)
}
}
}
private struct FilterSection: View {
let filterOptions: [FilterOption]
@Binding var selectedFilter: FilterOption
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(filterOptions, id: \.self) { option in
FilterChip(option: option, isSelected: option == selectedFilter)
.onTapGesture { selectedFilter = option }
}
}
}
}
}
```
Avoid:
```swift
var body: some View {
List {
header
filters
results
footer
}
}
private var header: some View {
VStack(alignment: .leading, spacing: 6) {
Text(title).font(.title2)
Text(subtitle).font(.subheadline)
}
}
```
### 3b) Extract actions and side effects out of `body`
- Do not keep non-trivial button actions inline in the view body.
- Do not bury business logic inside `.task`, `.onAppear`, `.onChange`, or `.refreshable`.
- Prefer calling small private methods from the view, and move real business logic into services/models.
- The body should read like UI, not like a view controller.
```swift
Button("Save", action: save)
.disabled(isSaving)
.task(id: searchText) {
await reload(for: searchText)
}
private func save() {
Task { await saveAsync() }
}
private func reload(for searchText: String) async {
guard !searchText.isEmpty else {
results = []
return
}
await searchService.search(searchText)
}
```
### 4) Keep a stable view tree (avoid top-level conditional view swapping)
- Avoid `body` or computed views that return completely different root branches via `if/else`.
- Prefer a single stable base view with conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, etc.).
- Root-level branch swapping causes identity churn, broader invalidation, and extra recomputation.
Prefer:
```swift
var body: some View {
List {
documentsListContent
}
.toolbar {
if canEdit {
editToolbar
}
}
}
```
Avoid:
```swift
var documentsListView: some View {
if canEdit {
editableDocumentsList
} else {
readOnlyDocumentsList
}
}
```
### 5) View model handling (only if already present or explicitly requested)
- Treat view models as a legacy or explicit-need pattern, not the default.
- Do not introduce a view model unless the request or existing code clearly calls for one.
- If a view model exists, make it non-optional when possible.
- Pass dependencies to the view via `init`, then create the view model in the view's `init`.
- Avoid `bootstrapIfNeeded` patterns and other delayed setup workarounds.
Example (Observation-based):
```swift
@State private var viewModel: SomeViewModel
init(dependency: Dependency) {
_viewModel = State(initialValue: SomeViewModel(dependency: dependency))
}
```
### 6) Observation usage
- For `@Observable` reference types on iOS 17+, store them as `@State` in the owning view.
- Pass observables down explicitly; avoid optional state unless the UI genuinely needs it.
- If the deployment target includes iOS 16 or earlier, use `@StateObject` at the owner and `@ObservedObject` when injecting legacy observable models.
## Workflow
1. Reorder the view to match the ordering rules.
2. Remove inline actions and side effects from `body`; move business logic into services/models and keep only thin orchestration in the view.
3. Shorten long bodies by extracting dedicated subview types; avoid rebuilding the screen out of many computed `some View` helpers.
4. Ensure stable view structure: avoid top-level `if`-based branch swapping; move conditions to localized sections/modifiers.
5. If a view model exists or is explicitly required, replace optional view models with a non-optional `@State` view model initialized in `init`.
6. Confirm Observation usage: `@State` for root `@Observable` models on iOS 17+, legacy wrappers only when the deployment target requires them.
7. Keep behavior intact: do not change layout or business logic unless requested.
## Notes
- Prefer small, explicit view types over large conditional blocks and large computed `some View` properties.
- Keep computed view builders below `body` and non-view computed vars above `init`.
- A good SwiftUI refactor should make the view read top-to-bottom as data flow plus layout, not as mixed layout and imperative logic.
- For MV-first guidance and rationale, see `references/mv-patterns.md`.
## Large-view handling
When a SwiftUI view file exceeds ~300 lines, split it aggressively. Extract meaningful sections into dedicated `View` types instead of hiding complexity in many computed properties. Use `private` extensions with `// MARK: -` comments for actions and helpers, but do not treat extensions as a substitute for breaking a giant screen into smaller view types. If an extracted subview is reused or independently meaningful, move it into its own file.

View File

@@ -0,0 +1,4 @@
interface:
display_name: "SwiftUI View Refactor"
short_description: "Refactor large SwiftUI view files"
default_prompt: "Use $swiftui-view-refactor to clean up and split this SwiftUI view without changing its behavior."

View File

@@ -0,0 +1,161 @@
# MV Patterns Reference
Distilled guidance for deciding whether a SwiftUI feature should stay as plain MV or introduce a view model.
Inspired by the user's provided source, "SwiftUI in 2025: Forget MVVM" (Thomas Ricouard), but rewritten here as a practical refactoring reference.
## Default stance
- Default to MV: views are lightweight state expressions and orchestration points.
- Prefer `@State`, `@Environment`, `@Query`, `.task`, `.task(id:)`, and `onChange` before reaching for a view model.
- Keep business logic in services, models, or domain types, not in the view body.
- Split large screens into smaller view types before inventing a view model layer.
- Avoid manual fetching or state plumbing that duplicates SwiftUI or SwiftData mechanisms.
- Test services, models, and transformations first; views should stay simple and declarative.
## When to avoid a view model
Do not introduce a view model when it would mostly:
- mirror local view state,
- wrap values already available through `@Environment`,
- duplicate `@Query`, `@State`, or `Binding`-based data flow,
- exist only because the view body is too long,
- hold one-off async loading logic that can live in `.task` plus local view state.
In these cases, simplify the view and data flow instead of adding indirection.
## When a view model may be justified
A view model can be reasonable when at least one of these is true:
- the user explicitly asks for one,
- the codebase already standardizes on a view model pattern for that feature,
- the screen needs a long-lived reference model with behavior that does not fit naturally in services alone,
- the feature is adapting a non-SwiftUI API that needs a dedicated bridge object,
- multiple views share the same presentation-specific state and that state is not better modeled as app-level environment data.
Even then, keep the view model small, explicit, and non-optional when possible.
## Preferred pattern: local state plus environment
```swift
struct FeedView: View {
@Environment(BlueSkyClient.self) private var client
enum ViewState {
case loading
case error(String)
case loaded([Post])
}
@State private var viewState: ViewState = .loading
var body: some View {
List {
switch viewState {
case .loading:
ProgressView("Loading feed...")
case .error(let message):
ErrorStateView(message: message, retryAction: { await loadFeed() })
case .loaded(let posts):
ForEach(posts) { post in
PostRowView(post: post)
}
}
}
.task { await loadFeed() }
}
private func loadFeed() async {
do {
let posts = try await client.getFeed()
viewState = .loaded(posts)
} catch {
viewState = .error(error.localizedDescription)
}
}
}
```
Why this is preferred:
- state stays close to the UI that renders it,
- dependencies come from the environment instead of a wrapper object,
- the view coordinates UI flow while the service owns the real work.
## Preferred pattern: use modifiers as lightweight orchestration
```swift
.task(id: searchText) {
guard !searchText.isEmpty else {
results = []
return
}
await searchFeed(query: searchText)
}
.onChange(of: isInSearch, initial: false) {
guard !isInSearch else { return }
Task { await fetchSuggestedFeed() }
}
```
Use view lifecycle modifiers for simple, local orchestration. Do not convert these into a view model by default unless the behavior clearly outgrows the view.
## SwiftData note
SwiftData is a strong argument for keeping data flow inside the view when possible.
Prefer:
```swift
struct BookListView: View {
@Query private var books: [Book]
@Environment(\.modelContext) private var modelContext
var body: some View {
List {
ForEach(books) { book in
BookRowView(book: book)
.swipeActions {
Button("Delete", role: .destructive) {
modelContext.delete(book)
}
}
}
}
}
}
```
Avoid adding a view model that manually fetches and mirrors the same state unless the feature has an explicit reason to do so.
## Testing guidance
Prefer to test:
- services and business rules,
- models and state transformations,
- async workflows at the service layer,
- UI behavior with previews or higher-level UI tests.
Do not introduce a view model primarily to make a simple SwiftUI view "testable." That usually adds ceremony without improving the architecture.
## Refactor checklist
When refactoring toward MV:
- Remove view models that only wrap environment dependencies or local view state.
- Replace optional or delayed-initialized view models when plain view state is enough.
- Pull business logic out of the view body and into services/models.
- Keep the view as a thin coordinator of UI state, navigation, and user actions.
- Split large bodies into smaller view types before adding new layers of indirection.
## Bottom line
Treat view models as the exception, not the default.
In modern SwiftUI, the default stack is:
- `@State` for local state,
- `@Environment` for shared dependencies,
- `@Query` for SwiftData-backed collections,
- lifecycle modifiers for lightweight orchestration,
- services and models for business logic.
Reach for a view model only when the feature clearly needs one.