mirror of
https://github.com/Dimillian/Skills.git
synced 2026-03-25 08:55:54 +00:00
Compare commits
13 commits
a7fe700feb
...
39770fc180
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39770fc180 | ||
|
|
383169d1ea | ||
|
|
1d09141807 | ||
|
|
7c43dba1e1 | ||
|
|
cd6b767a6e | ||
|
|
f13a211c0f | ||
|
|
fe34551d78 | ||
|
|
b638136486 | ||
|
|
0395637cf4 | ||
|
|
098389dd35 | ||
|
|
1ec2d7af93 | ||
|
|
ce79ef24da | ||
|
|
e0e8b34203 |
27 changed files with 1036 additions and 641 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
178
README.md
178
README.md
|
|
@ -2,169 +2,45 @@
|
|||
|
||||
# Skills Public
|
||||
|
||||
A collection of specialized skills for iOS and Swift development workflows.
|
||||
A collection of reusable development skills for Apple platforms, GitHub workflows, React performance work, and skill curation.
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains a set of focused skills designed to assist with common iOS development tasks, from generating release notes to debugging apps and maintaining code quality.
|
||||
This repository contains focused, self-contained skills that help with recurring engineering tasks such as generating App Store release notes, debugging iOS apps, improving SwiftUI and React code, packaging macOS apps, and auditing what new skills a project actually needs.
|
||||
|
||||
Install: place these skill folders under `$CODEX_HOME/skills/public` (or symlink this repo there).
|
||||
Install: place these skill folders under `$CODEX_HOME/skills`
|
||||
|
||||
Optional: enable the pre-commit hook to keep `docs/skills.json` in sync:
|
||||
`git config core.hooksPath scripts/git-hooks`
|
||||
|
||||
### sk
|
||||
|
||||
Install via [sk](https://github.com/803/skills-supply), the universal package manager for AI agent skills (supports Claude, Codex, OpenCode, etc...).
|
||||
|
||||
```bash
|
||||
sk pkg add github Dimillian/Skills
|
||||
sk sync
|
||||
```
|
||||
|
||||
## Skills
|
||||
|
||||
### 📝 App Store Changelog
|
||||
This repo currently includes 11 skills:
|
||||
|
||||
**Purpose**: Generate user-facing App Store release notes from git history.
|
||||
|
||||
Automatically collects commits and changes since the last git tag (or a specified ref) and transforms them into clear, benefit-focused release notes suitable for the App Store. Filters out internal-only changes and groups user-visible improvements by theme.
|
||||
|
||||
**Key Features**:
|
||||
- Collects commits and touched files since the last tag
|
||||
- Identifies user-visible changes vs internal work
|
||||
- Generates concise, benefit-focused bullet points
|
||||
- Validates changes map back to actual commits
|
||||
|
||||
**Use When**: You need to create App Store "What's New" text or release notes based on git history.
|
||||
|
||||
---
|
||||
|
||||
### 🐛 iOS Debugger Agent
|
||||
|
||||
**Purpose**: Build, run, and debug iOS projects on simulators using XcodeBuildMCP.
|
||||
|
||||
Provides a comprehensive workflow for building iOS apps, launching them on simulators, interacting with the UI, and capturing logs. Handles simulator discovery, session setup, and runtime debugging.
|
||||
|
||||
**Key Features**:
|
||||
- Discovers and manages booted simulators
|
||||
- Builds and runs apps on simulators
|
||||
- Interacts with UI (tap, type, swipe, gestures)
|
||||
- Captures and analyzes app logs
|
||||
- Screenshots and UI inspection
|
||||
|
||||
**Use When**: You need to run an iOS app, interact with the simulator UI, inspect on-screen state, or diagnose runtime behavior.
|
||||
|
||||
---
|
||||
|
||||
### 🧭 GH Issue Fix Flow
|
||||
|
||||
**Purpose**: Resolve GitHub issues end-to-end using `gh`, local edits, builds/tests, and git push.
|
||||
|
||||
Provides a structured flow for reading issues, implementing fixes, validating with XcodeBuildMCP, and shipping changes with a closing commit.
|
||||
|
||||
**Key Features**:
|
||||
- Fetches full issue context with `gh issue view`
|
||||
- Guides code discovery and focused edits
|
||||
- Runs targeted builds/tests via XcodeBuildMCP
|
||||
- Commits with closing message and pushes
|
||||
|
||||
**Use When**: You need to take an issue number, inspect it with `gh`, implement a fix, run tests, and push.
|
||||
|
||||
---
|
||||
|
||||
### ⚡ Swift Concurrency Expert
|
||||
|
||||
**Purpose**: Review and fix Swift Concurrency issues for Swift 6.2+ codebases.
|
||||
|
||||
Applies actor isolation, Sendable safety, and modern concurrency patterns to resolve compiler errors and improve concurrency compliance. Focuses on minimal behavior changes while ensuring data-race safety.
|
||||
|
||||
**Key Features**:
|
||||
- Identifies actor context and isolation issues
|
||||
- Applies safe fixes preserving existing behavior
|
||||
- Handles UI-bound types, protocols, and background work
|
||||
- Ensures Sendable compliance
|
||||
|
||||
**Use When**: You need to review Swift Concurrency usage, improve concurrency compliance, or fix Swift concurrency compiler errors.
|
||||
|
||||
---
|
||||
|
||||
### 💎 SwiftUI Liquid Glass
|
||||
|
||||
**Purpose**: Implement and review SwiftUI features using iOS 26+ Liquid Glass API.
|
||||
|
||||
Helps adopt the native Liquid Glass API in SwiftUI interfaces, ensuring correct usage, performance, and design alignment. Supports both new implementations and refactoring existing features.
|
||||
|
||||
**Key Features**:
|
||||
- Uses native `glassEffect` and `GlassEffectContainer` APIs
|
||||
- Ensures proper modifier ordering and composition
|
||||
- Handles iOS 26+ availability with fallbacks
|
||||
- Implements interactive glass for tappable elements
|
||||
- Supports morphing transitions
|
||||
|
||||
**Use When**: You need to adopt Liquid Glass in new SwiftUI UI, refactor existing features to Liquid Glass, or review Liquid Glass usage for correctness.
|
||||
|
||||
---
|
||||
|
||||
### 🧩 SwiftUI UI Patterns
|
||||
|
||||
**Purpose**: Best practices and example-driven guidance for building SwiftUI views and components.
|
||||
|
||||
Provides a structured approach to view composition, state ownership, and component selection, with references to common patterns and scaffolding guidance.
|
||||
|
||||
**Key Features**:
|
||||
- Component references for TabView, NavigationStack, Sheets, and more
|
||||
- Scaffolding guidance for new app wiring
|
||||
- Emphasis on SwiftUI-native state and composition
|
||||
- Guidance for consistent, maintainable view structure
|
||||
|
||||
**Use When**: You need help designing SwiftUI UI, composing screens, or selecting component patterns.
|
||||
|
||||
---
|
||||
|
||||
### 🔧 SwiftUI View Refactor
|
||||
|
||||
**Purpose**: Refactor SwiftUI view files for consistent structure and dependency patterns.
|
||||
|
||||
Applies standardized ordering, Model-View (MV) patterns, and correct Observation usage to SwiftUI views. Focuses on making views lightweight, composable, and maintainable.
|
||||
|
||||
**Key Features**:
|
||||
- Enforces consistent view ordering (Environment → State → init → body → helpers)
|
||||
- Promotes MV patterns over view models when possible
|
||||
- Handles view models safely (non-optional when possible)
|
||||
- Ensures correct `@Observable` and `@State` usage
|
||||
- Supports dependency injection via `@Environment`
|
||||
|
||||
**Use When**: You need to clean up a SwiftUI view's structure, handle view models safely, or standardize dependency injection and Observation usage.
|
||||
|
||||
---
|
||||
|
||||
### 🚀 SwiftUI Performance Audit
|
||||
|
||||
**Purpose**: Audit and improve SwiftUI runtime performance from code review and architecture.
|
||||
|
||||
Focuses on identifying common SwiftUI performance pitfalls in view code and data flow, recommending targeted refactors, and guiding user-run Instruments profiling when code review is not enough.
|
||||
|
||||
**Key Features**:
|
||||
- Code-first review for slow rendering, janky scrolling, and excessive updates
|
||||
- Targets common SwiftUI pitfalls (unstable identity, heavy `body`, layout thrash)
|
||||
- Provides remediation guidance and refactor suggestions
|
||||
- Offers a user-run Instruments workflow when needed
|
||||
|
||||
**Use When**: You need to diagnose SwiftUI performance issues, improve view/update efficiency, or get guidance on profiling with Instruments.
|
||||
|
||||
---
|
||||
|
||||
### 🧰 macOS SwiftPM App Packaging (No Xcode)
|
||||
|
||||
**Purpose**: Scaffold, build, and package SwiftPM-based macOS apps without an Xcode project.
|
||||
|
||||
Bootstraps a minimal SwiftPM macOS app folder, then uses shell scripts to build, assemble the .app bundle, sign, and optionally notarize or generate Sparkle appcasts.
|
||||
|
||||
**Key Features**:
|
||||
- Bootstrap template for a SwiftPM macOS app layout
|
||||
- Packaging script to assemble a .app bundle without Xcode
|
||||
- Dev loop script to package and launch the app
|
||||
- Optional release signing/notarization and appcast tooling
|
||||
|
||||
**Use When**: You need a from-scratch macOS app layout and packaging flow without Xcode.
|
||||
|
||||
---
|
||||
| Skill | Folder | Description |
|
||||
| --- | --- | --- |
|
||||
| App Store Changelog | `app-store-changelog` | Creates user-facing App Store release notes from git history by collecting changes since the last tag, filtering for user-visible work, and rewriting it into concise "What's New" bullets. |
|
||||
| GitHub | `github` | Uses the `gh` CLI to inspect and operate on GitHub issues, pull requests, workflow runs, and API data, including CI checks, run logs, and advanced queries. |
|
||||
| iOS Debugger Agent | `ios-debugger-agent` | Uses XcodeBuildMCP to build, launch, and debug the current iOS app on a booted simulator, including UI inspection, interaction, screenshots, and log capture. |
|
||||
| macOS SwiftPM App Packaging (No Xcode) | `macos-spm-app-packaging` | Scaffolds, builds, packages, signs, and optionally notarizes SwiftPM-based macOS apps without requiring an Xcode project. |
|
||||
| Project Skill Audit | `project-skill-audit` | Analyzes a project's past Codex sessions, memory, existing local skills, and conventions to recommend the highest-value new skills or updates to existing ones. |
|
||||
| React Component Performance | `react-component-performance` | Diagnoses slow React components by finding re-render churn, expensive render work, unstable props, and list bottlenecks, then suggests targeted optimizations and validation steps. |
|
||||
| Swift Concurrency Expert | `swift-concurrency-expert` | Reviews and fixes Swift 6.2+ concurrency issues such as actor isolation problems, `Sendable` violations, main-actor annotations, and data-race diagnostics. |
|
||||
| SwiftUI Liquid Glass | `swiftui-liquid-glass` | Implements, reviews, or refactors SwiftUI features to use the iOS 26+ Liquid Glass APIs correctly, with proper modifier ordering, grouping, interactivity, and fallbacks. |
|
||||
| SwiftUI Performance Audit | `swiftui-performance-audit` | Audits SwiftUI runtime performance from code and architecture, focusing on invalidation storms, identity churn, layout thrash, heavy render work, and profiling guidance. |
|
||||
| SwiftUI UI Patterns | `swiftui-ui-patterns` | Provides best practices and example-driven guidance for building SwiftUI screens and components, including navigation, sheets, app wiring, async state, and reusable UI patterns. |
|
||||
| SwiftUI View Refactor | `swiftui-view-refactor` | Refactors SwiftUI view files toward smaller subviews, MV-style data flow, stable view trees, explicit dependency injection, and correct Observation usage. |
|
||||
|
||||
## Usage
|
||||
|
||||
Each skill is self-contained with its own documentation. Refer to the `SKILL.md` file in each skill's directory for detailed workflows, guidelines, and examples.
|
||||
Each skill is self-contained. Refer to the `SKILL.md` file in each skill directory for triggers, workflow guidance, examples, and supporting references.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
4
app-store-changelog/agents/openai.yaml
Normal file
4
app-store-changelog/agents/openai.yaml
Normal 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."
|
||||
4
github/agents/openai.yaml
Normal file
4
github/agents/openai.yaml
Normal 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."
|
||||
4
ios-debugger-agent/agents/openai.yaml
Normal file
4
ios-debugger-agent/agents/openai.yaml
Normal 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."
|
||||
4
macos-spm-app-packaging/agents/openai.yaml
Normal file
4
macos-spm-app-packaging/agents/openai.yaml
Normal 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."
|
||||
182
project-skill-audit/SKILL.md
Normal file
182
project-skill-audit/SKILL.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
---
|
||||
name: project-skill-audit
|
||||
description: Analyze a project's past Codex sessions, memory files, and existing local skills to recommend the highest-value skills to create or update. Use when a user asks what skills a project needs, wants skill ideas grounded in real project history, wants an audit of current project-local skills, or wants recommendations for updating stale or incomplete skills instead of creating duplicates.
|
||||
---
|
||||
|
||||
# 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.
|
||||
|
||||
## 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.
|
||||
4
project-skill-audit/agents/openai.yaml
Normal file
4
project-skill-audit/agents/openai.yaml
Normal 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."
|
||||
4
react-component-performance/agents/openai.yaml
Normal file
4
react-component-performance/agents/openai.yaml
Normal 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."
|
||||
4
swift-concurrency-expert/agents/openai.yaml
Normal file
4
swift-concurrency-expert/agents/openai.yaml
Normal 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."
|
||||
4
swiftui-liquid-glass/agents/openai.yaml
Normal file
4
swiftui-liquid-glass/agents/openai.yaml
Normal 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."
|
||||
|
|
@ -5,184 +5,82 @@ description: Audit and improve SwiftUI runtime performance from code review and
|
|||
|
||||
# SwiftUI Performance Audit
|
||||
|
||||
## Overview
|
||||
## Quick start
|
||||
|
||||
Audit SwiftUI view performance end-to-end, from instrumentation and baselining to root-cause analysis and concrete remediation steps.
|
||||
Use this skill to diagnose SwiftUI performance issues from code first, then request profiling evidence when code review alone cannot explain the symptoms.
|
||||
|
||||
## Workflow Decision Tree
|
||||
## Workflow
|
||||
|
||||
- If the user provides code, start with "Code-First Review."
|
||||
- If the user only describes symptoms, ask for minimal code/context, then do "Code-First Review."
|
||||
- If code review is inconclusive, go to "Guide the User to Profile" and ask for a trace or screenshots.
|
||||
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. Code-First Review
|
||||
## 1. Intake
|
||||
|
||||
Collect:
|
||||
- Target view/feature code.
|
||||
- Data flow: state, environment, observable models.
|
||||
- Symptoms and reproduction steps.
|
||||
- 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:
|
||||
- View invalidation storms from broad state changes.
|
||||
- Unstable identity in lists (`id` churn, `UUID()` per render).
|
||||
- Top-level conditional view swapping (`if/else` returning different root branches).
|
||||
- Heavy work in `body` (formatting, sorting, image decoding).
|
||||
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
|
||||
- Large images without downsampling or resizing.
|
||||
- Over-animated hierarchies (implicit animations on large trees).
|
||||
- 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.
|
||||
|
||||
## 2. Guide the User to Profile
|
||||
## 3. Guide the User to Profile
|
||||
|
||||
Explain how to collect data with Instruments:
|
||||
- Use the SwiftUI template in Instruments (Release build).
|
||||
- Reproduce the exact interaction (scroll, navigation, animation).
|
||||
- Capture SwiftUI timeline and Time Profiler.
|
||||
- Export or screenshot the relevant lanes and the call tree.
|
||||
|
||||
Ask for:
|
||||
- Trace export or screenshots of SwiftUI lanes + Time Profiler call tree.
|
||||
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.
|
||||
|
||||
## 3. Analyze and Diagnose
|
||||
Use `references/profiling-intake.md` for the exact checklist and collection steps.
|
||||
|
||||
Prioritize likely SwiftUI culprits:
|
||||
- View invalidation storms from broad state changes.
|
||||
- Unstable identity in lists (`id` churn, `UUID()` per render).
|
||||
- Top-level conditional view swapping (`if/else` returning different root branches).
|
||||
- Heavy work in `body` (formatting, sorting, image decoding).
|
||||
- Layout thrash (deep stacks, `GeometryReader`, preference chains).
|
||||
- Large images without downsampling or resizing.
|
||||
- Over-animated hierarchies (implicit animations on large trees).
|
||||
## 4. Analyze and Diagnose
|
||||
|
||||
Summarize findings with evidence from traces/logs.
|
||||
- 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.
|
||||
|
||||
## 4. Remediate
|
||||
## 5. Remediate
|
||||
|
||||
Apply targeted fixes:
|
||||
- Narrow state scope (`@State`/`@Observable` closer to leaf views).
|
||||
- Narrow state scope and reduce broad observation fan-out.
|
||||
- Stabilize identities for `ForEach` and lists.
|
||||
- Move heavy work out of `body` (precompute, cache, `@State`).
|
||||
- Use `equatable()` or value wrappers for expensive subtrees.
|
||||
- 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.
|
||||
|
||||
## Common Code Smells (and Fixes)
|
||||
Use `references/code-smells.md` for examples, Observation-specific fan-out guidance, and remediation patterns.
|
||||
|
||||
Look for these patterns during code review.
|
||||
|
||||
### Expensive formatters in `body`
|
||||
|
||||
```swift
|
||||
var body: some View {
|
||||
let number = NumberFormatter() // slow allocation
|
||||
let measure = MeasurementFormatter() // slow allocation
|
||||
Text(measure.string(from: .init(value: meters, unit: .meters)))
|
||||
}
|
||||
```
|
||||
|
||||
Prefer cached formatters in a model or a dedicated helper:
|
||||
|
||||
```swift
|
||||
final class DistanceFormatter {
|
||||
static let shared = DistanceFormatter()
|
||||
let number = NumberFormatter()
|
||||
let measure = MeasurementFormatter()
|
||||
}
|
||||
```
|
||||
|
||||
### Computed properties that do heavy work
|
||||
|
||||
```swift
|
||||
var filtered: [Item] {
|
||||
items.filter { $0.isEnabled } // runs on every body eval
|
||||
}
|
||||
```
|
||||
|
||||
Prefer precompute or cache on change:
|
||||
|
||||
```swift
|
||||
@State private var filtered: [Item] = []
|
||||
// update filtered when inputs change
|
||||
```
|
||||
|
||||
### Sorting/filtering in `body` or `ForEach`
|
||||
|
||||
```swift
|
||||
List {
|
||||
ForEach(items.sorted(by: sortRule)) { item in
|
||||
Row(item)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Prefer sort once before view updates:
|
||||
|
||||
```swift
|
||||
let sortedItems = items.sorted(by: sortRule)
|
||||
```
|
||||
|
||||
### Inline filtering in `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; use a stable ID.
|
||||
|
||||
### 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/modifiers (for example inside `toolbar`, row content, `overlay`, or `disabled`). This reduces root identity churn and helps SwiftUI diffing stay efficient.
|
||||
|
||||
### Image decoding on the main thread
|
||||
|
||||
```swift
|
||||
Image(uiImage: UIImage(data: data)!)
|
||||
```
|
||||
|
||||
Prefer decode/downsample off the main thread and store the result.
|
||||
|
||||
### Broad dependencies in observable models
|
||||
|
||||
```swift
|
||||
@Observable class Model {
|
||||
var items: [Item] = []
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Row(isFavorite: model.items.contains(item))
|
||||
}
|
||||
```
|
||||
|
||||
Prefer granular view models or per-item state to reduce update fan-out.
|
||||
|
||||
## 5. Verify
|
||||
## 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.
|
||||
|
|
@ -194,9 +92,14 @@ Provide:
|
|||
- Top issues (ordered by impact).
|
||||
- Proposed fixes with estimated effort.
|
||||
|
||||
Use `references/report-template.md` when formatting the final audit.
|
||||
|
||||
## References
|
||||
|
||||
Add Apple documentation and WWDC resources under `references/` as they are supplied by the user.
|
||||
- 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`
|
||||
|
|
|
|||
4
swiftui-performance-audit/agents/openai.yaml
Normal file
4
swiftui-performance-audit/agents/openai.yaml
Normal 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."
|
||||
150
swiftui-performance-audit/references/code-smells.md
Normal file
150
swiftui-performance-audit/references/code-smells.md
Normal 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
|
||||
44
swiftui-performance-audit/references/profiling-intake.md
Normal file
44
swiftui-performance-audit/references/profiling-intake.md
Normal 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.
|
||||
47
swiftui-performance-audit/references/report-template.md
Normal file
47
swiftui-performance-audit/references/report-template.md
Normal 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.
|
||||
|
|
@ -20,7 +20,7 @@ Choose a track based on your goal:
|
|||
|
||||
### New project scaffolding
|
||||
|
||||
- Start with `references/app-scaffolding-wiring.md` to wire TabView + NavigationStack + sheets.
|
||||
- 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.
|
||||
|
|
@ -28,21 +28,57 @@ Choose a track based on your goal:
|
|||
## 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.
|
||||
- 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 and its ownership location.
|
||||
2. Identify dependencies to inject via `@Environment`.
|
||||
3. Sketch the view hierarchy and extract repeated parts into subviews. **Build and verify no compiler errors before proceeding.**
|
||||
4. Implement async loading with `.task` and explicit state enum if needed.
|
||||
5. Add accessibility labels or identifiers when the UI is interactive.
|
||||
6. Validate with a build: confirm no compiler errors, check that previews render without crashing, and ensure state changes propagate correctly. 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.
|
||||
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
|
||||
|
||||
|
|
@ -52,46 +88,6 @@ Use `references/components-index.md` as the entry point. Each component referenc
|
|||
- Pitfalls and performance notes.
|
||||
- Paths to existing examples in the current repo.
|
||||
|
||||
For detail surfaces that progressively reveal actions, metadata, or contextual panels, use `references/scroll-reveal.md`.
|
||||
|
||||
## Sheet patterns
|
||||
|
||||
### Item-driven sheet (preferred)
|
||||
|
||||
```swift
|
||||
@State private var selectedItem: Item?
|
||||
|
||||
.sheet(item: $selectedItem) { item in
|
||||
EditItemSheet(item: item)
|
||||
}
|
||||
```
|
||||
|
||||
### Sheet owns its actions
|
||||
|
||||
```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()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding a new component reference
|
||||
|
||||
- Create `references/<component>.md`.
|
||||
|
|
|
|||
4
swiftui-ui-patterns/agents/openai.yaml
Normal file
4
swiftui-ui-patterns/agents/openai.yaml
Normal 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."
|
||||
|
|
@ -10,6 +10,13 @@ Show how to wire the app shell (TabView + NavigationStack + sheets) and install
|
|||
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
|
||||
|
|
|
|||
96
swiftui-ui-patterns/references/async-state.md
Normal file
96
swiftui-ui-patterns/references/async-state.md
Normal 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.
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
# Components Index
|
||||
|
||||
Use this file to find component-specific guidance. Each entry lists when to use it.
|
||||
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 modal routing: `references/sheets.md` — Use when you want centralized, enum-driven sheet presentation.
|
||||
- App wiring and dependency graph: `references/app-wiring.md` — Use to wire TabView + NavigationStack + sheets at the root and install global dependencies.
|
||||
- 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.
|
||||
|
|
@ -31,6 +30,13 @@ Use this file to find component-specific guidance. Each entry lists when to use
|
|||
- 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.
|
||||
|
|
|
|||
62
swiftui-ui-patterns/references/performance.md
Normal file
62
swiftui-ui-patterns/references/performance.md
Normal 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
|
||||
48
swiftui-ui-patterns/references/previews.md
Normal file
48
swiftui-ui-patterns/references/previews.md
Normal 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.
|
||||
|
|
@ -11,6 +11,18 @@ Use a centralized sheet routing pattern so any view can present modals without p
|
|||
- 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
|
||||
|
|
@ -99,15 +111,45 @@ struct NavigationSheet<Content: View>: View {
|
|||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,17 @@
|
|||
---
|
||||
name: swiftui-view-refactor
|
||||
description: Refactor and review SwiftUI view files for consistent structure, dependency injection, and Observation usage. Use when asked to clean up a SwiftUI view's layout/ordering, handle view models safely (non-optional when possible), or standardize how dependencies and @Observable state are initialized and passed.
|
||||
description: Refactor and review SwiftUI view files with strong defaults for small dedicated subviews, MV-over-MVVM data flow, stable view trees, explicit dependency injection, and correct Observation usage. Use when cleaning up a SwiftUI view, splitting long bodies, removing inline actions or side effects, reducing computed `some View` helpers, or standardizing `@Observable` and view model initialization patterns.
|
||||
---
|
||||
|
||||
# SwiftUI View Refactor
|
||||
|
||||
## Overview
|
||||
Apply a consistent structure and dependency pattern to SwiftUI views, with a focus on ordering, Model-View (MV) patterns, careful view model handling, and correct Observation usage.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
|
@ -20,19 +21,65 @@ Apply a consistent structure and dependency pattern to SwiftUI views, with a foc
|
|||
- computed view builders / other view helpers
|
||||
- helper / async functions
|
||||
|
||||
### 2) Prefer MV (Model-View) patterns
|
||||
- Default to MV: Views are lightweight state expressions; models/services own business logic.
|
||||
- Favor `@State`, `@Environment`, `@Query`, and `task`/`onChange` for orchestration.
|
||||
- Inject services and shared models via `@Environment`; keep views small and composable.
|
||||
- Split large views into subviews rather than introducing a view model.
|
||||
### 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) Split large bodies and view properties
|
||||
- If `body` grows beyond a screen or has multiple logical sections, split it into smaller subviews.
|
||||
- Extract large computed view properties into dedicated `View` types when they carry state or complex branching.
|
||||
- Keep related subviews as computed view properties in the same file; extract to a standalone `View` struct only when reuse is intended or it structurally makes sense.
|
||||
- Prefer passing small inputs (data, bindings, callbacks) over reusing the entire parent view state.
|
||||
### 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.
|
||||
|
||||
Example (long body → shorter body + computed views):
|
||||
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 {
|
||||
|
|
@ -50,42 +97,36 @@ private var header: some View {
|
|||
Text(subtitle).font(.subheadline)
|
||||
}
|
||||
}
|
||||
|
||||
private var filters: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
ForEach(filterOptions, id: \.self) { option in
|
||||
FilterChip(option: option, isSelected: option == selectedFilter)
|
||||
.onTapGesture { selectedFilter = option }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Example (extracting a complex computed view into a dedicated struct):
|
||||
### 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
|
||||
private var header: some View {
|
||||
HeaderSection(title: title, subtitle: subtitle, status: status)
|
||||
Button("Save", action: save)
|
||||
.disabled(isSaving)
|
||||
|
||||
.task(id: searchText) {
|
||||
await reload(for: searchText)
|
||||
}
|
||||
|
||||
private struct HeaderSection: View {
|
||||
let title: String
|
||||
let subtitle: String?
|
||||
let status: Status
|
||||
private func save() {
|
||||
Task { await saveAsync() }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title).font(.headline)
|
||||
if let subtitle { Text(subtitle).font(.subheadline) }
|
||||
StatusBadge(status: status)
|
||||
}
|
||||
private func reload(for searchText: String) async {
|
||||
guard !searchText.isEmpty else {
|
||||
results = []
|
||||
return
|
||||
}
|
||||
await searchService.search(searchText)
|
||||
}
|
||||
```
|
||||
|
||||
### 3b) Keep a stable view tree (avoid top-level conditional view swapping)
|
||||
### 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.
|
||||
|
|
@ -117,11 +158,12 @@ var documentsListView: some View {
|
|||
}
|
||||
```
|
||||
|
||||
### 4) View model handling (only if already present)
|
||||
### 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 pass them into the view model in the view's `init`.
|
||||
- Avoid `bootstrapIfNeeded` patterns.
|
||||
- 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):
|
||||
|
||||
|
|
@ -133,25 +175,28 @@ init(dependency: Dependency) {
|
|||
}
|
||||
```
|
||||
|
||||
### 5) Observation usage
|
||||
- For `@Observable` reference types, store them as `@State` in the root view.
|
||||
- Pass observables down explicitly as needed; avoid optional state unless required.
|
||||
### 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. Favor MV: move lightweight orchestration into the view using `@State`, `@Environment`, `@Query`, `task`, and `onChange`.
|
||||
3. Ensure stable view structure: avoid top-level `if`-based branch swapping; move conditions to localized sections/modifiers.
|
||||
4. If a view model exists, replace optional view models with a non-optional `@State` view model initialized in `init` by passing dependencies from the view.
|
||||
5. Confirm Observation usage: `@State` for root `@Observable` view models, no redundant wrappers.
|
||||
6. Keep behavior intact: do not change layout or business logic unless requested.
|
||||
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 helpers over large conditional blocks.
|
||||
- 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 using extensions to group related helpers. Move async and helper functions into dedicated `private` extensions separated with `// MARK: -` comments (e.g., `// MARK: - Actions`, `// MARK: - Subviews`, `// MARK: - Helpers`). Keep the main `struct` focused on stored properties, `init`, and `body`.
|
||||
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.
|
||||
|
|
|
|||
4
swiftui-view-refactor/agents/openai.yaml
Normal file
4
swiftui-view-refactor/agents/openai.yaml
Normal 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."
|
||||
|
|
@ -1,69 +1,45 @@
|
|||
# MV Patterns Reference
|
||||
|
||||
Source provided by user: "SwiftUI in 2025: Forget MVVM" (Thomas Ricouard).
|
||||
Distilled guidance for deciding whether a SwiftUI feature should stay as plain MV or introduce a view model.
|
||||
|
||||
Use this as guidance when deciding whether to 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
|
||||
|
||||
Key points:
|
||||
- Default to MV: views are lightweight state expressions and orchestration points.
|
||||
- Prefer `@State`, `@Environment`, `@Query`, `task`, and `onChange` over view models.
|
||||
- Inject services and shared models via `@Environment`; keep logic in services/models.
|
||||
- Split large views into smaller views instead of moving logic into a view model.
|
||||
- Avoid manual data fetching that duplicates SwiftUI/SwiftData mechanisms.
|
||||
- Test models/services and business logic; views should stay simple and declarative.
|
||||
- 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.
|
||||
|
||||
# SwiftUI in 2025: Forget MVVM
|
||||
## When to avoid a view model
|
||||
|
||||
*Let me tell you why*
|
||||
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.
|
||||
|
||||
**Thomas Ricouard**
|
||||
10 min read · Jun 2, 2025
|
||||
In these cases, simplify the view and data flow instead of adding indirection.
|
||||
|
||||
---
|
||||
## When a view model may be justified
|
||||
|
||||
It’s 2025, and I’m still getting asked the same question:
|
||||
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.
|
||||
|
||||
> “Where are your ViewModels?”
|
||||
Even then, keep the view model small, explicit, and non-optional when possible.
|
||||
|
||||
Every time I share this opinion or code from my open-source projects like my BlueSky client **IcySky**, or even the Medium iOS app, developers are surprised to see clean, simple views without a single ViewModel in sight.
|
||||
|
||||
Let me be clear:
|
||||
|
||||
You don’t need ViewModels in SwiftUI.
|
||||
You never did.
|
||||
You never will.
|
||||
|
||||
---
|
||||
|
||||
## The MVVM Trap
|
||||
|
||||
When SwiftUI launched in 2019, many developers brought their UIKit baggage with them. We were so used to the *Massive View Controller* problem that we immediately reached for MVVM as our savior.
|
||||
|
||||
But SwiftUI isn’t UIKit.
|
||||
|
||||
It was designed from the ground up with a different philosophy, highlighted in multiple WWDC sessions like:
|
||||
|
||||
- *Data Flow Through SwiftUI (WWDC19)*
|
||||
- *Data Essentials in SwiftUI (WWDC20)*
|
||||
- *Discover Observation in SwiftUI (WWDC23)*
|
||||
|
||||
Those sessions barely mention ViewModels.
|
||||
|
||||
Why? Because ViewModels are almost alien to SwiftUI’s data flow model.
|
||||
|
||||
SwiftUI views are **structs**, not classes. They are lightweight, disposable, and recreated frequently. Adding a ViewModel means fighting the framework’s core design.
|
||||
|
||||
---
|
||||
|
||||
## Views as Pure State Expressions
|
||||
|
||||
In my latest IcySky app, every view follows the same pattern I’ve advocated for years.
|
||||
## Preferred pattern: local state plus environment
|
||||
|
||||
```swift
|
||||
struct FeedView: View {
|
||||
|
||||
@Environment(BlueSkyClient.self) private var client
|
||||
@Environment(AppTheme.self) private var theme
|
||||
|
||||
enum ViewState {
|
||||
case loading
|
||||
|
|
@ -72,138 +48,66 @@ struct FeedView: View {
|
|||
}
|
||||
|
||||
@State private var viewState: ViewState = .loading
|
||||
@State private var isRefreshing = false
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
List {
|
||||
switch viewState {
|
||||
case .loading:
|
||||
ProgressView("Loading feed...")
|
||||
.frame(maxWidth: .infinity)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
case .error(let message):
|
||||
ErrorStateView(
|
||||
message: message,
|
||||
retryAction: { await loadFeed() }
|
||||
)
|
||||
.listRowSeparator(.hidden)
|
||||
|
||||
ErrorStateView(message: message, retryAction: { await loadFeed() })
|
||||
case .loaded(let posts):
|
||||
ForEach(posts) { post in
|
||||
PostRowView(post: post)
|
||||
.listRowInsets(.init())
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.refreshable { await refreshFeed() }
|
||||
.task { await loadFeed() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The state is defined inside the view, using an enum.
|
||||
|
||||
No ViewModel.
|
||||
No indirection.
|
||||
The view is a direct expression of state.
|
||||
|
||||
## The Magic of Environment
|
||||
|
||||
Instead of dependency injection through ViewModels, SwiftUI gives us @Environment.
|
||||
|
||||
```swift
|
||||
@Environment(BlueSkyClient.self) private var client
|
||||
|
||||
private func loadFeed() async {
|
||||
private func loadFeed() async {
|
||||
do {
|
||||
let posts = try await client.getFeed()
|
||||
viewState = .loaded(posts)
|
||||
} catch {
|
||||
viewState = .error(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Your services live in the environment, are testable in isolation, and encapsulate complexity.
|
||||
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.
|
||||
|
||||
The view orchestrates UI flow — nothing else.
|
||||
|
||||
Real-World Complexity
|
||||
“This only works for simple apps.”
|
||||
|
||||
No.
|
||||
|
||||
IcySky handles authentication, complex feeds, navigation, and user interaction — without ViewModels.
|
||||
|
||||
The Medium iOS app (millions of users) is now mostly SwiftUI and uses very few ViewModels, most of them legacy from 2019.
|
||||
|
||||
For new features, we inject services into the environment and build lightweight views with local state.
|
||||
|
||||
Using `.task(id:)` and `.onChange()`
|
||||
|
||||
## SwiftUI’s modifiers act as small state reducers.
|
||||
## Preferred pattern: use modifiers as lightweight orchestration
|
||||
|
||||
```swift
|
||||
.task(id: searchText) {
|
||||
guard !searchText.isEmpty else { return }
|
||||
guard !searchText.isEmpty else {
|
||||
results = []
|
||||
return
|
||||
}
|
||||
await searchFeed(query: searchText)
|
||||
}
|
||||
|
||||
.onChange(of: isInSearch, initial: false) {
|
||||
guard !isInSearch else { return }
|
||||
Task { await fetchSuggestedFeed() }
|
||||
}
|
||||
```
|
||||
|
||||
Readable. Local. Explicit.
|
||||
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.
|
||||
|
||||
## App-Level Environment Setup
|
||||
## SwiftData note
|
||||
|
||||
```swift
|
||||
@main
|
||||
struct IcySkyApp: App {
|
||||
SwiftData is a strong argument for keeping data flow inside the view when possible.
|
||||
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
|
||||
@State var client: BSkyClient?
|
||||
@State var auth: Auth = .init()
|
||||
@State var currentUser: CurrentUser?
|
||||
@State var router: AppRouter = .init(initialTab: .feed)
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
TabView(selection: $router.selectedTab) {
|
||||
if client != nil && currentUser != nil {
|
||||
ForEach(AppTab.allCases) { tab in
|
||||
AppTabRootView(tab: tab)
|
||||
.tag(tab)
|
||||
.toolbarVisibility(.hidden, for: .tabBar)
|
||||
}
|
||||
} else {
|
||||
ProgressView()
|
||||
.containerRelativeFrame([.horizontal, .vertical])
|
||||
}
|
||||
}
|
||||
.environment(client)
|
||||
.environment(currentUser)
|
||||
.environment(auth)
|
||||
.environment(router)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All dependencies are injected once and available everywhere.
|
||||
|
||||
## SwiftData: The Perfect Example
|
||||
SwiftData was built to work directly in views.
|
||||
Prefer:
|
||||
|
||||
```swift
|
||||
struct BookListView: View {
|
||||
|
||||
@Query private var books: [Book]
|
||||
@Environment(\.modelContext) private var modelContext
|
||||
|
||||
|
|
@ -222,93 +126,36 @@ struct BookListView: View {
|
|||
}
|
||||
```
|
||||
|
||||
Now compare that to forcing a ViewModel:
|
||||
Avoid adding a view model that manually fetches and mirrors the same state unless the feature has an explicit reason to do so.
|
||||
|
||||
```swift
|
||||
@Observable
|
||||
class BookListViewModel {
|
||||
private var modelContext: ModelContext
|
||||
var books: [Book] = []
|
||||
## Testing guidance
|
||||
|
||||
init(modelContext: ModelContext) {
|
||||
self.modelContext = modelContext
|
||||
fetchBooks()
|
||||
}
|
||||
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.
|
||||
|
||||
func fetchBooks() {
|
||||
let descriptor = FetchDescriptor<Book>()
|
||||
books = try! modelContext.fetch(descriptor)
|
||||
}
|
||||
}
|
||||
```
|
||||
Do not introduce a view model primarily to make a simple SwiftUI view "testable." That usually adds ceremony without improving the architecture.
|
||||
|
||||
Manual fetching. Manual refresh. Boilerplate everywhere.
|
||||
## Refactor checklist
|
||||
|
||||
You’re fighting the framework.
|
||||
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.
|
||||
|
||||
## Testing Reality
|
||||
Testing SwiftUI views provides minimal value.
|
||||
## Bottom line
|
||||
|
||||
Instead:
|
||||
Treat view models as the exception, not the default.
|
||||
|
||||
* Unit test services and business logic
|
||||
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.
|
||||
|
||||
* Test models and transformations
|
||||
|
||||
* Use SwiftUI previews for visual regression
|
||||
|
||||
* Use UI automation for E2E tests
|
||||
|
||||
* If needed, use `ViewInspector` for view introspection.
|
||||
|
||||
## The 2025 Reality
|
||||
|
||||
SwiftUI is mature:
|
||||
|
||||
* `@Observable`
|
||||
|
||||
* Better Environment
|
||||
|
||||
* Improved async & task lifecycle
|
||||
|
||||
* Almost everything you need lives inside the view.
|
||||
|
||||
I’ll reconsider ViewModels when Apple lets us access Environment outside views.
|
||||
|
||||
Until then, vanilla SwiftUI is the canon.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Every ViewModel adds:
|
||||
|
||||
* More complexity
|
||||
|
||||
* More objects to sync
|
||||
|
||||
* More indirection
|
||||
|
||||
* More cognitive overhead
|
||||
|
||||
SwiftUI gives you:
|
||||
|
||||
* `@State`
|
||||
|
||||
* `@Environment`
|
||||
|
||||
* `@Observable`
|
||||
|
||||
* Binding
|
||||
|
||||
Use them. Trust the framework.
|
||||
|
||||
## The Bottom Line
|
||||
In 2025, there’s no excuse for cluttering SwiftUI apps with unnecessary ViewModels.
|
||||
|
||||
Let views be pure expressions of state.
|
||||
|
||||
Focus complexity where it belongs: services and business logic.
|
||||
|
||||
Goodbye MVVM 🚮
|
||||
Long live the View 👑
|
||||
|
||||
Happy coding 🚀
|
||||
Reach for a view model only when the feature clearly needs one.
|
||||
|
|
|
|||
Loading…
Reference in a new issue