Compare commits

...

8 commits

Author SHA1 Message Date
Thomas Ricouard
fe34551d78 cleanup 2026-03-15 11:12:21 +01:00
Thomas Ricouard
b638136486 improve SwiftUI performance skill 2026-03-15 11:01:31 +01:00
Thomas Ricouard
0395637cf4 fixup iOS version for @Observable 2026-03-15 10:53:10 +01:00
Thomas Ricouard
098389dd35 Enhance swiftui-patterns skill 2026-03-15 10:50:34 +01:00
Thomas Ricouard
1ec2d7af93 Update SKILL.md to refine project scaffolding instructions and introduce a state ownership matrix. Added detailed navigation and routing guidelines, preview guidance, async task lifecycle recommendations, performance guardrails, environment injection policies, and anti-patterns to enhance SwiftUI development practices. 2026-03-15 10:44:45 +01:00
Thomas Ricouard
ce79ef24da Enhance SKILL.md with fallback guidance for iOS 16 and earlier. Added recommendation to use ObservableObject with appropriate ownership attributes when the Observation API is unavailable. 2026-03-15 10:40:22 +01:00
Thomas Ricouard
970b318b90
Merge pull request #7 from popey/improve/skill-review-optimization
feat: improve 8 skills with targeted optimizations
2026-03-15 10:35:21 +01:00
Alan Pope
3db84e63d0
feat: improve 8 skills with targeted optimizations
Hullo @dimillian 👋

I ran your skills through `tessl skill review` at work and found some targeted improvements. Here's the before/after:

| Skill | Before | After | Change |
|-------|--------|-------|--------|
| swiftui-ui-patterns | 81% | 100% | +19% |
| github | 85% | 100% | +15% |
| macos-spm-app-packaging | 86% | 100% | +14% |
| react-component-performance | 86% | 100% | +14% |
| swift-concurrency-expert | 88% | 100% | +12% |
| app-store-changelog | 93% | 100% | +7% |
| ios-debugger-agent | 94% | 100% | +6% |
| swiftui-view-refactor | 93% | 95% | +2% |
| swiftui-liquid-glass | 100% | 100% | — |
| swiftui-performance-audit | 100% | 100% | — |

<details>
<summary>Changes made</summary>

**swiftui-ui-patterns** — Expanded description with additional trigger terms (VStack/HStack, @State, @Binding, navigation hierarchies, custom view modifiers). Added explicit build validation checkpoints and error recovery guidance to the workflow steps.

**github** — Added explicit "Use when..." clause with natural trigger terms (check CI status, create PR, list issues). Added a structured "Debugging a CI Failure" numbered workflow organizing existing commands into a clear investigation sequence.

**macos-spm-app-packaging** — Added a minimum end-to-end example (bootstrap to running app). Added validation checkpoints after packaging, signing, and notarization with specific verification commands (codesign, spctl, stapler). Added a common notarization failures troubleshooting table.

**react-component-performance** — Added three concrete before/after code examples (isolate ticking state, stabilize callbacks with useCallback + memo, derived data with useMemo). Expanded profiling validation with explicit React DevTools Profiler steps.

**swift-concurrency-expert** — Expanded description with concrete actions (adding Sendable conformance, @MainActor annotations, resolving actor isolation warnings). Added verification step to workflow. Added three before/after Swift code examples covering @MainActor, protocol conformance isolation, and @concurrent.

**app-store-changelog** — Added commit-to-bullet transformation examples showing how raw commits map to user-facing App Store bullets, plus examples of internal-only commits that get dropped. Added a complete example "What's New" output block.

**ios-debugger-agent** — Added build failure handling and post-launch verification checkpoints to the core workflow (verify app launched via describe_ui or screenshot before proceeding to UI interaction).

**swiftui-view-refactor** — Tightened prose in sections 3, 3b, and large-view handling. Removed a redundant example. Converted workflow list to clean numbered format.

</details>

Honest disclosure — I work at @tesslio where we build tooling around skills like these. Not a pitch - just saw room for improvement and wanted to contribute.

If you want to run reviews, evals and optimizations yourself, just `npm install @tessl/cli` then run `tessl skill review path/to/your/SKILL.md`, and click here (https://tessl.io/registry/skills/submit) to find out more.

Thanks in advance 🙏
2026-03-06 16:53:32 +00:00
19 changed files with 885 additions and 238 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View file

@ -30,8 +30,35 @@ Generate a comprehensive, user-facing changelog from git history since the last
- Check for duplicates and overly technical wording.
- Ask for clarification if any change is ambiguous or possibly internal-only.
## Commit-to-Bullet Examples
The following shows how raw commits are translated into App Store bullets:
| Raw commit message | App Store bullet |
|---|---|
| `fix(auth): resolve token refresh race condition on iOS 17` | • Fixed a login issue that could leave some users unexpectedly signed out. |
| `feat(search): add voice input to search bar` | • Search your library hands-free with the new voice input option. |
| `perf(timeline): lazy-load images to reduce scroll jank` | • Scrolling through your timeline is now smoother and faster. |
Internal-only commits that are **dropped** (no user impact):
- `chore: upgrade fastlane to 2.219`
- `refactor(network): extract URLSession wrapper into module`
- `ci: add nightly build job`
## Example Output
```
What's New in Version 3.4
• Search your library hands-free with the new voice input option.
• Scrolling through your timeline is now smoother and faster.
• Fixed a login issue that could leave some users unexpectedly signed out.
• Added dark-mode support to the settings screen.
• Improved load times when opening large photo albums.
```
## Output Format
- Title (optional): "Whats New" or product name + version.
- Title (optional): "What's New" or product name + version.
- Bullet list only; one sentence per bullet.
- Stick to storefront limits if the user provides one.

View file

@ -1,6 +1,6 @@
---
name: github
description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries."
description: "Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries. Use when the user asks about GitHub issues, pull requests, workflows, or wants to interact with GitHub repositories from the command line — including tasks like check CI status, create PR, list issues, or query the GitHub API."
---
# GitHub Skill
@ -29,6 +29,27 @@ View logs for failed steps only:
gh run view <run-id> --repo owner/repo --log-failed
```
### Debugging a CI Failure
Follow this sequence to investigate a failing CI run:
1. **Check PR status** — identify which checks are failing:
```bash
gh pr checks 55 --repo owner/repo
```
2. **List recent runs** — find the relevant run ID:
```bash
gh run list --repo owner/repo --limit 10
```
3. **View the failed run** — see which jobs and steps failed:
```bash
gh run view <run-id> --repo owner/repo
```
4. **Fetch failure logs** — get the detailed output for failed steps:
```bash
gh run view <run-id> --repo owner/repo --log-failed
```
## API for Advanced Queries
The `gh api` command is useful for accessing data not available through other subcommands.

View file

@ -24,6 +24,8 @@ Follow this sequence unless the user asks for a narrower action.
### 3) Build + run (when requested)
- Call `mcp__XcodeBuildMCP__build_run_sim`.
- **If the build fails**, check the error output and retry (optionally with `preferXcodebuild: true`) or escalate to the user before attempting any UI interaction.
- **After a successful build**, verify the app launched by calling `mcp__XcodeBuildMCP__describe_ui` or `mcp__XcodeBuildMCP__screenshot` before proceeding to UI interaction.
- If the app is already built and only launch is requested, use `mcp__XcodeBuildMCP__launch_app_sim`.
- If bundle id is unknown:
1) `mcp__XcodeBuildMCP__get_sim_app_path`

View file

@ -22,6 +22,63 @@ Bootstrap a complete SwiftPM macOS app folder, then build, package, and run it w
- Release (optional): `Scripts/sign-and-notarize.sh` and `Scripts/make_appcast.sh`.
- Tag + GitHub release (optional): create a git tag, upload the zip/appcast to the GitHub release, and publish.
## Minimum End-to-End Example
Shortest path from bootstrap to a running app:
```bash
# 1. Copy and rename the skeleton
cp -R assets/templates/bootstrap/ ~/Projects/MyApp
cd ~/Projects/MyApp
sed -i '' 's/MyApp/HelloApp/g' Package.swift version.env
# 2. Copy scripts
cp assets/templates/package_app.sh Scripts/
cp assets/templates/compile_and_run.sh Scripts/
chmod +x Scripts/*.sh
# 3. Build and launch
swift build
Scripts/compile_and_run.sh
```
## Validation Checkpoints
Run these after key steps to catch failures early before proceeding to the next stage.
**After packaging (`Scripts/package_app.sh`):**
```bash
# Confirm .app bundle structure is intact
ls -R build/HelloApp.app/Contents
# Check that the binary is present and executable
file build/HelloApp.app/Contents/MacOS/HelloApp
```
**After signing (`Scripts/sign-and-notarize.sh` or ad-hoc dev signing):**
```bash
# Inspect signature and entitlements
codesign -dv --verbose=4 build/HelloApp.app
# Verify the bundle passes Gatekeeper checks locally
spctl --assess --type execute --verbose build/HelloApp.app
```
**After notarization and stapling:**
```bash
# Confirm the staple ticket is attached
stapler validate build/HelloApp.app
# Re-run Gatekeeper to confirm notarization is recognised
spctl --assess --type execute --verbose build/HelloApp.app
```
## Common Notarization Failures
| Symptom | Likely Cause | Recovery |
|---|---|---|
| `The software asset has already been uploaded` | Duplicate submission for same version | Bump `BUILD_NUMBER` in `version.env` and repackage. |
| `Package Invalid: Invalid Code Signing Entitlements` | Entitlements in `.entitlements` file don't match provisioning | Audit entitlements against Apple's allowed set; remove unsupported keys. |
| `The executable does not have the hardened runtime enabled` | Missing `--options runtime` flag in `codesign` invocation | Edit `sign-and-notarize.sh` to add `--options runtime` to all `codesign` calls. |
| Notarization hangs / no status email | `xcrun notarytool` network or credential issue | Run `xcrun notarytool history` to check status; re-export App Store Connect API key if expired. |
| `stapler validate` fails after successful notarization | Ticket not yet propagated | Wait ~60 s, then re-run `xcrun stapler staple`. |
## Templates
- `assets/templates/package_app.sh`: Build binaries, create the .app bundle, copy resources, sign.
- `assets/templates/compile_and_run.sh`: Dev loop to kill running app, package, launch.

View file

@ -16,7 +16,7 @@ Identify render hotspots, isolate expensive updates, and apply targeted optimiza
3. Isolate fast-changing state from heavy subtrees.
4. Stabilize props and handlers; memoize where it pays off.
5. Reduce expensive work (computation, DOM size, list length).
6. Validate with profiling; avoid speculative changes.
6. **Validate**: open React DevTools Profiler → record the interaction → inspect the Flamegraph for components rendering longer than ~16 ms → compare against a pre-optimization baseline recording.
## Checklist
@ -33,11 +33,94 @@ Identify render hotspots, isolate expensive updates, and apply targeted optimiza
## Optimization Patterns
- **Isolate ticking state**: move a timer/animation into a child component so the parent list does not re-render every tick.
- **Stabilize callbacks**: prefer `useCallback` for handlers passed to memoized rows.
### Isolate ticking state
Move a timer or animation counter into a child so the parent list never re-renders on each tick.
```tsx
// ❌ Before entire parent (and list) re-renders every second
function Dashboard({ items }: { items: Item[] }) {
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
return (
<>
<Clock tick={tick} />
<ExpensiveList items={items} /> {/* re-renders every second */}
</>
);
}
// ✅ After only <Clock> re-renders; list is untouched
function Clock() {
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick(t => t + 1), 1000);
return () => clearInterval(id);
}, []);
return <span>{tick}s</span>;
}
function Dashboard({ items }: { items: Item[] }) {
return (
<>
<Clock />
<ExpensiveList items={items} />
</>
);
}
```
### Stabilize callbacks with `useCallback` + `memo`
```tsx
// ❌ Before new handler reference on every render busts Row memo
function List({ items }: { items: Item[] }) {
const handleClick = (id: string) => console.log(id); // new ref each render
return items.map(item => <Row key={item.id} item={item} onClick={handleClick} />);
}
// ✅ After stable handler; Row only re-renders when its own item changes
const Row = memo(({ item, onClick }: RowProps) => (
<li onClick={() => onClick(item.id)}>{item.name}</li>
));
function List({ items }: { items: Item[] }) {
const handleClick = useCallback((id: string) => console.log(id), []);
return items.map(item => <Row key={item.id} item={item} onClick={handleClick} />);
}
```
### Prefer derived data outside render
```tsx
// ❌ Before recomputes on every render
function Summary({ orders }: { orders: Order[] }) {
const total = orders.reduce((sum, o) => sum + o.amount, 0); // runs every render
return <p>Total: {total}</p>;
}
// ✅ After recomputes only when orders changes
function Summary({ orders }: { orders: Order[] }) {
const total = useMemo(() => orders.reduce((sum, o) => sum + o.amount, 0), [orders]);
return <p>Total: {total}</p>;
}
```
### Additional patterns
- **Split rows**: extract list rows into memoized components with narrow props.
- **Defer heavy rendering**: lazy-render or collapse expensive content until expanded.
- **Prefer derived data outside render**: compute summaries with `useMemo` or helper functions when inputs are stable.
## Profiling Validation Steps
1. Open **React DevTools → Profiler** tab.
2. Click **Record**, perform the slow interaction, then **Stop**.
3. Switch to **Flamegraph** view; any bar labeled with a component and time > ~16 ms is a candidate.
4. Use **Ranked chart** to sort by self render time and target the top offenders.
5. Apply one optimization at a time, re-record, and compare render counts and durations against the baseline.
## Example Reference

View file

@ -1,6 +1,6 @@
---
name: swift-concurrency-expert
description: Swift Concurrency review and remediation for Swift 6.2+. Use when asked to review Swift Concurrency usage, improve concurrency compliance, or fix Swift concurrency compiler errors in a feature or file.
description: Swift Concurrency review and remediation for Swift 6.2+. Use when asked to review Swift Concurrency usage, improve concurrency compliance, or fix Swift concurrency compiler errors in a feature or file. Concrete actions include adding Sendable conformance, applying @MainActor annotations, resolving actor isolation warnings, fixing data race diagnostics, and migrating completion handlers to async/await.
---
# Swift Concurrency Expert
@ -29,6 +29,74 @@ Common fixes:
- **Background work**: move expensive work into a `@concurrent` async function on a `nonisolated` type or use an `actor` to guard mutable state.
- **Sendable errors**: prefer immutable/value types; add `Sendable` conformance only when correct; avoid `@unchecked Sendable` unless you can prove thread safety.
### 3. Verify the fix
- Rebuild and confirm all concurrency diagnostics are resolved with no new warnings introduced.
- Run the test suite to check for regressions — concurrency changes can introduce subtle runtime issues even when the build is clean.
- If the fix surfaces new warnings, treat each one as a fresh triage (return to step 1) and resolve iteratively until the build is clean and tests pass.
### Examples
**UI-bound type — adding `@MainActor`**
```swift
// Before: data-race warning because ViewModel is accessed from the main thread
// but has no actor isolation
class ViewModel: ObservableObject {
@Published var title: String = ""
func load() { title = "Loaded" }
}
// After: annotate the whole type so all stored state and methods are
// automatically isolated to the main actor
@MainActor
class ViewModel: ObservableObject {
@Published var title: String = ""
func load() { title = "Loaded" }
}
```
**Protocol conformance isolation**
```swift
// Before: compiler error — SomeProtocol method is nonisolated but the
// conforming type is @MainActor
@MainActor
class Foo: SomeProtocol {
func protocolMethod() { /* accesses main-actor state */ }
}
// After: scope the conformance to @MainActor so the requirement is
// satisfied inside the correct isolation context
@MainActor
extension Foo: SomeProtocol {
func protocolMethod() { /* safely accesses main-actor state */ }
}
```
**Background work with `@concurrent`**
```swift
// Before: expensive computation blocks the main actor
@MainActor
func processData(_ input: [Int]) -> [Int] {
input.map { heavyTransform($0) } // runs on main thread
}
// After: hop off the main actor for the heavy work, then return the result
// The caller awaits the result and stays on its own actor
nonisolated func processData(_ input: [Int]) async -> [Int] {
await Task.detached(priority: .userInitiated) {
input.map { heavyTransform($0) }
}.value
}
// Or, using a @concurrent async function (Swift 6.2+):
@concurrent
func processData(_ input: [Int]) async -> [Int] {
input.map { heavyTransform($0) }
}
```
## Reference material

View file

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

View file

@ -0,0 +1,150 @@
# Common code smells and remediation patterns
## Intent
Use this reference during code-first review to map visible SwiftUI patterns to likely runtime costs and safer remediation guidance.
## High-priority smells
### Expensive formatters in `body`
```swift
var body: some View {
let number = NumberFormatter()
let measure = MeasurementFormatter()
Text(measure.string(from: .init(value: meters, unit: .meters)))
}
```
Prefer cached formatters in a model or dedicated helper:
```swift
final class DistanceFormatter {
static let shared = DistanceFormatter()
let number = NumberFormatter()
let measure = MeasurementFormatter()
}
```
### Heavy computed properties
```swift
var filtered: [Item] {
items.filter { $0.isEnabled }
}
```
Prefer deriving this once per meaningful input change in a model/helper, or store derived view-owned state only when the view truly owns the transformation lifecycle.
### Sorting or filtering inside `body`
```swift
List {
ForEach(items.sorted(by: sortRule)) { item in
Row(item)
}
}
```
Prefer sorting before render work begins:
```swift
let sortedItems = items.sorted(by: sortRule)
```
### Inline filtering inside `ForEach`
```swift
ForEach(items.filter { $0.isEnabled }) { item in
Row(item)
}
```
Prefer a prefiltered collection with stable identity.
### Unstable identity
```swift
ForEach(items, id: \.self) { item in
Row(item)
}
```
Avoid `id: \.self` for non-stable values or collections that reorder. Use a stable domain identifier.
### Top-level conditional view swapping
```swift
var content: some View {
if isEditing {
editingView
} else {
readOnlyView
}
}
```
Prefer one stable base view and localize conditions to sections or modifiers. This reduces root identity churn and makes diffing cheaper.
### Image decoding on the main thread
```swift
Image(uiImage: UIImage(data: data)!)
```
Prefer decode and downsample work off the main thread, then store the processed image.
## Observation fan-out
### Broad `@Observable` reads on iOS 17+
```swift
@Observable final class Model {
var items: [Item] = []
}
var body: some View {
Row(isFavorite: model.items.contains(item))
}
```
If many views read the same broad collection or root model, small changes can fan out into wide invalidation. Prefer narrower derived inputs, smaller observable surfaces, or per-item state closer to the leaf views.
### Broad `ObservableObject` reads on iOS 16 and earlier
```swift
final class Model: ObservableObject {
@Published var items: [Item] = []
}
```
The same warning applies to legacy observation. Avoid having many descendants observe a large shared object when they only need one derived field.
## Remediation notes
### `@State` is not a generic cache
Use `@State` for view-owned state and derived values that intentionally belong to the view lifecycle. Do not move arbitrary expensive computation into `@State` unless you also define when and why it updates.
Better alternatives:
- precompute in the model or store
- update derived state in response to a specific input change
- memoize in a dedicated helper
- preprocess on a background task before rendering
### `equatable()` is conditional guidance
Use `equatable()` only when:
- equality is cheaper than recomputing the subtree, and
- the view inputs are value-semantic and stable enough for meaningful equality checks
Do not apply `equatable()` as a blanket fix for all redraws.
## Triage order
When multiple smells appear together, prioritize in this order:
1. Broad invalidation and observation fan-out
2. Unstable identity and list churn
3. Main-thread work during render
4. Image decode or resize cost
5. Layout and animation complexity

View file

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

View file

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

View file

@ -1,6 +1,6 @@
---
name: swiftui-ui-patterns
description: Best practices and example-driven guidance for building SwiftUI views and components. Use when creating or refactoring SwiftUI UI, designing tab architecture with TabView, composing screens, or needing component-specific patterns and examples.
description: Best practices and example-driven guidance for building SwiftUI views and components, including navigation hierarchies, custom view modifiers, and responsive layouts with stacks and grids. Use when creating or refactoring SwiftUI UI, designing tab architecture with TabView, composing screens with VStack/HStack, managing @State or @Binding, building declarative iOS interfaces, or needing component-specific patterns and examples.
---
# SwiftUI UI Patterns
@ -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.
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 and update usage callsites if needed.
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`.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
---
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 views 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 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.
---
# SwiftUI View Refactor
@ -28,23 +28,11 @@ Apply a consistent structure and dependency pattern to SwiftUI views, with a foc
### 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 (`var header: some View { ... }`) into dedicated `View` types when they carry state or complex branching.
- It's fine to keep related subviews as computed view properties in the same file; extract to a standalone `View` struct only when it structurally makes sense or when reuse is intended.
- 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.
Example (extracting a section):
```swift
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HeaderSection(title: title, isPinned: isPinned)
DetailsSection(details: details)
ActionsSection(onSave: onSave, onCancel: onCancel)
}
}
```
Example (long body → shorter body + computed views in the same file):
Example (long body → shorter body + computed views):
```swift
var body: some View {
@ -75,7 +63,7 @@ private var filters: some View {
}
```
Example (extracting a complex computed view):
Example (extracting a complex computed view into a dedicated struct):
```swift
private var header: some View {
@ -98,9 +86,9 @@ private struct HeaderSection: View {
```
### 3b) Keep a stable view tree (avoid top-level conditional view swapping)
- Avoid patterns where a computed view (or `body`) returns completely different root branches using `if/else`.
- Prefer a single stable base view, and place conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, row content, etc.).
- Root-level branch swapping can cause identity churn, broader invalidation, and extra recomputation in SwiftUI.
- Avoid `body` or computed views that return completely different root branches via `if/else`.
- Prefer a single stable base view with conditions inside sections/modifiers (`overlay`, `opacity`, `disabled`, `toolbar`, etc.).
- Root-level branch swapping causes identity churn, broader invalidation, and extra recomputation.
Prefer:
@ -151,12 +139,12 @@ init(dependency: Dependency) {
## 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.
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.
## Notes
@ -166,4 +154,4 @@ init(dependency: Dependency) {
## Large-view handling
- When a SwiftUI view file exceeds ~300 lines, split it using extensions to group related helpers. Move async functions and helper functions into dedicated `private` extensions, separated with `// MARK: -` comments that describe their purpose (e.g., `// MARK: - Actions`, `// MARK: - Subviews`, `// MARK: - Helpers`). Keep the main `struct` focused on stored properties, init, and `body`, with view-building computed vars also grouped via marks when the file is long.
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`.