fix: remove broken SwiftUI skill and add CI reference check (#1476) (#1520)

Remove the bundled SwiftUI skill which had 13+ broken references to a
non-existent `../macos-apps/references/` directory. Add a CI script
that validates all relative .md file references in bundled skills,
preventing this class of bug from shipping again. Fix 5 additional
pre-existing broken references in other skills.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-19 18:04:37 -06:00 committed by GitHub
parent 69c0f68ac8
commit 816383a399
24 changed files with 179 additions and 11032 deletions

View file

@ -27,6 +27,17 @@ jobs:
exit 1
fi
skill-references:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24'
- name: Validate skill references
run: node scripts/check-skill-references.mjs
build:
runs-on: ubuntu-latest

View file

@ -0,0 +1,166 @@
#!/usr/bin/env node
/**
* Validates that relative .md file references in bundled skills point to
* files that actually exist on disk.
*
* Focused on catching broken cross-file references within skills:
* - Markdown links to .md files: [text](path/to/file.md)
* - Backtick-quoted .md paths that use relative navigation: `../foo/bar.md`
* or skill subdirectory paths: `references/foo.md`, `workflows/bar.md`
*
* Deliberately ignores:
* - URLs (http://, https://)
* - Paths starting with ~ (home-dir references, not repo-relative)
* - Glob patterns containing * or {}
* - Template placeholders containing {{ or {word}
* - Bare extensions like `.md`, `.ts`
* - Example/placeholder paths (path/to/...)
* - Paths that reference files outside the skills tree via ../ beyond the
* skills root (those are cross-concern refs, not validatable here)
*
* Exit 0 if all references resolve. Exit 1 if any are broken.
*/
import { readFileSync, readdirSync, existsSync } from "node:fs";
import { join, resolve, dirname, extname } from "node:path";
const SKILLS_DIR = resolve("src/resources/skills");
/** Recursively collect all .md files under a directory. */
function collectMdFiles(dir) {
const results = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...collectMdFiles(full));
} else if (entry.isFile() && extname(entry.name) === ".md") {
results.push(full);
}
}
return results;
}
/** Return true if this reference should be validated. */
function shouldValidate(ref) {
// Must end with .md (we only validate markdown cross-references)
if (!ref.endsWith(".md")) return false;
// Skip URLs
if (/^https?:\/\//.test(ref)) return false;
// Skip home-dir paths
if (ref.startsWith("~")) return false;
// Skip glob patterns
if (/[*{}]/.test(ref)) return false;
// Skip template placeholders like {{foo}} or {foo}
if (/\{[^}]+\}/.test(ref)) return false;
// Skip bare extensions like ".md"
if (/^\.\w+$/.test(ref)) return false;
// Skip obvious example paths
if (/^path\/to\//.test(ref)) return false;
// Skip absolute paths
if (ref.startsWith("/")) return false;
// Only validate paths that look like structural skill references:
// relative navigation (../ or ./) or skill subdirectories (references/, workflows/)
if (
!ref.startsWith("./") &&
!ref.startsWith("../") &&
!ref.startsWith("references/") &&
!ref.startsWith("workflows/") &&
!ref.startsWith("scripts/") &&
!ref.startsWith("templates/")
) {
return false;
}
return true;
}
/** Strip trailing anchor: foo.md#section -> foo.md */
function stripAnchor(ref) {
const idx = ref.indexOf("#");
return idx >= 0 ? ref.slice(0, idx) : ref;
}
/**
* Extract validatable .md references from markdown content.
* Returns array of { ref, line }.
*/
function extractReferences(content) {
const refs = [];
const lines = content.split("\n");
let inCodeBlock = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;
// Track fenced code blocks (``` or ~~~)
if (/^(\s*)(`{3,}|~{3,})/.test(line)) {
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) continue;
// Pattern 1: Markdown links [text](path.md) or [text](path.md#anchor)
const mdLinkRe = /\[(?:[^\]]*)\]\(([^)]+)\)/g;
let match;
while ((match = mdLinkRe.exec(line)) !== null) {
const raw = stripAnchor(match[1].trim());
if (shouldValidate(raw)) {
refs.push({ ref: raw, line: lineNum });
}
}
// Pattern 2: Backtick-quoted paths to .md files
const backtickRe = /`([^`]+\.md(?:#[^`]*)?)`/g;
while ((match = backtickRe.exec(line)) !== null) {
const raw = stripAnchor(match[1].trim());
if (shouldValidate(raw)) {
refs.push({ ref: raw, line: lineNum });
}
}
}
return refs;
}
// --- Main ---
if (!existsSync(SKILLS_DIR)) {
console.error(`Skills directory not found: ${SKILLS_DIR}`);
process.exit(1);
}
const mdFiles = collectMdFiles(SKILLS_DIR);
let brokenCount = 0;
let checkedCount = 0;
for (const file of mdFiles) {
const content = readFileSync(file, "utf-8");
const refs = extractReferences(content);
const fileDir = dirname(file);
const displayPath = file.replace(resolve(".") + "/", "");
for (const { ref, line } of refs) {
checkedCount++;
const resolved = resolve(fileDir, ref);
if (!existsSync(resolved)) {
console.error(
`ERROR: ${displayPath}:${line} references "${ref}" but file does not exist`
);
brokenCount++;
}
}
}
if (brokenCount > 0) {
console.error(
`\n${brokenCount} broken reference(s) found across ${mdFiles.length} skill files.`
);
process.exit(1);
} else {
console.log(
`All references valid. Checked ${checkedCount} reference(s) across ${mdFiles.length} skill file(s).`
);
process.exit(0);
}

View file

@ -438,4 +438,4 @@ startTransition(() => setExpensiveState(newValue));
- [web.dev LCP](https://web.dev/articles/lcp)
- [web.dev INP](https://web.dev/articles/inp)
- [web.dev CLS](https://web.dev/articles/cls)
- [Performance skill](../performance/SKILL.md)
- [Code Optimizer skill](../code-optimizer/SKILL.md)

View file

@ -42,7 +42,7 @@ The file must `export default function(pi: ExtensionAPI) { ... }`.
## Step 4: Check for Common Mistakes
Read `references/key-rules-gotchas.md` and verify each rule against the extension code.
Read `../references/key-rules-gotchas.md` and verify each rule against the extension code.
## Step 5: Add Debugging

View file

@ -88,5 +88,3 @@ EVIDENCE: [output from ci_monitor.cjs]
## References
- `references/gh/SKILL.md` — gh CLI reference
- `scripts/ci_monitor.cjs` — CI monitoring tool
- `scripts/ci_monitor.md` — Tool usage documentation

View file

@ -1,208 +0,0 @@
---
name: swiftui
description: SwiftUI apps from scratch through App Store. Full lifecycle - create, debug, test, optimize, ship.
---
<essential_principles>
## How We Work
**The user is the product owner. Claude is the developer.**
The user does not write code. The user does not read code. The user describes what they want and judges whether the result is acceptable. Claude implements, verifies, and reports outcomes.
### 1. Prove, Don't Promise
Never say "this should work." Prove it:
```bash
xcodebuild build 2>&1 | xcsift # Build passes
xcodebuild test # Tests pass
open .../App.app # App launches
```
If you didn't run it, you don't know it works.
### 2. Tests for Correctness, Eyes for Quality
| Question | How to Answer |
|----------|---------------|
| Does the logic work? | Write test, see it pass |
| Does it look right? | Launch app, user looks at it |
| Does it feel right? | User uses it |
| Does it crash? | Test + launch |
| Is it fast enough? | Profiler |
Tests verify *correctness*. The user verifies *desirability*.
### 3. Report Outcomes, Not Code
**Bad:** "I refactored the view model to use @Observable with environment injection"
**Good:** "Fixed the state bug. App now updates correctly when you add items. Ready for you to verify."
The user doesn't care what you changed. The user cares what's different.
### 4. Small Steps, Always Verified
```
Change → Verify → Report → Next change
```
Never batch up work. Never say "I made several changes." Each change is verified before the next. If something breaks, you know exactly what caused it.
### 5. Ask Before, Not After
Unclear requirement? Ask now.
Multiple valid approaches? Ask which.
Scope creep? Ask if wanted.
Big refactor needed? Ask permission.
Wrong: Build for 30 minutes, then "is this what you wanted?"
Right: "Before I start, does X mean Y or Z?"
### 6. Always Leave It Working
Every stopping point = working state. Tests pass, app launches, changes committed. The user can walk away anytime and come back to something that works.
</essential_principles>
<swiftui_principles>
## SwiftUI Framework Principles
### Declarative Mindset
Describe what the UI should look like for a given state, not how to mutate it. Let SwiftUI manage the rendering. Never force updates - change the state and let the framework react.
### Single Source of Truth
Every piece of data has one authoritative location. Use the right property wrapper: @State for view-local, @Observable for shared objects, @Environment for app-wide. Derived data should be computed, not stored.
### Composition Over Inheritance
Build complex UIs by composing small, focused views. Extract reusable components when patterns emerge. Prefer many small views over few large ones.
### Platform-Adaptive Design
Write once but respect platform idioms. Use native navigation patterns, respect safe areas, adapt to screen sizes. Test on all target platforms.
</swiftui_principles>
<intake>
**What would you like to do?**
1. Build a new SwiftUI app
2. Debug an existing SwiftUI app
3. Add a feature to an existing app
4. Write/run tests
5. Optimize performance
6. Ship/release to App Store
7. Something else
**Then read the matching workflow from `workflows/` and follow it.**
</intake>
<routing>
| Response | Workflow |
|----------|----------|
| 1, "new", "create", "build", "start" | `workflows/build-new-app.md` |
| 2, "broken", "fix", "debug", "crash", "bug" | `workflows/debug-swiftui.md` |
| 3, "add", "feature", "implement", "change" | `workflows/add-feature.md` |
| 4, "test", "tests", "TDD", "coverage" | `workflows/write-tests.md` |
| 5, "slow", "optimize", "performance", "fast" | `workflows/optimize-performance.md` |
| 6, "ship", "release", "deploy", "publish", "app store" | `workflows/ship-app.md` |
| 7, other | Clarify, then select workflow or references |
</routing>
<verification_loop>
## After Every Change
```bash
# 1. Does it build?
xcodebuild -scheme AppName build 2>&1 | xcsift
# 2. Do tests pass? (use Core scheme for SwiftUI apps to avoid @main hang)
xcodebuild -scheme AppNameCore test
# 3. Does it launch?
# macOS:
open ./build/Build/Products/Debug/AppName.app
# iOS Simulator:
xcrun simctl boot "iPhone 15 Pro" 2>/dev/null || true
xcrun simctl install booted ./build/Build/Products/Debug-iphonesimulator/AppName.app
xcrun simctl launch booted com.yourcompany.appname
```
Note: If tests hang, the test target likely depends on the app target which has `@main`. Extract testable code to a framework target. See `../macos-apps/references/testing-tdd.md` for the pattern.
Report to the user:
- "Build: ✓"
- "Tests: 12 pass, 0 fail"
- "App launches, ready for you to check [specific thing]"
</verification_loop>
<cli_infrastructure>
## CLI Workflow References
For building, debugging, testing, and shipping from CLI without opening Xcode, read these from `../macos-apps/references/`:
| Reference | Use For |
|-----------|---------|
| `cli-workflow.md` | Build, run, test commands; xcodebuild usage; code signing |
| `cli-observability.md` | Log streaming, crash analysis, memory debugging, LLDB |
| `project-scaffolding.md` | XcodeGen project.yml templates, file structure, entitlements |
| `testing-tdd.md` | Test patterns that work from CLI, avoiding @main hangs |
These docs are platform-agnostic. For iOS, change destinations:
```bash
# iOS Simulator
xcodebuild -scheme AppName -destination 'platform=iOS Simulator,name=iPhone 15 Pro' build
# macOS
xcodebuild -scheme AppName build
```
</cli_infrastructure>
<reference_index>
## Domain Knowledge
All in `references/`:
**Core:**
- architecture.md - MVVM patterns, project structure, dependency injection
- state-management.md - Property wrappers, @Observable, data flow
- layout-system.md - Stacks, grids, GeometryReader, custom layouts
**Navigation & Animation:**
- navigation.md - NavigationStack, sheets, tabs, deep linking
- animations.md - Built-in animations, transitions, matchedGeometryEffect
**Data & Platform:**
- swiftdata.md - Persistence, @Model, @Query, CloudKit sync
- platform-integration.md - iOS/macOS/watchOS/visionOS specifics
- uikit-appkit-interop.md - UIViewRepresentable, hosting controllers
**Support:**
- networking-async.md - async/await, .task modifier, API clients
- testing-debugging.md - Previews, unit tests, UI tests, debugging
- performance.md - Profiling, lazy loading, view identity
</reference_index>
<workflows_index>
## Workflows
All in `workflows/`:
| Workflow | Purpose |
|----------|---------|
| build-new-app.md | Create new SwiftUI app from scratch |
| debug-swiftui.md | Find and fix SwiftUI bugs |
| add-feature.md | Add functionality to existing app |
| write-tests.md | Write UI and unit tests |
| optimize-performance.md | Profile and improve performance |
| ship-app.md | App Store submission, TestFlight, distribution |
</workflows_index>
<canonical_terminology>
## Terminology
Use these terms consistently:
- **view** (not: widget, component, element)
- **@Observable** (not: ObservableObject, @Published for new iOS 17+ code)
- **NavigationStack** (not: NavigationView - deprecated)
- **SwiftData** (not: Core Data for new projects)
- **@Environment** (not: @EnvironmentObject for new code)
- **modifier** (not: method/function when describing view modifiers)
- **body** (not: render/build when describing view body)
</canonical_terminology>

View file

@ -1,921 +0,0 @@
<overview>
SwiftUI animations are declarative and state-driven. When state changes, SwiftUI automatically animates views from old to new values. Your role is to control timing curves, duration, and which state changes trigger animations.
Key insight: Animations are automatic when state changes - you control timing/curve, not the mechanics.
This file covers:
- Implicit vs explicit animations
- Spring animations (iOS 17+ duration/bounce API)
- Transitions for appearing/disappearing views
- matchedGeometryEffect for hero animations
- PhaseAnimator and KeyframeAnimator (iOS 17+)
- Gesture-driven animations
See also:
- navigation.md for NavigationStack transitions
- performance.md for animation optimization strategies
</overview>
<implicit_animations>
## Implicit Animations (.animation modifier)
Implicit animations apply whenever an animatable property changes on a view. Always specify which value triggers the animation using the `value:` parameter to prevent unexpected animations.
**Basic usage:**
```swift
struct ContentView: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Circle()
.fill(.blue)
.scaleEffect(scale)
.animation(.spring(), value: scale)
.onTapGesture {
scale = scale == 1.0 ? 1.5 : 1.0
}
}
}
```
**Animation types:**
- `.default` - System default spring animation
- `.linear(duration:)` - Constant speed from start to finish
- `.easeIn(duration:)` - Starts slow, accelerates
- `.easeOut(duration:)` - Starts fast, decelerates
- `.easeInOut(duration:)` - Slow start and end, fast middle
- `.spring()` - iOS 17+ spring with default parameters
- `.bouncy` - Preset spring with high bounce
- `.snappy` - Preset spring with quick, slight bounce
- `.smooth` - Preset spring with no bounce
**Value-specific animation:**
```swift
struct MultiPropertyView: View {
@State private var rotation: Double = 0
@State private var scale: CGFloat = 1.0
var body: some View {
Rectangle()
.fill(.red)
.scaleEffect(scale)
.rotationEffect(.degrees(rotation))
.animation(.spring(), value: rotation) // Only animate rotation
.animation(.easeInOut, value: scale) // Different animation for scale
}
}
```
**Why always use value: parameter:**
- Prevents unexpected animations on unrelated state changes
- Device rotation won't trigger animations
- More predictable behavior
- Better performance (only tracks specific value)
</implicit_animations>
<explicit_animations>
## Explicit Animations (withAnimation)
Explicit animations only affect properties that depend on values changed inside the `withAnimation` closure. Preferred for user-triggered actions.
**Basic usage:**
```swift
struct ContentView: View {
@State private var isExpanded = false
var body: some View {
VStack {
if isExpanded {
Text("Details")
.transition(.opacity)
}
Button("Toggle") {
withAnimation(.spring()) {
isExpanded.toggle()
}
}
}
}
}
```
**Completion handlers (iOS 17+):**
```swift
Button("Animate") {
withAnimation(.easeInOut(duration: 1.0)) {
offset.y = 200
} completion: {
// Animation finished - safe to perform next action
showNextStep = true
}
}
```
**Transaction-based:**
```swift
var transaction = Transaction(animation: .spring())
transaction.disablesAnimations = true // Temporarily disable animations
withTransaction(transaction) {
someState.toggle()
}
```
**Removing animations temporarily:**
```swift
withAnimation(nil) {
// Changes happen immediately without animation
resetState()
}
```
</explicit_animations>
<spring_animations>
## Spring Animations
Springs are the default animation in SwiftUI. They feel natural because they mimic real-world physics.
**Modern spring parameters (iOS 17+):**
```swift
// Duration and bounce control
.spring(duration: 0.5, bounce: 0.3)
// No bounce with blend duration for smooth transitions
.spring(duration: 0.5, bounce: 0, blendDuration: 0.2)
// With initial velocity for gesture-driven animations
.spring(duration: 0.6, bounce: 0.4)
```
**Bounce parameter:**
- `-1.0` to `1.0` range
- `0` = no bounce (critically damped)
- `0.3` to `0.5` = natural bounce
- `0.7` to `1.0` = exaggerated bounce
- Negative values create "anticipation" (overshoots in opposite direction first)
**Presets (iOS 17+):**
```swift
.bouncy // High bounce - playful, attention-grabbing
.snappy // Quick with slight bounce - feels responsive
.smooth // No bounce - elegant, sophisticated
```
**Tuning workflow:**
1. Start with duration that feels right
2. Adjust bounce to set character/feeling
3. Use presets first, then customize if needed
**Legacy spring (still works):**
```swift
// For backward compatibility or precise control
.spring(response: 0.5, dampingFraction: 0.7, blendDuration: 0)
```
**When to use springs:**
- User interactions (button presses, drags)
- Most UI state changes
- Default choice unless you need precise timing
</spring_animations>
<transitions>
## Transitions
Transitions control how views appear and disappear. Applied with `.transition()` modifier, animated by wrapping insertion/removal in `withAnimation`.
**Built-in transitions:**
```swift
struct TransitionsDemo: View {
@State private var showDetail = false
var body: some View {
VStack {
if showDetail {
Text("Detail")
.transition(.opacity) // Fade in/out
// .transition(.slide) // Slide from leading edge
// .transition(.scale) // Grow/shrink from center
// .transition(.move(edge: .bottom)) // Slide from bottom
// .transition(.push(from: .leading)) // Push from leading (iOS 16+)
}
Button("Toggle") {
withAnimation {
showDetail.toggle()
}
}
}
}
}
```
**Combining transitions:**
```swift
// Both opacity and scale together
.transition(.opacity.combined(with: .scale))
// Different insertion and removal
.transition(.asymmetric(
insertion: .move(edge: .leading).combined(with: .opacity),
removal: .move(edge: .trailing).combined(with: .opacity)
))
```
**Custom transitions:**
```swift
struct RotateModifier: ViewModifier {
let rotation: Double
func body(content: Content) -> some View {
content
.rotationEffect(.degrees(rotation))
.opacity(rotation == 0 ? 1 : 0)
}
}
extension AnyTransition {
static var pivot: AnyTransition {
.modifier(
active: RotateModifier(rotation: -90),
identity: RotateModifier(rotation: 0)
)
}
}
// Usage
Text("Pivoting in")
.transition(.pivot)
```
**Identity vs insertion/removal:**
- `identity` = final state when view is visible
- `active` = state during transition (appearing/disappearing)
</transitions>
<matched_geometry>
## matchedGeometryEffect
Synchronizes geometry between two views with the same ID, creating hero animations. Views don't need to be in the same container.
**Basic hero animation:**
```swift
struct HeroDemo: View {
@State private var isExpanded = false
@Namespace private var animation
var body: some View {
VStack {
if !isExpanded {
// Thumbnail state
Circle()
.fill(.blue)
.frame(width: 60, height: 60)
.matchedGeometryEffect(id: "circle", in: animation)
.onTapGesture {
withAnimation(.spring()) {
isExpanded = true
}
}
} else {
// Expanded state
VStack {
Circle()
.fill(.blue)
.frame(width: 200, height: 200)
.matchedGeometryEffect(id: "circle", in: animation)
Button("Close") {
withAnimation(.spring()) {
isExpanded = false
}
}
}
}
}
}
}
```
**Creating namespace:**
```swift
@Namespace private var animation // Property wrapper creates unique namespace
```
**isSource parameter:**
Controls which view provides geometry during transition.
```swift
// Example: Grid to detail view
struct ContentView: View {
@State private var selectedItem: Item?
@Namespace private var namespace
var body: some View {
ZStack {
// Grid view
LazyVGrid(columns: columns) {
ForEach(items) { item in
ItemCard(item: item)
.matchedGeometryEffect(
id: item.id,
in: namespace,
isSource: selectedItem == nil // Source when detail not shown
)
.onTapGesture {
selectedItem = item
}
}
}
// Detail view
if let item = selectedItem {
DetailView(item: item)
.matchedGeometryEffect(
id: item.id,
in: namespace,
isSource: selectedItem != nil // Source when detail shown
)
}
}
.animation(.spring(), value: selectedItem)
}
}
```
**Properties parameter:**
Control what gets matched.
```swift
.matchedGeometryEffect(
id: "shape",
in: namespace,
properties: .frame // Only match frame, not position
)
// Options: .frame, .position, .size
```
**Common pitfalls:**
- **Both views must exist simultaneously** during animation - use conditional rendering carefully
- **Same ID required** - use stable identifiers (UUIDs, database IDs)
- **Need explicit animation** - wrap state changes in `withAnimation`
- **ZStack coordination** - often need ZStack to ensure both views render during transition
</matched_geometry>
<phased_animations>
## Phased Animations (iOS 17+)
PhaseAnimator automatically cycles through animation phases. Ideal for loading indicators, attention-grabbing effects, or multi-step sequences.
**PhaseAnimator with continuous cycling:**
```swift
struct PulsingCircle: View {
var body: some View {
PhaseAnimator([false, true]) { isLarge in
Circle()
.fill(.red)
.scaleEffect(isLarge ? 1.5 : 1.0)
.opacity(isLarge ? 0.5 : 1.0)
} animation: { phase in
.easeInOut(duration: 1.0)
}
}
}
```
**PhaseAnimator with enum phases:**
```swift
enum LoadingPhase: CaseIterable {
case initial, loading, success
var scale: CGFloat {
switch self {
case .initial: 1.0
case .loading: 1.2
case .success: 1.5
}
}
var color: Color {
switch self {
case .initial: .gray
case .loading: .blue
case .success: .green
}
}
}
struct LoadingButton: View {
var body: some View {
PhaseAnimator(LoadingPhase.allCases) { phase in
Circle()
.fill(phase.color)
.scaleEffect(phase.scale)
} animation: { phase in
switch phase {
case .initial: .easeIn(duration: 0.3)
case .loading: .easeInOut(duration: 0.5)
case .success: .spring(duration: 0.6, bounce: 0.4)
}
}
}
}
```
**Trigger-based PhaseAnimator:**
```swift
struct TriggerDemo: View {
@State private var triggerValue = 0
var body: some View {
VStack {
PhaseAnimator([0, 1, 2], trigger: triggerValue) { phase in
RoundedRectangle(cornerRadius: 12)
.fill(.blue)
.frame(width: 100 + CGFloat(phase * 50), height: 100)
.offset(x: CGFloat(phase * 20))
}
Button("Animate") {
triggerValue += 1
}
}
}
}
```
**Use cases:**
- Loading spinners and progress indicators
- Attention-grabbing call-to-action buttons
- Celebratory success animations
- Idle state animations
- Tutorial highlights
</phased_animations>
<keyframe_animations>
## Keyframe Animations (iOS 17+)
KeyframeAnimator provides frame-by-frame control over complex animations. More powerful than PhaseAnimator when you need precise timing and multiple simultaneous property changes.
**Basic KeyframeAnimator:**
```swift
struct AnimationValues {
var scale = 1.0
var rotation = 0.0
var opacity = 1.0
}
struct KeyframeDemo: View {
@State private var trigger = false
var body: some View {
KeyframeAnimator(
initialValue: AnimationValues(),
trigger: trigger
) { values in
Rectangle()
.fill(.purple)
.scaleEffect(values.scale)
.rotationEffect(.degrees(values.rotation))
.opacity(values.opacity)
.frame(width: 100, height: 100)
} keyframes: { _ in
KeyframeTrack(\.scale) {
SpringKeyframe(1.5, duration: 0.3)
CubicKeyframe(0.8, duration: 0.2)
CubicKeyframe(1.0, duration: 0.2)
}
KeyframeTrack(\.rotation) {
LinearKeyframe(180, duration: 0.4)
CubicKeyframe(360, duration: 0.3)
}
KeyframeTrack(\.opacity) {
CubicKeyframe(0.5, duration: 0.3)
CubicKeyframe(1.0, duration: 0.4)
}
}
.onTapGesture {
trigger.toggle()
}
}
}
```
**Keyframe types:**
```swift
// Linear - constant speed interpolation
LinearKeyframe(targetValue, duration: 0.5)
// Cubic - smooth Bezier curve
CubicKeyframe(targetValue, duration: 0.5)
// Spring - physics-based bounce
SpringKeyframe(targetValue, duration: 0.5, spring: .bouncy)
// Move - jump immediately to value
MoveKeyframe(targetValue)
```
**Complex multi-property animation:**
```swift
struct AnimationState {
var position: CGPoint = .zero
var color: Color = .blue
var size: CGFloat = 50
}
KeyframeAnimator(initialValue: AnimationState(), trigger: animate) { state in
Circle()
.fill(state.color)
.frame(width: state.size, height: state.size)
.position(state.position)
} keyframes: { _ in
KeyframeTrack(\.position) {
CubicKeyframe(CGPoint(x: 200, y: 100), duration: 0.4)
SpringKeyframe(CGPoint(x: 200, y: 300), duration: 0.6)
CubicKeyframe(CGPoint(x: 0, y: 0), duration: 0.5)
}
KeyframeTrack(\.color) {
CubicKeyframe(.red, duration: 0.5)
CubicKeyframe(.green, duration: 0.5)
CubicKeyframe(.blue, duration: 0.5)
}
KeyframeTrack(\.size) {
SpringKeyframe(100, duration: 0.6, spring: .bouncy)
CubicKeyframe(50, duration: 0.4)
}
}
```
**When to use KeyframeAnimator:**
- Complex choreographed animations
- Precise timing control needed
- Multiple properties animating with different curves
- Path-based animations
- Recreating motion design prototypes
</keyframe_animations>
<gesture_animations>
## Gesture-Driven Animations
Interactive animations that respond to user input in real-time.
**DragGesture with spring animation:**
```swift
struct DraggableCard: View {
@State private var offset: CGSize = .zero
var body: some View {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(width: 200, height: 300)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
offset = .zero
}
}
)
}
}
```
**Interruptible animations:**
```swift
struct InterruptibleView: View {
@State private var position: CGFloat = 0
var body: some View {
Circle()
.fill(.red)
.frame(width: 60, height: 60)
.offset(y: position)
.animation(.spring(), value: position)
.gesture(
DragGesture()
.onChanged { value in
// Interrupts ongoing animation immediately
position = value.translation.height
}
.onEnded { value in
// Determine snap point based on velocity
let velocity = value.predictedEndLocation.y - value.location.y
if abs(velocity) > 500 {
position = velocity > 0 ? 300 : -300
} else {
position = 0
}
}
)
}
}
```
**GestureState for automatic reset:**
```swift
struct GestureStateExample: View {
@GestureState private var dragOffset: CGSize = .zero
@State private var permanentOffset: CGSize = .zero
var body: some View {
Rectangle()
.fill(.purple)
.frame(width: 100, height: 100)
.offset(x: permanentOffset.width + dragOffset.width,
y: permanentOffset.height + dragOffset.height)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
withAnimation(.spring()) {
permanentOffset.width += value.translation.width
permanentOffset.height += value.translation.height
}
}
)
}
}
```
**Combining gestures with animations:**
```swift
struct SwipeToDelete: View {
@State private var offset: CGFloat = 0
@State private var isDeleted = false
var body: some View {
if !isDeleted {
HStack {
Text("Swipe to delete")
Spacer()
}
.padding()
.background(.white)
.offset(x: offset)
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width < 0 {
offset = value.translation.width
}
}
.onEnded { value in
if offset < -100 {
withAnimation(.easeOut(duration: 0.3)) {
offset = -500
} completion: {
isDeleted = true
}
} else {
withAnimation(.spring()) {
offset = 0
}
}
}
)
}
}
}
```
**Velocity-based animations:**
```swift
struct VelocityDrag: View {
@State private var offset: CGSize = .zero
var body: some View {
Circle()
.fill(.green)
.frame(width: 80, height: 80)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { value in
let velocity = value.velocity
// Use velocity magnitude to determine spring response
let speed = sqrt(velocity.width * velocity.width +
velocity.height * velocity.height)
let animation: Animation = speed > 1000
? .spring(duration: 0.4, bounce: 0.5)
: .spring(duration: 0.6, bounce: 0.3)
withAnimation(animation) {
offset = .zero
}
}
)
}
}
```
</gesture_animations>
<decision_tree>
## Choosing the Right Animation
**Simple state change:**
- Use `.animation(.default, value: state)` for single property changes
- Implicit animation is simplest approach
**User-triggered change:**
- Use `withAnimation { }` for button taps, user actions
- Explicit animation provides better control
- Use completion handlers (iOS 17+) for sequential actions
**View appearing/disappearing:**
- Use `.transition()` for conditional views
- Combine with `withAnimation` to trigger
- Consider `.asymmetric()` for different in/out animations
**Shared element between screens:**
- Use `matchedGeometryEffect` for hero animations
- Requires both views to exist during transition
- Best with `@Namespace` and explicit animations
**Multi-step sequence:**
- Use `PhaseAnimator` (iOS 17+) for simple phase-based sequences
- Great for loading states, idle animations
- Trigger-based for user-initiated sequences
**Complex keyframed motion:**
- Use `KeyframeAnimator` (iOS 17+) for precise timing
- Multiple properties with independent curves
- Recreating motion design specs
**User-controlled motion:**
- Use `DragGesture` + animation for interactive elements
- `@GestureState` for automatic state reset
- Consider velocity for natural physics
**Performance tips:**
- Animate opacity, scale, offset (cheap)
- Avoid animating frame size, padding (expensive)
- Use `.drawingGroup()` for complex hierarchies being animated
- Avoid animating during scroll (competes with scroll performance)
- Profile with Instruments if animations drop frames
</decision_tree>
<anti_patterns>
## What NOT to Do
<anti_pattern name="Animation without value parameter">
**Problem:**
```swift
.animation(.spring()) // No value parameter
```
**Why it's bad:**
Animates every property change, including device rotation, parent view updates, and unrelated state changes. Creates unexpected animations and performance issues.
**Instead:**
```swift
.animation(.spring(), value: specificState)
```
</anti_pattern>
<anti_pattern name="Animating layout-heavy properties">
**Problem:**
```swift
withAnimation {
frameWidth = 300 // Triggers layout recalculation
padding = 20 // Triggers layout recalculation
}
```
**Why it's bad:**
Frame size and padding changes force SwiftUI to recalculate layout, which is expensive. Can cause stuttering on complex views.
**Instead:**
```swift
withAnimation {
scale = 1.5 // Cheap transform
opacity = 0.5 // Cheap property
offset = CGSize(width: 20, height: 0) // Cheap transform
}
```
</anti_pattern>
<anti_pattern name="matchedGeometryEffect without namespace">
**Problem:**
```swift
Circle()
.matchedGeometryEffect(id: "circle", in: ???) // Forgot @Namespace
```
**Why it's bad:**
Won't compile. Namespace is required to coordinate geometry matching.
**Instead:**
```swift
@Namespace private var animation
Circle()
.matchedGeometryEffect(id: "circle", in: animation)
```
</anti_pattern>
<anti_pattern name="Nested withAnimation blocks">
**Problem:**
```swift
withAnimation(.easeIn) {
withAnimation(.spring()) {
state = newValue
}
}
```
**Why it's bad:**
Inner animation is ignored. Only outer animation applies. Creates confusion about which animation runs.
**Instead:**
```swift
withAnimation(.spring()) {
state = newValue
}
```
</anti_pattern>
<anti_pattern name="Transition without withAnimation">
**Problem:**
```swift
if showDetail {
DetailView()
.transition(.slide) // Transition defined but not triggered
}
```
**Why it's bad:**
View appears/disappears instantly. Transition is never applied without animation context.
**Instead:**
```swift
Button("Toggle") {
withAnimation {
showDetail.toggle()
}
}
```
</anti_pattern>
<anti_pattern name="Animating computed properties">
**Problem:**
```swift
var computedValue: Double {
return stateA * stateB
}
.animation(.spring(), value: computedValue)
```
**Why it's bad:**
Computed properties can change for many reasons. Animation triggers on any dependency change, not just intentional updates.
**Instead:**
```swift
.animation(.spring(), value: stateA)
.animation(.spring(), value: stateB)
```
</anti_pattern>
<anti_pattern name="matchedGeometryEffect with overlapping views">
**Problem:**
```swift
// Both views exist at same time with same ID
GridItem()
.matchedGeometryEffect(id: item.id, in: namespace)
DetailItem()
.matchedGeometryEffect(id: item.id, in: namespace)
```
**Why it's bad:**
Without proper `isSource` configuration, SwiftUI doesn't know which view's geometry to use. Creates unpredictable animations.
**Instead:**
```swift
GridItem()
.matchedGeometryEffect(id: item.id, in: namespace, isSource: selectedItem == nil)
DetailItem()
.matchedGeometryEffect(id: item.id, in: namespace, isSource: selectedItem != nil)
```
</anti_pattern>
</anti_patterns>

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,214 +0,0 @@
<overview>
SwiftUI networking in 2025 is built around Swift's structured concurrency (async/await) with the @Observable macro for state management. Combine is primarily used for specialized reactive scenarios.
**When to use async/await:**
- Loading data when views appear (.task modifier)
- Sequential API calls with dependencies
- Error handling with do-catch
- Any new code requiring async operations
**When Combine is still useful:**
- Complex reactive pipelines (debouncing, throttling)
- Form validation with multiple interdependent fields
- Real-time data streams (websockets, timers)
**Core principle:** Use async/await by default. Add Combine only when reactive operators provide clear value.
</overview>
<task_modifier>
## The .task Modifier
**Basic usage:**
```swift
struct ArticleView: View {
@State private var article: Article?
let articleID: String
var body: some View {
content
.task {
article = try? await fetchArticle(id: articleID)
}
}
}
```
**With dependency (.task(id:)):**
```swift
struct SearchView: View {
@State private var query = ""
@State private var results: [Result] = []
var body: some View {
List(results) { result in Text(result.name) }
.searchable(text: $query)
.task(id: query) {
guard !query.isEmpty else { return }
try? await Task.sleep(for: .milliseconds(300))
guard !Task.isCancelled else { return }
results = (try? await search(query: query)) ?? []
}
}
}
```
**Key behaviors:**
- Runs when view appears
- Auto-cancels on view disappear
- .task(id:) restarts when dependency changes
</task_modifier>
<async_await_patterns>
## Async/Await Patterns
**Loading with @Observable:**
```swift
@Observable
@MainActor
class ArticleViewModel {
private(set) var state: LoadingState<Article> = .idle
func load(id: String) async {
state = .loading
do {
let article = try await apiClient.fetchArticle(id: id)
state = .loaded(article)
} catch is CancellationError {
// Don't update state
} catch {
state = .failed(error)
}
}
}
```
**Parallel calls:**
```swift
func loadProfile(id: String) async throws -> Profile {
let user = try await fetchUser(id: id)
async let posts = fetchPosts(userID: user.id)
async let followers = fetchFollowers(userID: user.id)
return Profile(user: user, posts: try await posts, followers: try await followers)
}
```
</async_await_patterns>
<api_client_design>
## API Client Architecture
```swift
protocol APIClient {
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T
}
@MainActor
final class ProductionAPIClient: APIClient {
private let baseURL: URL
private let session: URLSession
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
let request = try buildRequest(endpoint)
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw APIError.httpError((response as? HTTPURLResponse)?.statusCode ?? 0)
}
return try JSONDecoder().decode(T.self, from: data)
}
}
```
</api_client_design>
<loading_states>
## Loading States
```swift
enum LoadingState<Value> {
case idle
case loading
case loaded(Value)
case failed(Error)
var isLoading: Bool {
if case .loading = self { return true }
return false
}
}
struct AsyncContentView<Value, Content: View>: View {
let state: LoadingState<Value>
let retry: () async -> Void
@ViewBuilder let content: (Value) -> Content
var body: some View {
switch state {
case .idle: Color.clear
case .loading: ProgressView()
case .loaded(let value): content(value)
case .failed(let error):
ContentUnavailableView("Error", systemImage: "exclamationmark.triangle", description: Text(error.localizedDescription))
}
}
}
```
</loading_states>
<error_handling>
## Error Handling & Retry
**Basic retry:**
```swift
func fetchWithRetry<T>(maxRetries: Int = 3, operation: () async throws -> T) async throws -> T {
var lastError: Error?
for attempt in 0..<maxRetries {
do {
return try await operation()
} catch {
lastError = error
if error is CancellationError { throw error }
if attempt < maxRetries - 1 {
try await Task.sleep(for: .seconds(pow(2, Double(attempt))))
}
}
}
throw lastError!
}
```
</error_handling>
<decision_tree>
## Choosing the Right Approach
**Tied to view lifecycle?** → .task or .task(id:)
**User-triggered?** → Wrap in explicit Task {}
**Need reactive operators?** → Combine
**Loading data?** → Use LoadingState enum
**Sequential calls?** → async/await naturally
**Parallel calls?** → async let or TaskGroup
</decision_tree>
<anti_patterns>
## What NOT to Do
<anti_pattern name="Ignoring CancellationError">
**Problem:** Showing error UI when task is cancelled
**Instead:** Catch CancellationError separately, don't update state
</anti_pattern>
<anti_pattern name="Task in .task">
**Problem:** Task { await loadData() } inside .task
**Instead:** .task already creates a Task
</anti_pattern>
<anti_pattern name="Missing @MainActor">
**Problem:** View model updates from background thread
**Instead:** Mark @Observable view models with @MainActor
</anti_pattern>
<anti_pattern name="ObservableObject for new code">
**Problem:** Using ObservableObject/@Published
**Instead:** Use @Observable macro (iOS 17+)
</anti_pattern>
</anti_patterns>

File diff suppressed because it is too large Load diff

View file

@ -1,204 +0,0 @@
<overview>
SwiftUI enables true multiplatform development: write once, adapt per platform. A single codebase can target iOS, iPadOS, macOS, watchOS, tvOS, and visionOS while respecting each platform's unique conventions and capabilities.
**Key insight:** SwiftUI's declarative syntax works everywhere, but each platform has distinct interaction models. iOS uses touch and gestures, macOS has precise mouse input and keyboard shortcuts, watchOS centers on the Digital Crown, and visionOS introduces spatial computing with gaze and hand tracking.
**When to read this:**
- Building multiplatform apps with shared logic but platform-specific UI
- Implementing macOS menu bar utilities or Settings windows
- Creating watchOS complications or Digital Crown interactions
- Developing visionOS apps with immersive spaces and ornaments
- Adapting layouts responsively across iPhone, iPad, and Mac
</overview>
<platform_conditionals>
## Platform Conditionals
**Compile-time platform checks:**
```swift
#if os(iOS)
// iOS-only code
#elseif os(macOS)
// macOS-only code
#elseif os(watchOS)
// watchOS-only code
#elseif os(visionOS)
// visionOS-only code
#endif
```
**Runtime API availability:**
```swift
if #available(iOS 17, macOS 14, *) {
// Use iOS 17+/macOS 14+ API
}
```
**Target environment:**
```swift
#if targetEnvironment(simulator)
// Running in simulator
#endif
#if canImport(UIKit)
// UIKit available
#endif
```
</platform_conditionals>
<ios_specifics>
## iOS-Specific Features
**Navigation patterns:**
- Tab bar at bottom
- Full-screen covers
- Pull-to-refresh with .refreshable
**System integration:**
- Push notifications
- Widgets and Live Activities
- App Intents / Siri
**Device variations:**
```swift
@Environment(\.horizontalSizeClass) var horizontalSizeClass
if horizontalSizeClass == .regular {
// iPad layout
}
```
</ios_specifics>
<macos_specifics>
## macOS-Specific Features
**Window management:**
```swift
WindowGroup("Main") { ContentView() }
.defaultSize(width: 800, height: 600)
Window("Settings") { SettingsView() }
Settings { SettingsView() }
```
**MenuBarExtra:**
```swift
MenuBarExtra("App Name", systemImage: "star") {
MenuBarContentView()
}
.menuBarExtraStyle(.window)
```
**Commands:**
```swift
.commands {
CommandGroup(replacing: .newItem) {
Button("New Document") { }
}
CommandMenu("Custom") {
Button("Action") { }
}
}
```
</macos_specifics>
<watchos_specifics>
## watchOS-Specific Features
**Digital Crown:**
```swift
@State private var crownValue: Double = 0.0
VStack { Text("\(crownValue)") }
.focusable()
.digitalCrownRotation($crownValue)
```
**Always-on display:**
```swift
@Environment(\.isLuminanceReduced) var isLuminanceReduced
```
</watchos_specifics>
<visionos_specifics>
## visionOS-Specific Features
**Immersive spaces:**
```swift
ImmersiveSpace(id: "immersive") {
RealityView { content in
// 3D content
}
}
```
**Window styles:**
```swift
.windowStyle(.volumetric)
```
**Ornaments:**
```swift
.ornament(attachmentAnchor: .scene(.bottom)) {
BottomControls()
}
```
</visionos_specifics>
<responsive_design>
## Responsive Design
**Size classes:**
```swift
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@Environment(\.verticalSizeClass) var verticalSizeClass
```
**ViewThatFits (iOS 16+):**
```swift
ViewThatFits {
WideLayout()
CompactLayout()
}
```
**containerRelativeFrame (iOS 17+):**
```swift
.containerRelativeFrame(.horizontal) { length, axis in
length * 0.8
}
```
</responsive_design>
<decision_tree>
## Platform Strategy
**Shared codebase structure:**
- Models, ViewModels, Services: All platforms
- Views: Platform-specific where needed
**When to use conditionals:**
- Platform-exclusive APIs
- Different navigation patterns
- Different default sizes
</decision_tree>
<anti_patterns>
## What NOT to Do
<anti_pattern name="Scattered #if os() conditionals">
**Problem:** Platform checks everywhere
**Instead:** Extract to platform-specific files
</anti_pattern>
<anti_pattern name="Ignoring platform idioms">
**Problem:** iOS patterns on macOS
**Instead:** Respect each platform's conventions
</anti_pattern>
<anti_pattern name="Testing only in simulator">
**Problem:** Missing real device behaviors
**Instead:** Test on physical devices
</anti_pattern>
</anti_patterns>

File diff suppressed because it is too large Load diff

View file

@ -1,297 +0,0 @@
<overview>
SwiftData is Apple's modern persistence framework introduced at WWDC 2023, built on Core Data but with a Swift-native API. It provides declarative data modeling, automatic persistence, and seamless SwiftUI integration with minimal boilerplate.
**Key insight:** SwiftData eliminates the complexity of Core Data while maintaining its power. Where Core Data requires NSManagedObject subclasses, fetch request controllers, and entity descriptions, SwiftData uses Swift macros (@Model, @Query) and modern Swift features like #Predicate for compile-time validation.
**Minimum deployment:** iOS 17, macOS 14, watchOS 10, tvOS 17, visionOS 1.0
**When to read this file:**
- Persisting app data locally or syncing with iCloud
- Defining data models and relationships
- Querying and filtering stored data
- Migrating from Core Data to SwiftData
- Before reading: architecture.md (understand app structure), state-management.md (understand @Observable)
- Read alongside: platform-integration.md (for CloudKit integration details)
</overview>
<model_definition>
## Defining Models
**@Model macro:**
```swift
import SwiftData
@Model
class Item {
var name: String
var timestamp: Date
var isCompleted: Bool
init(name: String) {
self.name = name
self.timestamp = Date()
self.isCompleted = false
}
}
```
The @Model macro transforms a Swift class into a SwiftData model. SwiftData automatically persists all stored properties.
**Supported property types:**
- Basic types: String, Int, Double, Bool, Date, UUID, URL, Data
- Codable types (stored as JSON)
- Collections: [String], [Int], etc.
- Relationships to other @Model types
- Optionals of any above type
**@Attribute options:**
```swift
@Model
class User {
@Attribute(.unique) var id: UUID
@Attribute(.externalStorage) var profileImage: Data
@Attribute(.spotlight) var displayName: String
@Attribute(.allowsCloudEncryption) var sensitiveInfo: String
var email: String
init(id: UUID = UUID(), displayName: String, email: String) {
self.id = id
self.displayName = displayName
self.email = email
self.profileImage = Data()
self.sensitiveInfo = ""
}
}
```
**@Transient for non-persisted properties:**
```swift
@Model
class Task {
var title: String
var createdAt: Date
@Transient var isEditing: Bool = false
var ageInDays: Int {
Calendar.current.dateComponents([.day], from: createdAt, to: Date()).day ?? 0
}
init(title: String) {
self.title = title
self.createdAt = Date()
}
}
```
</model_definition>
<relationships>
## Relationships
**One-to-many:**
```swift
@Model
class Folder {
var name: String
@Relationship(deleteRule: .cascade) var items: [Item] = []
init(name: String) {
self.name = name
}
}
@Model
class Item {
var name: String
var folder: Folder?
init(name: String, folder: Folder? = nil) {
self.name = name
self.folder = folder
}
}
```
**Delete rules:**
- `.cascade` - deletes related objects
- `.nullify` - sets relationship to nil (default)
- `.deny` - prevents deletion if relationship exists
- `.noAction` - does nothing (use with caution)
**Inverse relationships:**
```swift
@Model
class Author {
var name: String
@Relationship(inverse: \Book.author) var books: [Book] = []
init(name: String) {
self.name = name
}
}
```
</relationships>
<model_container>
## ModelContainer and ModelContext
**Setting up container in App:**
```swift
import SwiftUI
import SwiftData
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [Item.self, Folder.self])
}
}
```
**Custom configuration:**
```swift
let config = ModelConfiguration(
schema: Schema([Item.self, Folder.self]),
url: URL.documentsDirectory.appending(path: "MyApp.store"),
cloudKitDatabase: .automatic
)
let container = try ModelContainer(
for: Item.self,
configurations: config
)
```
**Accessing context in views:**
```swift
@Environment(\.modelContext) private var context
```
</model_container>
<querying>
## Querying Data
**@Query in views:**
```swift
@Query var items: [Item]
// With sorting
@Query(sort: \Item.timestamp, order: .reverse) var items: [Item]
// With filtering
@Query(filter: #Predicate<Item> { $0.isCompleted == false }) var items: [Item]
```
**Dynamic queries:**
```swift
struct SearchableItemList: View {
@Query var items: [Item]
init(searchText: String) {
let predicate = #Predicate<Item> { item in
searchText.isEmpty || item.name.localizedStandardContains(searchText)
}
_items = Query(filter: predicate)
}
}
```
**FetchDescriptor for context queries:**
```swift
let descriptor = FetchDescriptor<Item>(
predicate: #Predicate { $0.isCompleted },
sortBy: [SortDescriptor(\.timestamp)]
)
let items = try context.fetch(descriptor)
```
</querying>
<crud_operations>
## CRUD Operations
**Create:**
```swift
let item = Item(name: "New Item")
context.insert(item)
```
**Update:**
```swift
item.name = "Updated Name"
// Changes auto-save
```
**Delete:**
```swift
context.delete(item)
```
**Manual save:**
```swift
try context.save()
```
</crud_operations>
<cloudkit_sync>
## CloudKit Sync
**Enable in container:**
```swift
let config = ModelConfiguration(cloudKitDatabase: .automatic)
```
**CloudKit constraints:**
- Cannot use @Attribute(.unique) with CloudKit
- All properties need defaults or be optional
- Relationships must be optional
- Private database only
</cloudkit_sync>
<migration>
## Schema Migration
**Lightweight migration (automatic):**
- Adding properties with defaults
- Removing properties
- Renaming with @Attribute(originalName:)
**Schema versioning:**
```swift
enum SchemaV1: VersionedSchema {
static var versionIdentifier = Schema.Version(1, 0, 0)
static var models: [any PersistentModel.Type] { [Item.self] }
}
```
</migration>
<decision_tree>
## Choosing Your Approach
**New project, iOS 17+ only:** SwiftData
**Need iOS 16 support:** Core Data
**Existing Core Data project:** Keep Core Data unless full migration planned
**Need CloudKit:** SwiftData (simpler) or Core Data (more control)
</decision_tree>
<anti_patterns>
## What NOT to Do
<anti_pattern name="Using @Query outside SwiftUI views">
**Problem:** @Query requires SwiftUI environment
**Instead:** Use FetchDescriptor with explicit context in view models
</anti_pattern>
<anti_pattern name="Using @Attribute(.unique) with CloudKit">
**Problem:** Silently breaks CloudKit sync
**Instead:** Handle uniqueness in app logic
</anti_pattern>
<anti_pattern name="Transient properties in predicates">
**Problem:** Compiles but crashes at runtime
**Instead:** Use persisted properties for filtering
</anti_pattern>
</anti_patterns>

View file

@ -1,247 +0,0 @@
<overview>
Testing and debugging SwiftUI apps requires a multi-layered approach combining previews, unit tests, UI tests, and debugging tools. SwiftUI's declarative nature makes traditional debugging challenging, but modern tools provide robust solutions.
**Key principles:**
- Use #Preview macros for rapid visual iteration
- Test business logic with @Observable view models (not views directly)
- Write focused UI tests using accessibility identifiers
- Profile with Instruments on real devices
SwiftUI views cannot be unit tested directly. Test view models and use UI automation tests for interaction testing.
</overview>
<previews>
## Xcode Previews
**Basic #Preview:**
```swift
#Preview {
ContentView()
}
#Preview("Dark Mode") {
ContentView()
.preferredColorScheme(.dark)
}
```
**Multiple states:**
```swift
#Preview("Empty") { TaskListView(tasks: []) }
#Preview("Loaded") { TaskListView(tasks: Task.sampleData) }
#Preview("Error") { TaskListView(tasks: [], error: "Network unavailable") }
```
**With @Binding (Xcode 16+):**
```swift
#Preview {
@Previewable @State var isOn = true
ToggleView(isOn: $isOn)
}
```
**Mock data:**
```swift
extension Task {
static let sampleData: [Task] = [
Task(title: "Review PR", isCompleted: false),
Task(title: "Write tests", isCompleted: true)
]
}
```
</previews>
<unit_testing>
## Unit Testing View Models
**Testing @Observable with Swift Testing:**
```swift
import Testing
@testable import MyApp
@Test("Login validation")
func loginValidation() {
let viewModel = LoginViewModel()
viewModel.email = ""
viewModel.password = "password123"
#expect(!viewModel.isValidInput)
viewModel.email = "user@example.com"
#expect(viewModel.isValidInput)
}
@Test("Async data loading")
func dataLoading() async {
let mockService = MockService()
let viewModel = TaskViewModel(service: mockService)
await viewModel.load()
#expect(!viewModel.tasks.isEmpty)
}
```
**Dependency injection for testing:**
```swift
@Observable
final class TaskViewModel {
private let service: TaskServiceProtocol
init(service: TaskServiceProtocol = TaskService()) {
self.service = service
}
}
```
</unit_testing>
<ui_testing>
## UI Testing
**Setting accessibility identifiers:**
```swift
TextField("Email", text: $email)
.accessibilityIdentifier("emailField")
Button("Login") { }
.accessibilityIdentifier("loginButton")
```
**Writing UI tests:**
```swift
import XCTest
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
let emailField = app.textFields["emailField"]
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap()
emailField.typeText("user@example.com")
loginButton.tap()
let welcomeText = app.staticTexts["welcomeMessage"]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5))
}
}
```
</ui_testing>
<debugging>
## Debugging Techniques
**_printChanges():**
```swift
var body: some View {
let _ = Self._printChanges()
VStack { /* content */ }
}
```
**View hierarchy debugger:**
Debug menu → View Debugging → Capture View Hierarchy
**Lifecycle debugging:**
```swift
.onAppear { print("View appeared") }
.onDisappear { print("View disappeared") }
.task { print("Task started") }
```
**Visual debugging:**
```swift
.border(.red)
.background(.yellow.opacity(0.3))
```
</debugging>
<instruments>
## Instruments Profiling
**SwiftUI template (Xcode 16+):**
- View Body: Track view creation count
- View Properties: Monitor property changes
- Core Animation Commits: Animation performance
**Time Profiler:**
1. Product → Profile (Cmd+I)
2. Select Time Profiler
3. Record while using app
4. Sort by "Self" time to find hotspots
**Allocations:**
- Track memory usage
- Filter by "Persistent" to find leaks
**Always profile on real devices, not simulators.**
</instruments>
<common_bugs>
## Common SwiftUI Bugs
**View not updating:**
```swift
// Problem: missing @State
var count = 0 // Won't trigger updates
// Fix: use @State
@State private var count = 0
```
**ForEach crash on empty binding:**
```swift
// Problem: binding crashes on empty
ForEach($items) { $item in }
// Fix: check for empty
if !items.isEmpty {
ForEach($items) { $item in }
}
```
**Animation not working:**
```swift
// Problem: no value parameter
.animation(.spring())
// Fix: specify value
.animation(.spring(), value: isExpanded)
```
</common_bugs>
<decision_tree>
## Testing Strategy
**Preview:** Visual iteration, different states
**Unit Test:** @Observable view models, business logic
**UI Test:** Critical user flows, login, checkout
**Manual Test:** Animations, accessibility, performance
</decision_tree>
<anti_patterns>
## What NOT to Do
<anti_pattern name="Testing view bodies">
**Problem:** Trying to unit test SwiftUI views directly
**Instead:** Extract logic to view models, test those
</anti_pattern>
<anti_pattern name="Missing accessibility identifiers">
**Problem:** Using text to find elements in UI tests
**Instead:** Use .accessibilityIdentifier("stableId")
</anti_pattern>
<anti_pattern name="No dependency injection">
**Problem:** Hardcoded dependencies in view models
**Instead:** Use protocols, inject mocks in tests
</anti_pattern>
</anti_patterns>

View file

@ -1,218 +0,0 @@
<overview>
SwiftUI wraps UIKit on iOS and AppKit on macOS. Interoperability enables using UIKit/AppKit features not yet available in SwiftUI, and incrementally adopting SwiftUI in existing projects.
**Bridging patterns:**
- **SwiftUI → UIKit/AppKit**: UIViewRepresentable, NSViewRepresentable, UIViewControllerRepresentable
- **UIKit/AppKit → SwiftUI**: UIHostingController, NSHostingController/NSHostingView
- **Coordinator pattern**: Bridge delegates and target-action patterns to SwiftUI
**When to read this:**
- Wrapping UIKit views not available in SwiftUI
- Embedding SwiftUI in existing UIKit apps
- Handling delegate-based APIs
</overview>
<uiview_representable>
## UIViewRepresentable
**Basic structure:**
```swift
struct CustomTextField: UIViewRepresentable {
@Binding var text: String
func makeUIView(context: Context) -> UITextField {
let textField = UITextField()
textField.delegate = context.coordinator
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
if uiView.text != text {
uiView.text = text
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: CustomTextField
init(_ parent: CustomTextField) {
self.parent = parent
}
func textFieldDidChangeSelection(_ textField: UITextField) {
parent.text = textField.text ?? ""
}
}
}
```
**Lifecycle:**
- `makeUIView` - called once when created
- `updateUIView` - called when SwiftUI state changes
- `dismantleUIView` - optional cleanup
</uiview_representable>
<uiviewcontroller_representable>
## UIViewControllerRepresentable
```swift
struct ImagePicker: UIViewControllerRepresentable {
@Binding var image: UIImage?
@Environment(\.dismiss) var dismiss
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
parent.image = info[.originalImage] as? UIImage
parent.dismiss()
}
}
}
```
</uiviewcontroller_representable>
<nsview_representable>
## NSViewRepresentable (macOS)
Same pattern as UIViewRepresentable:
```swift
struct ColorWell: NSViewRepresentable {
@Binding var color: NSColor
func makeNSView(context: Context) -> NSColorWell {
let colorWell = NSColorWell()
colorWell.target = context.coordinator
colorWell.action = #selector(Coordinator.colorDidChange(_:))
return colorWell
}
func updateNSView(_ nsView: NSColorWell, context: Context) {
nsView.color = color
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject {
var parent: ColorWell
init(_ parent: ColorWell) {
self.parent = parent
}
@objc func colorDidChange(_ sender: NSColorWell) {
parent.color = sender.color
}
}
}
```
</nsview_representable>
<hosting_controller>
## UIHostingController
**Embedding SwiftUI in UIKit:**
```swift
class MainViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let swiftUIView = MySwiftUIView()
let hostingController = UIHostingController(rootView: swiftUIView)
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingController.view.topAnchor.constraint(equalTo: view.topAnchor),
hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
hostingController.didMove(toParent: self)
}
}
```
</hosting_controller>
<coordinator_pattern>
## Coordinator Pattern
**When to use:**
- Handling delegate callbacks
- Managing target-action patterns
- Bridging imperative events to SwiftUI
**Structure:**
```swift
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, SomeDelegate {
var parent: ParentView
init(_ parent: ParentView) {
self.parent = parent
}
}
```
</coordinator_pattern>
<decision_tree>
## When to Use Interop
**Use UIKit/AppKit when:**
- SwiftUI lacks the feature
- Performance critical scenarios
- Integrating existing code
**Stay with pure SwiftUI when:**
- SwiftUI has native support
- Xcode Previews matter
- Cross-platform code needed
</decision_tree>
<anti_patterns>
## What NOT to Do
<anti_pattern name="UIKit by default">
**Problem:** Using UIViewRepresentable when SwiftUI works
**Instead:** Check if SwiftUI added the feature
</anti_pattern>
<anti_pattern name="Skipping Coordinator">
**Problem:** Handling delegates without Coordinator
**Instead:** Always use Coordinator for delegate patterns
</anti_pattern>
<anti_pattern name="Memory leaks in hosting">
**Problem:** Not managing child view controller properly
**Instead:** addChild → addSubview → didMove(toParent:)
</anti_pattern>
</anti_patterns>

View file

@ -1,191 +0,0 @@
<required_reading>
**Read these reference files NOW before starting:**
1. `../macos-apps/references/cli-workflow.md` - Build, run, test from CLI
2. `references/architecture.md` - App structure, MVVM patterns
3. `references/state-management.md` - Property wrappers, @Observable
</required_reading>
<process>
## Step 1: Understand Existing Codebase
```bash
find . -name "*.swift" -type f | head -20
```
**Identify:**
- App architecture (MVVM, TCA, etc.)
- Existing patterns and conventions
- Navigation approach
- Dependency injection method
## Step 2: Plan Feature Integration
**Define scope:**
- What views needed?
- What state must be managed?
- Does it need persistence (SwiftData)?
- Does it need network calls?
- How does it connect to existing features?
## Step 3: Create Feature Module
Follow existing organization:
```
Features/
YourFeature/
Views/
YourFeatureView.swift
ViewModels/
YourFeatureViewModel.swift
Models/
YourFeatureModel.swift
```
## Step 4: Implement View Model
```swift
@Observable
final class YourFeatureViewModel {
var items: [YourModel] = []
var isLoading = false
var errorMessage: String?
private let dataService: DataService
init(dataService: DataService) {
self.dataService = dataService
}
func loadData() async {
isLoading = true
defer { isLoading = false }
do {
items = try await dataService.fetchItems()
} catch {
errorMessage = error.localizedDescription
}
}
}
```
## Step 5: Implement Views
```swift
struct YourFeatureView: View {
@State private var viewModel: YourFeatureViewModel
init(viewModel: YourFeatureViewModel) {
self.viewModel = viewModel
}
var body: some View {
List(viewModel.items) { item in
NavigationLink(value: item) {
YourItemRow(item: item)
}
}
.navigationTitle("Feature Title")
.navigationDestination(for: YourModel.self) { item in
YourFeatureDetailView(item: item)
}
.task {
await viewModel.loadData()
}
}
}
```
## Step 6: Wire Up Navigation
**NavigationStack routing:**
```swift
NavigationLink(value: NavigationDestination.yourFeature) {
Text("Go to Feature")
}
.navigationDestination(for: NavigationDestination.self) { destination in
switch destination {
case .yourFeature:
YourFeatureView(viewModel: viewModel)
}
}
```
**Sheet presentation:**
```swift
@State private var showingFeature = false
Button("Show") { showingFeature = true }
.sheet(isPresented: $showingFeature) {
NavigationStack { YourFeatureView(viewModel: viewModel) }
}
```
## Step 7: Build and Verify
```bash
# 1. Build
xcodebuild -scheme AppName build 2>&1 | xcsift
# 2. Run tests
xcodebuild -scheme AppName test 2>&1 | xcsift
# 3. Launch and monitor
# macOS:
open ./build/Build/Products/Debug/AppName.app
log stream --predicate 'subsystem == "com.yourcompany.appname"' --level debug
# iOS Simulator:
xcrun simctl boot "iPhone 15 Pro" 2>/dev/null || true
xcrun simctl install booted ./build/Build/Products/Debug-iphonesimulator/AppName.app
xcrun simctl launch booted com.yourcompany.appname
```
Report to user:
- "Build: ✓"
- "Tests: X pass, 0 fail"
- "Feature added. Ready for you to test [navigation path to feature]"
**User verifies:**
- Navigate to feature from all entry points
- Test interactions
- Check loading/error states
- Verify light and dark mode
</process>
<anti_patterns>
## Avoid These Mistakes
**Not following existing patterns:**
- Creating new navigation when project has established pattern
- Using different naming conventions
- Introducing new DI when project has standard
**Overengineering:**
- Adding abstraction that doesn't exist elsewhere
- Creating generic solutions for specific problems
- Breaking single view into dozens of tiny files prematurely
**Tight coupling:**
- Accessing other features' view models directly
- Hardcoding dependencies
- Circular dependencies between features
**Breaking existing functionality:**
- Modifying shared view models without checking all callers
- Changing navigation state structure
- Removing @Environment values other views depend on
</anti_patterns>
<success_criteria>
This workflow is complete when:
- [ ] Feature matches existing architecture patterns
- [ ] Views compose with existing navigation
- [ ] State management follows project conventions
- [ ] Dependency injection consistent with existing code
- [ ] All existing tests pass
- [ ] No compiler warnings introduced
- [ ] Error states handled gracefully
- [ ] Code follows existing naming conventions
</success_criteria>

View file

@ -1,311 +0,0 @@
<required_reading>
**Read these reference files NOW before starting:**
1. `../macos-apps/references/project-scaffolding.md` - XcodeGen templates and file structure
2. `../macos-apps/references/cli-workflow.md` - Build/run/test from CLI
3. `references/architecture.md` - MVVM patterns and project structure
4. `references/state-management.md` - Property wrappers
</required_reading>
<process>
## Step 1: Clarify Requirements
Ask the user:
- What does the app do? (core functionality)
- Which platform? (iOS, macOS, or both)
- Any specific features needed? (persistence, networking, system integration)
## Step 2: Scaffold Project with XcodeGen
```bash
# Create directory structure
mkdir -p AppName/Sources AppName/Tests AppName/Resources
cd AppName
# Create project.yml (see ../macos-apps/references/project-scaffolding.md for full template)
cat > project.yml << 'EOF'
name: AppName
options:
bundleIdPrefix: com.yourcompany
deploymentTarget:
iOS: "17.0"
macOS: "14.0"
xcodeVersion: "15.0"
createIntermediateGroups: true
targets:
AppName:
type: application
platform: iOS # or macOS, or [iOS, macOS] for multi-platform
sources: [Sources]
resources: [Resources]
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.yourcompany.appname
DEVELOPMENT_TEAM: YOURTEAMID
SWIFT_VERSION: "5.9"
AppNameTests:
type: bundle.unit-test
platform: iOS
sources: [Tests]
dependencies:
- target: AppName
schemes:
AppName:
build:
targets:
AppName: all
AppNameTests: [test]
test:
targets: [AppNameTests]
EOF
# Generate xcodeproj
xcodegen generate
# Verify
xcodebuild -list -project AppName.xcodeproj
```
## Step 3: Create Source Files
```
Sources/
├── AppNameApp.swift # App entry point
├── ContentView.swift # Main view
├── Models/
├── ViewModels/
├── Views/
│ ├── Screens/
│ └── Components/
├── Services/
└── Info.plist
```
## Step 4: Configure App Entry Point
```swift
import SwiftUI
@main
struct YourAppNameApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
## Step 5: Create Base Navigation
**Tab-based app:**
```swift
struct MainTabView: View {
var body: some View {
TabView {
HomeView()
.tabItem { Label("Home", systemImage: "house") }
SettingsView()
.tabItem { Label("Settings", systemImage: "gear") }
}
}
}
```
**Stack-based navigation:**
```swift
struct RootView: View {
var body: some View {
NavigationStack {
HomeView()
}
}
}
```
## Step 6: Implement First View Model
```swift
import Foundation
import Observation
@Observable
final class HomeViewModel {
var items: [Item] = []
var isLoading = false
var errorMessage: String?
func loadData() async {
isLoading = true
defer { isLoading = false }
do {
// items = try await service.fetchItems()
} catch {
errorMessage = error.localizedDescription
}
}
}
```
## Step 7: Create Main View
```swift
struct HomeView: View {
@State private var viewModel = HomeViewModel()
var body: some View {
List(viewModel.items) { item in
Text(item.name)
}
.navigationTitle("Home")
.overlay {
if viewModel.isLoading { ProgressView() }
}
.task {
await viewModel.loadData()
}
}
}
#Preview {
NavigationStack { HomeView() }
}
```
## Step 8: Wire Up Dependencies
```swift
@Observable
final class AppDependencies {
let apiService: APIService
static let shared = AppDependencies()
private init() {
self.apiService = APIService()
}
}
```
Inject in App:
```swift
@main
struct YourAppNameApp: App {
@State private var dependencies = AppDependencies.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(dependencies)
}
}
}
```
## Step 9: Build and Verify
```bash
# Build with error parsing
xcodebuild -scheme AppName -destination 'platform=iOS Simulator,name=iPhone 15 Pro' build 2>&1 | xcsift
# Boot simulator and install
xcrun simctl boot "iPhone 15 Pro" 2>/dev/null || true
xcrun simctl install booted ./build/Build/Products/Debug-iphonesimulator/AppName.app
# Launch and stream logs
xcrun simctl launch booted com.yourcompany.appname
log stream --predicate 'subsystem == "com.yourcompany.appname"' --level debug
```
For macOS apps:
```bash
xcodebuild -scheme AppName build 2>&1 | xcsift
open ./build/Build/Products/Debug/AppName.app
```
Report to user:
- "Build: ✓"
- "App installed on simulator, launching now"
- "Ready for you to check [specific functionality]"
</process>
<anti_patterns>
## Avoid These Mistakes
**Using NavigationView:**
```swift
// DON'T
NavigationView { ContentView() }
// DO
NavigationStack { ContentView() }
```
**Using ObservableObject for new code:**
```swift
// DON'T
class ViewModel: ObservableObject {
@Published var data = []
}
// DO
@Observable
final class ViewModel {
var data = []
}
```
**Massive views:**
```swift
// DON'T
struct HomeView: View {
var body: some View {
VStack { /* 300 lines */ }
}
}
// DO
struct HomeView: View {
var body: some View {
VStack {
HeaderComponent()
ContentList()
FooterActions()
}
}
}
```
**Missing previews:**
```swift
// Always add previews for iteration
#Preview { HomeView() }
```
**Business logic in views:**
```swift
// Move to view model
struct ProductView: View {
@State private var viewModel = ProductViewModel()
var body: some View {
Button("Buy") { Task { await viewModel.purchase() } }
}
}
```
</anti_patterns>
<success_criteria>
This workflow is complete when:
- [ ] Project builds without errors
- [ ] Folder structure matches MVVM pattern
- [ ] Navigation set up with NavigationStack or TabView
- [ ] At least one @Observable view model exists
- [ ] Dependencies injected via @Environment
- [ ] No deprecated APIs (NavigationView, ObservableObject)
- [ ] SwiftUI previews render correctly
- [ ] App launches without warnings
</success_criteria>

View file

@ -1,192 +0,0 @@
<required_reading>
**Read these reference files NOW before starting:**
1. `../macos-apps/references/cli-observability.md` - Log streaming, crash analysis, LLDB, memory debugging
2. `references/testing-debugging.md` - SwiftUI-specific debugging techniques
3. `references/state-management.md` - State management issues are #1 bug source
</required_reading>
<process>
## Step 1: Reproduce the Bug Consistently
**Isolate the issue:**
- Create minimal reproducible example
- Remove unrelated views and logic
- Test in both preview and simulator/device
**Document:**
- What action triggers it?
- Every time or intermittent?
- Which platforms/OS versions?
## Step 2: Identify Bug Category
**State Management (60% of bugs):**
- View not updating
- Infinite update loops
- @State/@Binding incorrect usage
- Missing @Observable
**Layout Issues:**
- Views not appearing
- Wrong positioning
- ScrollView/List sizing problems
**Navigation Issues:**
- Stack corruption
- Sheets not dismissing
- Deep linking breaking
**Performance Issues:**
- UI freezing
- Excessive redraws
- Memory leaks
## Step 3: Add Observability
**Add _printChanges() to suspect view:**
```swift
var body: some View {
let _ = Self._printChanges()
// rest of view
}
```
This prints exactly which property caused the view to redraw.
**Add logging for runtime visibility:**
```swift
import os
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Debug")
// In your code:
logger.debug("State changed: \(self.items.count) items")
```
**Stream logs from CLI:**
```bash
# While app is running
log stream --predicate 'subsystem == "com.yourcompany.appname"' --level debug
# Search historical logs
log show --predicate 'subsystem == "com.yourcompany.appname"' --last 1h
```
## Step 4: Check Common Causes
**State red flags:**
- Mutating @State from outside owning view
- Using @StateObject when should use @Observable
- Missing @Bindable for passing bindings
**View identity issues:**
- Array index as id when order changes
- Missing .id() when identity should reset
- Same id for different content
**Environment problems:**
- Custom @Environment not provided
- Using deprecated @EnvironmentObject
## Step 5: Apply Fix
**State fix:**
```swift
// Wrong: ObservableObject
class ViewModel: ObservableObject {
@Published var count = 0
}
// Right: @Observable
@Observable
class ViewModel {
var count = 0
}
```
**View identity fix:**
```swift
// Wrong: index as id
ForEach(items.indices, id: \.self) { index in }
// Right: stable id
ForEach(items) { item in }
```
**Navigation fix:**
```swift
// Wrong: NavigationView
NavigationView { }
// Right: NavigationStack
NavigationStack { }
```
## Step 6: Verify Fix from CLI
```bash
# 1. Rebuild
xcodebuild -scheme AppName build 2>&1 | xcsift
# 2. Run tests
xcodebuild -scheme AppName test 2>&1 | xcsift
# 3. Launch and monitor
open ./build/Build/Products/Debug/AppName.app
log stream --predicate 'subsystem == "com.yourcompany.appname"' --level debug
# 4. Check for memory leaks
leaks AppName
# 5. If crash occurred, check crash logs
ls ~/Library/Logs/DiagnosticReports/ | grep AppName
cat ~/Library/Logs/DiagnosticReports/AppName_*.ips | head -100
```
**For deep debugging, attach LLDB:**
```bash
lldb -n AppName
(lldb) breakpoint set --file ContentView.swift --line 42
(lldb) continue
```
Report to user:
- "Bug no longer reproduces after [specific fix]"
- "Tests pass: X pass, 0 fail"
- "No memory leaks detected"
- "Ready for you to verify the fix"
</process>
<anti_patterns>
## Avoid These Mistakes
**Random changes:**
- Trying property wrappers without understanding
- Adding .id(UUID()) hoping it fixes things
- Wrapping in DispatchQueue.main.async as band-aid
**Ignoring root cause:**
- Hiding warnings instead of fixing
- Working around instead of fixing architecture
**Skipping _printChanges():**
- For state bugs, this is the fastest diagnostic
- Running this FIRST saves hours
**Using deprecated APIs:**
- Fix bugs in ObservableObject? Migrate to @Observable
- NavigationView bugs? Switch to NavigationStack
**Mutating state in body:**
- Never change @State during body computation
- Move to .task, .onChange, or button actions
</anti_patterns>
<success_criteria>
This workflow is complete when:
- [ ] Bug is reproducible (or documented as intermittent)
- [ ] Root cause identified using _printChanges() or other tool
- [ ] Fix applied following SwiftUI best practices
- [ ] Bug no longer occurs
- [ ] No new bugs introduced
- [ ] Tested on all target platforms
- [ ] Console shows no related warnings
</success_criteria>

View file

@ -1,197 +0,0 @@
<required_reading>
**Read these reference files NOW before starting:**
1. `../macos-apps/references/cli-observability.md` - xctrace profiling, leak detection, memory debugging
2. `references/performance.md` - Profiling, lazy loading, view identity, optimization
3. `references/layout-system.md` - Layout containers and GeometryReader pitfalls
</required_reading>
<process>
## Step 1: Establish Performance Baseline
```bash
# Build release for accurate profiling
xcodebuild -scheme AppName -configuration Release build 2>&1 | xcsift
# List available profiling templates
xcrun xctrace list templates
# Time Profiler - CPU usage baseline
xcrun xctrace record \
--template 'Time Profiler' \
--time-limit 30s \
--output baseline-cpu.trace \
--launch -- ./build/Build/Products/Release/AppName.app/Contents/MacOS/AppName
# SwiftUI template (if available)
xcrun xctrace record \
--template 'SwiftUI' \
--time-limit 30s \
--output baseline-swiftui.trace \
--launch -- ./build/Build/Products/Release/AppName.app/Contents/MacOS/AppName
# Export trace data
xcrun xctrace export --input baseline-cpu.trace --toc
```
Document baseline: CPU usage, view update count, frame rate during slow flows.
## Step 2: Profile View Updates
Add to suspect views:
```swift
var body: some View {
let _ = Self._printChanges()
// rest of view
}
```
Check console for which properties caused invalidation.
## Step 3: Fix Unnecessary View Recreation
**Stable view identity:**
```swift
// Wrong: index as id
ForEach(items.indices, id: \.self) { }
// Right: stable id
ForEach(items) { item in
ItemRow(item: item).id(item.id)
}
```
**Isolate frequently-changing state:**
```swift
// Before: entire list recreates
struct SlowList: View {
@State private var items: [Item] = []
@State private var count: Int = 0 // Updates often
var body: some View {
List(items) { item in ItemRow(item: item) }
}
}
// After: isolate count to separate view
struct FastList: View {
@State private var items: [Item] = []
var body: some View {
VStack {
CountBadge() // Only this updates
List(items) { item in ItemRow(item: item) }
}
}
}
```
## Step 4: Optimize Lists
```swift
// Use lazy containers
ScrollView {
LazyVStack(spacing: 8) {
ForEach(items) { item in
ItemRow(item: item)
}
}
}
```
## Step 5: Reduce Layout Passes
```swift
// Avoid GeometryReader when possible
// Before:
GeometryReader { geo in
Circle().frame(width: geo.size.width * 0.8)
}
// After:
Circle()
.frame(maxWidth: .infinity)
.aspectRatio(1, contentMode: .fit)
.padding(.horizontal, 20)
```
## Step 6: Use @Observable
```swift
// Before: ObservableObject invalidates everything
class OldViewModel: ObservableObject {
@Published var name = ""
@Published var count = 0
}
// After: granular updates
@Observable
class ViewModel {
var name = ""
var count = 0
}
```
## Step 7: Verify Improvements from CLI
```bash
# 1. Rebuild release
xcodebuild -scheme AppName -configuration Release build 2>&1 | xcsift
# 2. Profile again with same settings
xcrun xctrace record \
--template 'Time Profiler' \
--time-limit 30s \
--output optimized-cpu.trace \
--launch -- ./build/Build/Products/Release/AppName.app/Contents/MacOS/AppName
# 3. Check for memory leaks
leaks AppName
# 4. Run tests to ensure no regressions
xcodebuild test -scheme AppName 2>&1 | xcsift
# 5. Launch for user verification
open ./build/Build/Products/Release/AppName.app
```
Report to user:
- "CPU usage reduced from X% to Y%"
- "View body invocations reduced by Z%"
- "No memory leaks detected"
- "Tests: all pass, no regressions"
- "App launched - please verify scrolling feels smooth"
</process>
<anti_patterns>
## Avoid These Mistakes
**Optimizing without profiling:**
- Always measure with Instruments first
- Let data guide decisions
**Using .equatable() as first resort:**
- Masks the issue instead of fixing it
- Can cause stale UI
**Testing only in simulator:**
- Simulator runs on Mac CPU
- Always profile on real devices
**Ignoring view identity:**
- Use explicit id() when needed
- Ensure stable IDs in ForEach
**Premature view extraction:**
- Extract when it isolates state observation
- Not "for performance" by default
</anti_patterns>
<success_criteria>
This workflow is complete when:
- [ ] Time Profiler shows reduced CPU usage
- [ ] 50%+ reduction in unnecessary view body invocations
- [ ] Scroll performance at 60fps
- [ ] App feels responsive on oldest supported device
- [ ] Memory usage stable, no leaks
- [ ] _printChanges() confirms targeted updates
</success_criteria>

View file

@ -1,203 +0,0 @@
<required_reading>
**Read these reference files NOW before starting:**
1. `../macos-apps/references/cli-workflow.md` - Build, test, sign, notarize from CLI
2. `../macos-apps/references/security-code-signing.md` - Code signing and notarization
3. `references/platform-integration.md` - iOS/macOS specifics, platform requirements
</required_reading>
<process>
## Step 1: Run Tests
```bash
# iOS
xcodebuild test -scheme AppName -destination 'platform=iOS Simulator,name=iPhone 15 Pro' 2>&1 | xcsift
# macOS
xcodebuild test -scheme AppName 2>&1 | xcsift
```
All tests must pass before shipping.
## Step 2: Profile Performance from CLI
```bash
# Build release for accurate profiling
xcodebuild -scheme AppName -configuration Release build 2>&1 | xcsift
# Time Profiler
xcrun xctrace record \
--template 'Time Profiler' \
--time-limit 30s \
--output ship-profile.trace \
--launch -- ./build/Build/Products/Release/AppName.app/Contents/MacOS/AppName
# Check for leaks
leaks AppName
# Memory allocations
xcrun xctrace record \
--template 'Allocations' \
--time-limit 30s \
--output ship-allocations.trace \
--attach $(pgrep AppName)
```
Report: "No memory leaks. CPU usage acceptable. Ready to ship."
## Step 3: Update Version Numbers
```bash
# Marketing version
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString 1.0.0" "YourApp/Info.plist"
# Build number (must increment each submission)
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion 1" "YourApp/Info.plist"
```
## Step 4: Create Privacy Manifest
Create `PrivacyInfo.xcprivacy` with all accessed APIs:
- NSPrivacyAccessedAPICategoryUserDefaults
- NSPrivacyAccessedAPICategoryFileTimestamp
- etc.
Required for iOS 17+ and macOS 14+.
## Step 5: Verify App Icons
All required sizes in Assets.xcassets:
- 1024x1024 App Store icon (required)
- All device sizes filled
## Step 6: Configure Code Signing
Set in project.yml (XcodeGen) or verify existing settings:
```yaml
settings:
base:
CODE_SIGN_STYLE: Automatic
DEVELOPMENT_TEAM: YOURTEAMID
CODE_SIGN_IDENTITY: "Apple Distribution"
```
Or set via xcodebuild:
```bash
xcodebuild -scheme AppName \
CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM=YOURTEAMID \
archive
```
## Step 7: Create Archive
```bash
xcodebuild archive \
-scheme YourApp \
-configuration Release \
-archivePath ./build/YourApp.xcarchive \
-destination 'generic/platform=iOS'
```
## Step 8: Export for App Store
```bash
xcodebuild -exportArchive \
-archivePath ./build/YourApp.xcarchive \
-exportPath ./build/Export \
-exportOptionsPlist ExportOptions.plist
```
## Step 9: Create App in App Store Connect
1. Visit appstoreconnect.apple.com
2. My Apps → + → New App
3. Fill in name, bundle ID, SKU
## Step 10: Upload Build from CLI
```bash
# Validate before upload
xcrun altool --validate-app -f ./build/Export/AppName.ipa -t ios --apiKey YOUR_KEY --apiIssuer YOUR_ISSUER
# Upload to App Store Connect
xcrun altool --upload-app -f ./build/Export/AppName.ipa -t ios --apiKey YOUR_KEY --apiIssuer YOUR_ISSUER
# For macOS apps, notarize first (see ../macos-apps/references/security-code-signing.md)
xcrun notarytool submit AppName.zip --apple-id your@email.com --team-id TEAMID --password @keychain:AC_PASSWORD --wait
xcrun stapler staple AppName.app
```
Alternative: Use Transporter app if API keys aren't set up.
## Step 11: Complete Metadata
In App Store Connect:
- Description (4000 char max)
- Keywords (100 char max)
- Screenshots (at least 1 per device type)
- Privacy Policy URL
- Support URL
## Step 12: Configure TestFlight (Optional)
1. Wait for build processing
2. Add internal testers (up to 100)
3. For external testing, submit for Beta App Review
## Step 13: Submit for Review
1. Select processed build
2. Complete App Review Information
3. Provide demo account if login required
4. Submit for Review
Review typically completes in 24-48 hours.
## Step 14: Handle Outcome
**If approved:** Release manually or automatically
**If rejected:**
- Read rejection reason
- Fix issues
- Increment build number
- Re-upload and resubmit
</process>
<anti_patterns>
## Avoid These Mistakes
**Testing only in simulator:**
- Always test on physical devices before submission
**Incomplete privacy manifest:**
- Document all accessed APIs
- Use Xcode's Privacy Report
**Same build number:**
- Must increment CFBundleVersion for each upload
**Debug code in release:**
- Remove NSLog, test accounts, debug views
- Use #if DEBUG
**Screenshots of splash screen:**
- Must show app in actual use
- Guideline 2.3.3 rejection risk
**Not testing exported build:**
- Export process applies different signing
- Apps can crash after export despite working in Xcode
</anti_patterns>
<success_criteria>
This workflow is complete when:
- [ ] All tests pass
- [ ] Version and build numbers updated
- [ ] Privacy manifest complete
- [ ] Archive created successfully
- [ ] Build uploaded to App Store Connect
- [ ] Metadata and screenshots complete
- [ ] App submitted for review
- [ ] App approved and live on App Store
</success_criteria>

View file

@ -1,235 +0,0 @@
<required_reading>
**Read these reference files NOW before starting:**
1. `../macos-apps/references/cli-workflow.md` - Test commands from CLI
2. `../macos-apps/references/testing-tdd.md` - TDD patterns, avoiding @main hangs
3. `references/testing-debugging.md` - SwiftUI-specific testing and debugging
</required_reading>
<process>
## Step 1: Identify Testing Scope
**Test business logic in view models, not views:**
```swift
// Testable view model
@Observable
final class LoginViewModel {
var email = ""
var password = ""
var isLoading = false
var isValidInput: Bool {
!email.isEmpty && password.count >= 8
}
}
// View is just presentation
struct LoginView: View {
let viewModel: LoginViewModel
var body: some View {
Form {
TextField("Email", text: $viewModel.email)
Button("Login") { }
.disabled(!viewModel.isValidInput)
}
}
}
```
## Step 2: Write Unit Tests
**Using Swift Testing (@Test):**
```swift
import Testing
@testable import MyApp
@Test("Email validation")
func emailValidation() {
let viewModel = LoginViewModel()
viewModel.email = ""
viewModel.password = "password123"
#expect(!viewModel.isValidInput)
viewModel.email = "user@example.com"
#expect(viewModel.isValidInput)
}
@Test("Async loading")
func asyncLoading() async {
let mockService = MockService()
let viewModel = TaskViewModel(service: mockService)
await viewModel.load()
#expect(!viewModel.tasks.isEmpty)
}
```
## Step 3: Add Accessibility Identifiers
```swift
TextField("Email", text: $email)
.accessibilityIdentifier("emailField")
SecureField("Password", text: $password)
.accessibilityIdentifier("passwordField")
Button("Login") { }
.accessibilityIdentifier("loginButton")
```
## Step 4: Write UI Tests
```swift
import XCTest
final class LoginUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testLoginFlow() {
let emailField = app.textFields["emailField"]
let passwordField = app.secureTextFields["passwordField"]
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(emailField.waitForExistence(timeout: 5))
emailField.tap()
emailField.typeText("user@example.com")
passwordField.tap()
passwordField.typeText("password123")
XCTAssertTrue(loginButton.isEnabled)
loginButton.tap()
let welcomeText = app.staticTexts["welcomeMessage"]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5))
}
}
```
## Step 5: Create Previews for Visual Testing
```swift
#Preview("Empty") { LoginView(viewModel: LoginViewModel()) }
#Preview("Filled") {
let viewModel = LoginViewModel()
viewModel.email = "user@example.com"
viewModel.password = "password123"
return LoginView(viewModel: viewModel)
}
#Preview("Error") {
let viewModel = LoginViewModel()
viewModel.errorMessage = "Invalid credentials"
return LoginView(viewModel: viewModel)
}
#Preview("Dark Mode") {
LoginView(viewModel: LoginViewModel())
.preferredColorScheme(.dark)
}
```
## Step 6: Run Tests from CLI
```bash
# Run all tests with parsed output
xcodebuild test -scheme AppName -destination 'platform=iOS Simulator,name=iPhone 15 Pro' 2>&1 | xcsift
# Run only unit tests
xcodebuild test -scheme AppName -only-testing:AppNameTests 2>&1 | xcsift
# Run only UI tests
xcodebuild test -scheme AppName -only-testing:AppNameUITests 2>&1 | xcsift
# Run specific test class
xcodebuild test -scheme AppName -only-testing:AppNameTests/LoginViewModelTests 2>&1 | xcsift
# Run specific test method
xcodebuild test -scheme AppName -only-testing:AppNameTests/LoginViewModelTests/testEmailValidation 2>&1 | xcsift
# Generate test coverage
xcodebuild test -scheme AppName -enableCodeCoverage YES -resultBundlePath TestResults.xcresult 2>&1 | xcsift
xcrun xccov view --report TestResults.xcresult
```
**If tests hang:** The test target likely depends on the app target with `@main`. Extract testable code to a Core framework target. See `../macos-apps/references/testing-tdd.md`.
Report to user:
- "Tests: X pass, Y fail"
- "Coverage: Z% of lines"
- If failures: "Failed tests: [list]. Investigating..."
</process>
<anti_patterns>
## Avoid These Mistakes
**Testing view bodies:**
```swift
// Wrong: can't test views directly
func testView() {
let view = LoginView()
// Can't inspect SwiftUI view
}
// Right: test view model
@Test func emailInput() {
let viewModel = LoginViewModel()
viewModel.email = "test@example.com"
#expect(viewModel.email == "test@example.com")
}
```
**Missing accessibility identifiers:**
```swift
// Wrong: using text
let button = app.buttons["Login"]
// Right: stable identifier
let button = app.buttons["loginButton"]
```
**No dependency injection:**
```swift
// Wrong: can't mock
@Observable
class ViewModel {
private let service = RealService()
}
// Right: injectable
@Observable
class ViewModel {
private let service: ServiceProtocol
init(service: ServiceProtocol) {
self.service = service
}
}
```
**No edge case testing:**
```swift
// Test empty, invalid, error states
@Test func emptyEmail() { }
@Test func shortPassword() { }
@Test func networkError() { }
```
</anti_patterns>
<success_criteria>
This workflow is complete when:
- [ ] Unit tests verify view model business logic
- [ ] UI tests verify user flows using accessibility identifiers
- [ ] All tests pass: `xcodebuild test -scheme YourApp`
- [ ] Edge cases and error states have coverage
- [ ] Dependencies use protocols for testability
- [ ] Previews exist for major UI states
</success_criteria>

View file

@ -163,8 +163,6 @@ When performing an audit, structure findings as:
## References
For detailed guidelines on specific areas:
- [Performance Optimization](../performance/SKILL.md)
- [Core Web Vitals](../core-web-vitals/SKILL.md)
- [Accessibility](../accessibility/SKILL.md)
- [SEO](../seo/SKILL.md)
- [Best Practices](../best-practices/SKILL.md)