Compare commits

...

5 commits

16 changed files with 423 additions and 438 deletions

BIN
.DS_Store vendored

Binary file not shown.

169
README.md
View file

@ -2,169 +2,36 @@
# 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`
## 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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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.

View file

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

View file

@ -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
Its 2025, and Im 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 dont 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 isnt 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 SwiftUIs data flow model.
SwiftUI views are **structs**, not classes. They are lightweight, disposable, and recreated frequently. Adding a ViewModel means fighting the frameworks core design.
---
## Views as Pure State Expressions
In my latest IcySky app, every view follows the same pattern Ive 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,51 +48,22 @@ 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 {
do {
@ -126,84 +73,41 @@ private func loadFeed() async {
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()`
## SwiftUIs 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
Youre 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.
Ill 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, theres 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.