feat(skills): add bundled skills — frontend-design, swiftui, debug-like-expert
This commit is contained in:
parent
7cebe5b519
commit
763644bd95
25 changed files with 13051 additions and 0 deletions
231
src/resources/skills/debug-like-expert/SKILL.md
Normal file
231
src/resources/skills/debug-like-expert/SKILL.md
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
---
|
||||
name: debug-like-expert
|
||||
description: Deep analysis debugging mode for complex issues. Activates methodical investigation protocol with evidence gathering, hypothesis testing, and rigorous verification. Use when standard troubleshooting fails or when issues require systematic root cause analysis.
|
||||
---
|
||||
|
||||
<objective>
|
||||
Deep analysis debugging mode for complex issues. This skill activates methodical investigation protocols with evidence gathering, hypothesis testing, and rigorous verification when standard troubleshooting has failed.
|
||||
|
||||
The skill emphasizes treating code you wrote with MORE skepticism than unfamiliar code, as cognitive biases about "how it should work" can blind you to actual implementation errors. Use scientific method to systematically identify root causes rather than applying quick fixes.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
This skill activates when standard troubleshooting has failed. The issue requires methodical investigation, not quick fixes. You are entering the mindset of a senior engineer who debugs with scientific rigor.
|
||||
|
||||
**Important**: If you wrote or modified any of the code being debugged, you have cognitive biases about how it works. Your mental model of "how it should work" may be wrong. Treat code you wrote with MORE skepticism than unfamiliar code - you're blind to your own assumptions.
|
||||
</context>
|
||||
|
||||
<core_principle>
|
||||
**VERIFY, DON'T ASSUME.** Every hypothesis must be tested. Every "fix" must be validated. No solutions without evidence.
|
||||
|
||||
**ESPECIALLY**: Code you designed or implemented is guilty until proven innocent. Your intent doesn't matter - only the code's actual behavior matters. Question your own design decisions as rigorously as you'd question anyone else's.
|
||||
</core_principle>
|
||||
|
||||
<analysis_only_rule>
|
||||
**THIS SKILL IS READ-ONLY. DO NOT MODIFY CODE.**
|
||||
|
||||
The entire purpose is deep analysis and diagnosis. Making changes during investigation:
|
||||
- Pollutes the evidence
|
||||
- Introduces new variables
|
||||
- Makes root cause harder to isolate
|
||||
|
||||
You are a diagnostician, not a surgeon. Present findings, then let the user decide.
|
||||
</analysis_only_rule>
|
||||
|
||||
<quick_start>
|
||||
|
||||
<evidence_gathering>
|
||||
|
||||
Before proposing any solution:
|
||||
|
||||
**A. Document Current State**
|
||||
- What is the EXACT error message or unexpected behavior?
|
||||
- What are the EXACT steps to reproduce?
|
||||
- What is the ACTUAL output vs EXPECTED output?
|
||||
- When did this start working incorrectly (if known)?
|
||||
|
||||
**B. Map the System**
|
||||
- Trace the execution path from entry point to failure point
|
||||
- Identify all components involved
|
||||
- Read relevant source files completely, not just scanning
|
||||
- Note dependencies, imports, configurations affecting this area
|
||||
|
||||
**C. Gather External Knowledge (when needed)**
|
||||
- Use MCP servers for API documentation, library details, or domain knowledge
|
||||
- Use web search for error messages, framework-specific behaviors, or recent changes
|
||||
- Check official docs for intended behavior vs what you observe
|
||||
- Look for known issues, breaking changes, or version-specific quirks
|
||||
|
||||
See [references/when-to-research.md](references/when-to-research.md) for detailed guidance on research strategy.
|
||||
|
||||
</evidence_gathering>
|
||||
|
||||
<root_cause_analysis>
|
||||
|
||||
**A. Form Hypotheses**
|
||||
|
||||
Based on evidence, list possible causes:
|
||||
1. [Hypothesis 1] - because [specific evidence]
|
||||
2. [Hypothesis 2] - because [specific evidence]
|
||||
3. [Hypothesis 3] - because [specific evidence]
|
||||
|
||||
**B. Test Each Hypothesis**
|
||||
|
||||
For each hypothesis:
|
||||
- What would prove this true?
|
||||
- What would prove this false?
|
||||
- Design a minimal test
|
||||
- Execute and document results
|
||||
|
||||
See [references/hypothesis-testing.md](references/hypothesis-testing.md) for scientific method application.
|
||||
|
||||
**C. Eliminate or Confirm**
|
||||
|
||||
Don't move forward until you can answer:
|
||||
- Which hypothesis is supported by evidence?
|
||||
- What evidence contradicts other hypotheses?
|
||||
- What additional information is needed?
|
||||
|
||||
</root_cause_analysis>
|
||||
|
||||
<solution_proposal>
|
||||
|
||||
**Only after confirming root cause:**
|
||||
|
||||
**A. Design Recommended Fix**
|
||||
- What is the MINIMAL change that would address the root cause?
|
||||
- What are potential side effects?
|
||||
- What could this break?
|
||||
- What tests should run after implementation?
|
||||
|
||||
**B. Document, Don't Implement**
|
||||
- Describe the fix with enough detail for implementation
|
||||
- Include specific file paths, line numbers, and code snippets
|
||||
- Explain WHY this addresses the root cause
|
||||
- Note any prerequisites or dependencies
|
||||
|
||||
**DO NOT make any code changes. Present your recommendations only.**
|
||||
|
||||
See [references/verification-patterns.md](references/verification-patterns.md) for verification approaches to use after implementation.
|
||||
|
||||
</solution_proposal>
|
||||
|
||||
</quick_start>
|
||||
|
||||
<critical_rules>
|
||||
|
||||
1. **NO DRIVE-BY FIXES**: If you can't explain WHY a change works, don't make it
|
||||
2. **VERIFY EVERYTHING**: Test your assumptions. Read the actual code. Check the actual behavior
|
||||
3. **USE ALL TOOLS**:
|
||||
- MCP servers for external knowledge
|
||||
- Web search for error messages, docs, known issues
|
||||
- Extended thinking ("think deeply") for complex reasoning
|
||||
- File reading for complete context
|
||||
4. **THINK OUT LOUD**: Document your reasoning at each step
|
||||
5. **ONE VARIABLE**: Change one thing at a time, verify, then proceed
|
||||
6. **COMPLETE READS**: Don't skim code. Read entire relevant files
|
||||
7. **CHASE DEPENDENCIES**: If the issue involves libraries, configs, or external systems, investigate those too
|
||||
8. **QUESTION PREVIOUS WORK**: Maybe the earlier "fix" was wrong. Re-examine with fresh eyes
|
||||
|
||||
</critical_rules>
|
||||
|
||||
<success_criteria>
|
||||
|
||||
Before completing:
|
||||
- [ ] Do you understand WHY the issue occurred?
|
||||
- [ ] Have you identified a root cause with evidence?
|
||||
- [ ] Have you documented your reasoning?
|
||||
- [ ] Can you explain the issue to someone else?
|
||||
- [ ] Is your recommended fix specific and actionable?
|
||||
|
||||
If you can't answer "yes" to all of these, keep investigating.
|
||||
|
||||
**CRITICAL**: Present findings via decision gate. Do NOT implement changes.
|
||||
|
||||
</success_criteria>
|
||||
|
||||
<output_format>
|
||||
|
||||
```markdown
|
||||
## Issue: [Problem Description]
|
||||
|
||||
### Evidence
|
||||
[What you observed - exact errors, behaviors, outputs]
|
||||
|
||||
### Investigation
|
||||
[What you checked, what you found, what you ruled out]
|
||||
|
||||
### Root Cause
|
||||
[The actual underlying problem with evidence]
|
||||
|
||||
### Recommended Fix
|
||||
[What SHOULD be changed and WHY - specific files, lines, code]
|
||||
|
||||
### Verification Plan
|
||||
[How to confirm the fix works after implementation]
|
||||
|
||||
### Risk Assessment
|
||||
[Potential side effects, what could break, confidence level]
|
||||
```
|
||||
|
||||
</output_format>
|
||||
|
||||
<advanced_topics>
|
||||
|
||||
For deeper topics, see reference files:
|
||||
|
||||
**Debugging mindset**: [references/debugging-mindset.md](references/debugging-mindset.md)
|
||||
- First principles thinking applied to debugging
|
||||
- Cognitive biases that lead to bad fixes
|
||||
- The discipline of systematic investigation
|
||||
- When to stop and restart with fresh assumptions
|
||||
|
||||
**Investigation techniques**: [references/investigation-techniques.md](references/investigation-techniques.md)
|
||||
- Binary search / divide and conquer
|
||||
- Rubber duck debugging
|
||||
- Minimal reproduction
|
||||
- Working backwards from desired state
|
||||
- Adding observability before changing code
|
||||
|
||||
**Hypothesis testing**: [references/hypothesis-testing.md](references/hypothesis-testing.md)
|
||||
- Forming falsifiable hypotheses
|
||||
- Designing experiments that prove/disprove
|
||||
- What makes evidence strong vs weak
|
||||
- Recovering from wrong hypotheses gracefully
|
||||
|
||||
**Verification patterns**: [references/verification-patterns.md](references/verification-patterns.md)
|
||||
- Definition of "verified" (not just "it ran")
|
||||
- Testing reproduction steps
|
||||
- Regression testing adjacent functionality
|
||||
- When to write tests before fixing
|
||||
|
||||
**Research strategy**: [references/when-to-research.md](references/when-to-research.md)
|
||||
- Signals that you need external knowledge
|
||||
- What to search for vs what to reason about
|
||||
- Balancing research time vs experimentation
|
||||
|
||||
</advanced_topics>
|
||||
|
||||
<decision_gate>
|
||||
|
||||
**After presenting findings, ALWAYS offer these options:**
|
||||
|
||||
```
|
||||
─────────────────────────────────────────
|
||||
ANALYSIS COMPLETE
|
||||
|
||||
What would you like to do?
|
||||
|
||||
1. **Fix it now** - I'll implement the recommended changes
|
||||
2. **Create findings document** - Save analysis to a markdown file
|
||||
3. **Explore further** - Investigate additional hypotheses
|
||||
4. **Get second opinion** - Review with different assumptions
|
||||
5. **Other** - Tell me what you need
|
||||
─────────────────────────────────────────
|
||||
```
|
||||
|
||||
**Wait for user response before taking any action.**
|
||||
|
||||
This gate is MANDATORY. Never skip it. Never auto-implement.
|
||||
|
||||
</decision_gate>
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
<philosophy>
|
||||
Debugging is applied epistemology. You're investigating a system to discover truth about its behavior. The difference between junior and senior debugging is not knowledge of frameworks - it's the discipline of systematic investigation.
|
||||
</philosophy>
|
||||
|
||||
<meta_debugging>
|
||||
**Special challenge**: When you're debugging code you wrote or modified, you're fighting your own mental model.
|
||||
|
||||
**Why this is harder**:
|
||||
- You made the design decisions - they feel obviously correct
|
||||
- You remember your intent, not what you actually implemented
|
||||
- You see what you meant to write, not what's there
|
||||
- Familiarity breeds blindness to bugs
|
||||
|
||||
**The trap**:
|
||||
- "I know this works because I implemented it correctly"
|
||||
- "The bug must be elsewhere - I designed this part"
|
||||
- "I tested this approach"
|
||||
- These thoughts are red flags. Code you wrote is guilty until proven innocent.
|
||||
|
||||
**The discipline**:
|
||||
|
||||
**1. Treat your own code as foreign**
|
||||
- Read it as if someone else wrote it
|
||||
- Don't assume it does what you intended
|
||||
- Verify what it actually does, not what you think it does
|
||||
- Fresh eyes see bugs; familiar eyes see intent
|
||||
|
||||
**2. Question your own design decisions**
|
||||
- "I chose approach X because..." - Was that reasoning sound?
|
||||
- "I assumed Y would..." - Have you verified Y actually does that?
|
||||
- Your implementation decisions are hypotheses, not facts
|
||||
|
||||
**3. Admit your mental model might be wrong**
|
||||
- You built a mental model of how this works
|
||||
- That model might be incomplete or incorrect
|
||||
- The code's behavior is truth; your model is just a guess
|
||||
- Be willing to discover you misunderstood the problem
|
||||
|
||||
**4. Prioritize code you touched**
|
||||
- If you modified 100 lines and something breaks
|
||||
- Those 100 lines are the prime suspects
|
||||
- Don't assume the bug is in the framework or existing code
|
||||
- Start investigating where you made changes
|
||||
|
||||
<example>
|
||||
❌ "I implemented the auth flow correctly, the bug must be in the existing user service"
|
||||
|
||||
✅ "I implemented the auth flow. Let me verify each part:
|
||||
- Does login actually set the token? [test it]
|
||||
- Does the middleware actually validate it? [test it]
|
||||
- Does logout actually clear it? [test it]
|
||||
- One of these is probably wrong"
|
||||
|
||||
The second approach found that logout wasn't clearing the token from localStorage, only from memory.
|
||||
</example>
|
||||
|
||||
**The hardest admission**: "I implemented this wrong."
|
||||
|
||||
Not "the requirements were unclear" or "the library is confusing" - YOU made an error. Whether it was 5 minutes ago or 5 days ago doesn't matter. Your code, your responsibility, your bug to find.
|
||||
|
||||
This intellectual honesty is the difference between debugging for hours and finding bugs quickly.
|
||||
</meta_debugging>
|
||||
|
||||
<foundation>
|
||||
When debugging, return to foundational truths:
|
||||
|
||||
**What do you know for certain?**
|
||||
- What have you directly observed (not assumed)?
|
||||
- What can you prove with a test right now?
|
||||
- What is speculation vs evidence?
|
||||
|
||||
**What are you assuming?**
|
||||
- "This library should work this way" - Have you verified?
|
||||
- "The docs say X" - Have you tested that X actually happens?
|
||||
- "This worked before" - Can you prove when it worked and what changed?
|
||||
|
||||
Strip away everything you think you know. Build understanding from observable facts.
|
||||
</foundation>
|
||||
|
||||
<example>
|
||||
❌ "React state updates should be synchronous here"
|
||||
✅ "Let me add a console.log to observe when state actually updates"
|
||||
|
||||
❌ "The API must be returning bad data"
|
||||
✅ "Let me log the exact response payload to see what's actually being returned"
|
||||
|
||||
❌ "This database query should be fast"
|
||||
✅ "Let me run EXPLAIN to see the actual execution plan"
|
||||
</example>
|
||||
|
||||
<cognitive_biases>
|
||||
|
||||
<bias name="confirmation_bias">
|
||||
**The problem**: You form a hypothesis and only look for evidence that confirms it.
|
||||
|
||||
**The trap**: "I think it's a race condition" → You only look for async code, missing the actual typo in a variable name.
|
||||
|
||||
**The antidote**: Actively seek evidence that disproves your hypothesis. Ask "What would prove me wrong?"
|
||||
</bias>
|
||||
|
||||
<bias name="anchoring">
|
||||
**The problem**: The first explanation you encounter becomes your anchor, and you adjust from there instead of considering alternatives.
|
||||
|
||||
**The trap**: Error message mentions "timeout" → You assume it's a network issue, when it's actually a deadlock.
|
||||
|
||||
**The antidote**: Generate multiple independent hypotheses before investigating any single one. Force yourself to list 3+ possible causes.
|
||||
</bias>
|
||||
|
||||
<bias name="availability_heuristic">
|
||||
**The problem**: You remember recent bugs and assume similar symptoms mean the same cause.
|
||||
|
||||
**The trap**: "We had a caching issue last week, this must be caching too."
|
||||
|
||||
**The antidote**: Treat each bug as novel until evidence suggests otherwise. Recent memory is not evidence.
|
||||
</bias>
|
||||
|
||||
<bias name="sunk_cost_fallacy">
|
||||
**The problem**: You've spent 2 hours debugging down one path, so you keep going even when evidence suggests it's wrong.
|
||||
|
||||
**The trap**: "I've almost figured out this state management issue" - when the actual bug is in the API layer.
|
||||
|
||||
**The antidote**: Set checkpoints. Every 30 minutes, ask: "If I started fresh right now, is this still the path I'd take?"
|
||||
</bias>
|
||||
|
||||
</cognitive_biases>
|
||||
|
||||
<systematic_investigation>
|
||||
|
||||
<discipline name="change_one_variable">
|
||||
**Why it matters**: If you change multiple things at once, you don't know which one fixed (or broke) it.
|
||||
|
||||
**In practice**:
|
||||
1. Make one change
|
||||
2. Test
|
||||
3. Observe result
|
||||
4. Document
|
||||
5. Repeat
|
||||
|
||||
**The temptation**: "Let me also update this dependency and refactor this function and change this config..."
|
||||
|
||||
**The reality**: Now you have no idea what actually mattered.
|
||||
</discipline>
|
||||
|
||||
<discipline name="complete_reading">
|
||||
**Why it matters**: Skimming code causes you to miss crucial details. You see what you expect to see, not what's there.
|
||||
|
||||
**In practice**:
|
||||
- Read entire functions, not just the "relevant" lines
|
||||
- Read imports and dependencies
|
||||
- Read configuration files completely
|
||||
- Read test files to understand intended behavior
|
||||
|
||||
**The shortcut**: "This function is long, I'll just read the part where the error happens"
|
||||
|
||||
**The miss**: The bug is actually in how the function is called 50 lines up.
|
||||
</discipline>
|
||||
|
||||
<discipline name="embrace_not_knowing">
|
||||
**Why it matters**: Premature certainty stops investigation. "I don't know" is a position of strength.
|
||||
|
||||
**In practice**:
|
||||
- "I don't know why this fails" - Good. Now you can investigate.
|
||||
- "It must be X" - Dangerous. You've stopped thinking.
|
||||
|
||||
**The pressure**: Users want answers. Managers want ETAs. Your ego wants to look smart.
|
||||
|
||||
**The truth**: "I need to investigate further" is more professional than a wrong fix.
|
||||
</discipline>
|
||||
|
||||
</systematic_investigation>
|
||||
|
||||
<when_to_restart>
|
||||
|
||||
<restart_signals>
|
||||
You should consider starting over when:
|
||||
|
||||
1. **You've been investigating for 2+ hours with no progress**
|
||||
- You're likely tunnel-visioned
|
||||
- Take a break, then restart from evidence gathering
|
||||
|
||||
2. **You've made 3+ "fixes" that didn't work**
|
||||
- Your mental model is wrong
|
||||
- Go back to first principles
|
||||
|
||||
3. **You can't explain the current behavior**
|
||||
- Don't add more changes on top of confusion
|
||||
- First understand what's happening, then fix it
|
||||
|
||||
4. **You're debugging the debugger**
|
||||
- "Is my logging broken? Is the debugger lying?"
|
||||
- Step back. Something fundamental is wrong.
|
||||
|
||||
5. **The fix works but you don't know why**
|
||||
- This isn't fixed. This is luck.
|
||||
- Investigate until you understand, or revert the change
|
||||
</restart_signals>
|
||||
|
||||
<restart_protocol>
|
||||
When restarting:
|
||||
|
||||
1. **Close all files and terminals**
|
||||
2. **Write down what you know for certain** (not what you think)
|
||||
3. **Write down what you've ruled out**
|
||||
4. **List new hypotheses** (different from before)
|
||||
5. **Begin again from Phase 1: Evidence Gathering**
|
||||
|
||||
This isn't failure. This is professionalism.
|
||||
</restart_protocol>
|
||||
|
||||
</when_to_restart>
|
||||
|
||||
<humility>
|
||||
The best debuggers have deep humility about their mental models:
|
||||
|
||||
**They know**:
|
||||
- Their understanding of the system is incomplete
|
||||
- Documentation can be wrong or outdated
|
||||
- Their memory of "how this works" may be faulty
|
||||
- The system's behavior is the only truth
|
||||
|
||||
**They don't**:
|
||||
- Trust their first instinct
|
||||
- Assume anything works as designed
|
||||
- Skip verification steps
|
||||
- Declare victory without proof
|
||||
|
||||
**They ask**:
|
||||
- "What am I missing?"
|
||||
- "What am I wrong about?"
|
||||
- "What haven't I tested?"
|
||||
- "What does the evidence actually say?"
|
||||
</humility>
|
||||
|
||||
<craft>
|
||||
Debugging is a craft that improves with practice:
|
||||
|
||||
**Novice debuggers**:
|
||||
- Try random things hoping something works
|
||||
- Skip reading code carefully
|
||||
- Don't test their hypotheses
|
||||
- Declare success too early
|
||||
|
||||
**Expert debuggers**:
|
||||
- Form hypotheses explicitly
|
||||
- Test hypotheses systematically
|
||||
- Read code like literature
|
||||
- Verify fixes rigorously
|
||||
- Learn from each investigation
|
||||
|
||||
**The difference**: Not intelligence. Not knowledge. Discipline.
|
||||
|
||||
Practice the discipline of systematic investigation, and debugging becomes a strength.
|
||||
</craft>
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
|
||||
<overview>
|
||||
Debugging is applied scientific method. You observe a phenomenon (the bug), form hypotheses about its cause, design experiments to test those hypotheses, and revise based on evidence. This isn't metaphorical - it's literal experimental science.
|
||||
</overview>
|
||||
|
||||
|
||||
<principle name="falsifiability">
|
||||
A good hypothesis can be proven wrong. If you can't design an experiment that could disprove it, it's not a useful hypothesis.
|
||||
|
||||
**Bad hypotheses** (unfalsifiable):
|
||||
- "Something is wrong with the state"
|
||||
- "The timing is off"
|
||||
- "There's a race condition somewhere"
|
||||
- "The library is buggy"
|
||||
|
||||
**Good hypotheses** (falsifiable):
|
||||
- "The user state is being reset because the component remounts when the route changes"
|
||||
- "The API call completes after the component unmounts, causing the state update on unmounted component warning"
|
||||
- "Two async operations are modifying the same array without locking, causing data loss"
|
||||
- "The library's caching mechanism is returning stale data because our cache key doesn't include the timestamp"
|
||||
|
||||
**The difference**: Specificity. Good hypotheses make specific, testable claims.
|
||||
</principle>
|
||||
|
||||
<how_to_form>
|
||||
**Process for forming hypotheses**:
|
||||
|
||||
1. **Observe the behavior precisely**
|
||||
- Not "it's broken"
|
||||
- But "the counter shows 3 when clicking once, should show 1"
|
||||
|
||||
2. **Ask "What could cause this?"**
|
||||
- List every possible cause you can think of
|
||||
- Don't judge them yet, just brainstorm
|
||||
|
||||
3. **Make each hypothesis specific**
|
||||
- Not "state is wrong"
|
||||
- But "state is being updated twice because handleClick is called twice"
|
||||
|
||||
4. **Identify what evidence would support/refute each**
|
||||
- If hypothesis X is true, I should see Y
|
||||
- If hypothesis X is false, I should see Z
|
||||
|
||||
<example>
|
||||
**Observation**: Button click sometimes saves data, sometimes doesn't.
|
||||
|
||||
**Vague hypothesis**: "The save isn't working reliably"
|
||||
❌ Unfalsifiable, not specific
|
||||
|
||||
**Specific hypotheses**:
|
||||
1. "The save API call is timing out when network is slow"
|
||||
- Testable: Check network tab for timeout errors
|
||||
- Falsifiable: If all requests complete successfully, this is wrong
|
||||
|
||||
2. "The save button is being double-clicked, and the second request overwrites with stale data"
|
||||
- Testable: Add logging to count clicks
|
||||
- Falsifiable: If only one click is registered, this is wrong
|
||||
|
||||
3. "The save is successful but the UI doesn't update because the response is being ignored"
|
||||
- Testable: Check if API returns success
|
||||
- Falsifiable: If UI updates on successful response, this is wrong
|
||||
</example>
|
||||
</how_to_form>
|
||||
|
||||
|
||||
<experimental_design>
|
||||
An experiment is a test that produces evidence supporting or refuting a hypothesis.
|
||||
|
||||
**Good experiments**:
|
||||
- Test one hypothesis at a time
|
||||
- Have clear success/failure criteria
|
||||
- Produce unambiguous results
|
||||
- Are repeatable
|
||||
|
||||
**Bad experiments**:
|
||||
- Test multiple things at once
|
||||
- Have unclear outcomes ("maybe it works better?")
|
||||
- Rely on subjective judgment
|
||||
- Can't be reproduced
|
||||
|
||||
<framework>
|
||||
For each hypothesis, design an experiment:
|
||||
|
||||
**1. Prediction**: If hypothesis H is true, then I will observe X
|
||||
**2. Test setup**: What do I need to do to test this?
|
||||
**3. Measurement**: What exactly am I measuring?
|
||||
**4. Success criteria**: What result confirms H? What result refutes H?
|
||||
**5. Run the experiment**: Execute the test
|
||||
**6. Observe the result**: Record what actually happened
|
||||
**7. Conclude**: Does this support or refute H?
|
||||
|
||||
</framework>
|
||||
|
||||
<example>
|
||||
**Hypothesis**: "The component is re-rendering excessively because the parent is passing a new object reference on every render"
|
||||
|
||||
**1. Prediction**: If true, the component will re-render even when the object's values haven't changed
|
||||
|
||||
**2. Test setup**:
|
||||
- Add console.log in component body to count renders
|
||||
- Add console.log in parent to track when object is created
|
||||
- Add useEffect with the object as dependency to log when it changes
|
||||
|
||||
**3. Measurement**: Count of renders and object creations
|
||||
|
||||
**4. Success criteria**:
|
||||
- Confirms H: Component re-renders match parent renders, object reference changes each time
|
||||
- Refutes H: Component only re-renders when object values actually change
|
||||
|
||||
**5. Run**: Execute the code with logging
|
||||
|
||||
**6. Observe**:
|
||||
```
|
||||
[Parent] Created user object
|
||||
[Child] Rendering (1)
|
||||
[Parent] Created user object
|
||||
[Child] Rendering (2)
|
||||
[Parent] Created user object
|
||||
[Child] Rendering (3)
|
||||
```
|
||||
|
||||
**7. Conclude**: CONFIRMED. New object every parent render → child re-renders
|
||||
</example>
|
||||
</experimental_design>
|
||||
|
||||
|
||||
<evidence_quality>
|
||||
Not all evidence is equal. Learn to distinguish strong from weak evidence.
|
||||
|
||||
**Strong evidence**:
|
||||
- Directly observable ("I can see in the logs that X happens")
|
||||
- Repeatable ("This fails every time I do Y")
|
||||
- Unambiguous ("The value is definitely null, not undefined")
|
||||
- Independent ("This happens even in a fresh browser with no cache")
|
||||
|
||||
**Weak evidence**:
|
||||
- Hearsay ("I think I saw this fail once")
|
||||
- Non-repeatable ("It failed that one time but I can't reproduce it")
|
||||
- Ambiguous ("Something seems off")
|
||||
- Confounded ("It works after I restarted the server and cleared the cache and updated the package")
|
||||
|
||||
<examples>
|
||||
**Strong**:
|
||||
```javascript
|
||||
console.log('User ID:', userId); // Output: User ID: undefined
|
||||
console.log('Type:', typeof userId); // Output: Type: undefined
|
||||
```
|
||||
✅ Direct observation, unambiguous
|
||||
|
||||
**Weak**:
|
||||
"I think the user ID might not be set correctly sometimes"
|
||||
❌ Vague, not verified, uncertain
|
||||
|
||||
**Strong**:
|
||||
```javascript
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const result = processData(testData);
|
||||
if (result !== expected) {
|
||||
console.log('Failed on iteration', i);
|
||||
}
|
||||
}
|
||||
// Output: Failed on iterations: 3, 7, 12, 23, 31...
|
||||
```
|
||||
✅ Repeatable, shows pattern
|
||||
|
||||
**Weak**:
|
||||
"It usually works, but sometimes fails"
|
||||
❌ Not quantified, no pattern identified
|
||||
</examples>
|
||||
</evidence_quality>
|
||||
|
||||
|
||||
<decision_point>
|
||||
Don't act too early (premature fix) or too late (analysis paralysis).
|
||||
|
||||
**Act when you can answer YES to all**:
|
||||
|
||||
1. **Do you understand the mechanism?**
|
||||
- Not just "what fails" but "why it fails"
|
||||
- Can you explain the chain of events that produces the bug?
|
||||
|
||||
2. **Can you reproduce it reliably?**
|
||||
- Either always reproduces, or you understand the conditions that trigger it
|
||||
- If you can't reproduce, you don't understand it yet
|
||||
|
||||
3. **Do you have evidence, not just theory?**
|
||||
- You've observed the behavior directly
|
||||
- You've logged the values, traced the execution
|
||||
- You're not guessing
|
||||
|
||||
4. **Have you ruled out alternatives?**
|
||||
- You've considered other hypotheses
|
||||
- Evidence contradicts the alternatives
|
||||
- This is the most likely cause, not just the first idea
|
||||
|
||||
**Don't act if**:
|
||||
- "I think it might be X" - Too uncertain
|
||||
- "This could be the issue" - Not confident enough
|
||||
- "Let me try changing Y and see" - Random changes, not hypothesis-driven
|
||||
- "I'll fix it and if it works, great" - Outcome-based, not understanding-based
|
||||
|
||||
<example>
|
||||
**Too early** (don't act):
|
||||
- Hypothesis: "Maybe the API is slow"
|
||||
- Evidence: None, just a guess
|
||||
- Action: Add caching
|
||||
- Result: Bug persists, now you have caching to debug too
|
||||
|
||||
**Right time** (act):
|
||||
- Hypothesis: "API response is missing the 'status' field when user is inactive, causing the app to crash"
|
||||
- Evidence:
|
||||
- Logged API response for active user: has 'status' field
|
||||
- Logged API response for inactive user: missing 'status' field
|
||||
- Logged app behavior: crashes on accessing undefined status
|
||||
- Action: Add defensive check for missing status field
|
||||
- Result: Bug fixed because you understood the cause
|
||||
</example>
|
||||
</decision_point>
|
||||
|
||||
|
||||
<recovery>
|
||||
You will be wrong sometimes. This is normal. The skill is recovering gracefully.
|
||||
|
||||
**When your hypothesis is disproven**:
|
||||
|
||||
1. **Acknowledge it explicitly**
|
||||
- "This hypothesis was wrong because [evidence]"
|
||||
- Don't gloss over it or rationalize
|
||||
- Intellectual honesty with yourself
|
||||
|
||||
2. **Extract the learning**
|
||||
- What did this experiment teach you?
|
||||
- What did you rule out?
|
||||
- What new information do you have?
|
||||
|
||||
3. **Revise your understanding**
|
||||
- Update your mental model
|
||||
- What does the evidence actually suggest?
|
||||
|
||||
4. **Form new hypotheses**
|
||||
- Based on what you now know
|
||||
- Avoid just moving to "second-guess" - use the evidence
|
||||
|
||||
5. **Don't get attached to hypotheses**
|
||||
- You're not your ideas
|
||||
- Being wrong quickly is better than being wrong slowly
|
||||
|
||||
<example>
|
||||
**Initial hypothesis**: "The memory leak is caused by event listeners not being cleaned up"
|
||||
|
||||
**Experiment**: Check Chrome DevTools for listener counts
|
||||
**Result**: Listener count stays stable, doesn't grow over time
|
||||
|
||||
**Recovery**:
|
||||
1. ✅ "Event listeners are NOT the cause. The count doesn't increase."
|
||||
2. ✅ "I've ruled out event listeners as the culprit"
|
||||
3. ✅ "But the memory profile shows objects accumulating. What objects? Let me check the heap snapshot..."
|
||||
4. ✅ "New hypothesis: Large arrays are being cached and never released. Let me test by checking the heap for array sizes..."
|
||||
|
||||
This is good debugging. Wrong hypothesis, quick recovery, better understanding.
|
||||
</example>
|
||||
</recovery>
|
||||
|
||||
|
||||
<multiple_hypotheses>
|
||||
Don't fall in love with your first hypothesis. Generate multiple alternatives.
|
||||
|
||||
**Strategy**: "Strong inference" - Design experiments that differentiate between competing hypotheses.
|
||||
|
||||
<example>
|
||||
**Problem**: Form submission fails intermittently
|
||||
|
||||
**Competing hypotheses**:
|
||||
1. Network timeout
|
||||
2. Validation failure
|
||||
3. Race condition with auto-save
|
||||
4. Server-side rate limiting
|
||||
|
||||
**Design experiment that differentiates**:
|
||||
|
||||
Add logging at each stage:
|
||||
```javascript
|
||||
try {
|
||||
console.log('[1] Starting validation');
|
||||
const validation = await validate(formData);
|
||||
console.log('[1] Validation passed:', validation);
|
||||
|
||||
console.log('[2] Starting submission');
|
||||
const response = await api.submit(formData);
|
||||
console.log('[2] Response received:', response.status);
|
||||
|
||||
console.log('[3] Updating UI');
|
||||
updateUI(response);
|
||||
console.log('[3] Complete');
|
||||
} catch (error) {
|
||||
console.log('[ERROR] Failed at stage:', error);
|
||||
}
|
||||
```
|
||||
|
||||
**Observe results**:
|
||||
- Fails at [2] with timeout error → Hypothesis 1
|
||||
- Fails at [1] with validation error → Hypothesis 2
|
||||
- Succeeds but [3] has wrong data → Hypothesis 3
|
||||
- Fails at [2] with 429 status → Hypothesis 4
|
||||
|
||||
**One experiment, differentiates between four hypotheses.**
|
||||
</example>
|
||||
</multiple_hypotheses>
|
||||
|
||||
|
||||
<workflow>
|
||||
```
|
||||
1. Observe unexpected behavior
|
||||
↓
|
||||
2. Form specific hypotheses (plural)
|
||||
↓
|
||||
3. For each hypothesis: What would prove/disprove?
|
||||
↓
|
||||
4. Design experiment to test
|
||||
↓
|
||||
5. Run experiment
|
||||
↓
|
||||
6. Observe results
|
||||
↓
|
||||
7. Evaluate: Confirmed, refuted, or inconclusive?
|
||||
↓
|
||||
8a. If CONFIRMED → Design fix based on understanding
|
||||
8b. If REFUTED → Return to step 2 with new hypotheses
|
||||
8c. If INCONCLUSIVE → Redesign experiment or gather more data
|
||||
```
|
||||
|
||||
**Key insight**: This is a loop, not a line. You'll cycle through multiple times. That's expected.
|
||||
</workflow>
|
||||
|
||||
|
||||
<pitfalls>
|
||||
|
||||
**Pitfall: Testing multiple hypotheses at once**
|
||||
- You change three things and it works
|
||||
- Which one fixed it? You don't know
|
||||
- Solution: Test one hypothesis at a time
|
||||
|
||||
**Pitfall: Confirmation bias in experiments**
|
||||
- You only look for evidence that confirms your hypothesis
|
||||
- You ignore evidence that contradicts it
|
||||
- Solution: Actively seek disconfirming evidence
|
||||
|
||||
**Pitfall: Acting on weak evidence**
|
||||
- "It seems like maybe this could be..."
|
||||
- Solution: Wait for strong, unambiguous evidence
|
||||
|
||||
**Pitfall: Not documenting results**
|
||||
- You forget what you tested
|
||||
- You repeat the same experiments
|
||||
- Solution: Write down each hypothesis and its result
|
||||
|
||||
**Pitfall: Giving up on the scientific method**
|
||||
- Under pressure, you start making random changes
|
||||
- "Let me just try this..."
|
||||
- Solution: Double down on rigor when pressure increases
|
||||
</pitfalls>
|
||||
|
||||
<excellence>
|
||||
**Great debuggers**:
|
||||
- Form multiple competing hypotheses
|
||||
- Design clever experiments that differentiate between them
|
||||
- Follow the evidence wherever it leads
|
||||
- Revise their beliefs when proven wrong
|
||||
- Act only when they have strong evidence
|
||||
- Understand the mechanism, not just the symptom
|
||||
|
||||
This is the difference between guessing and debugging.
|
||||
</excellence>
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
|
||||
<overview>
|
||||
These are systematic approaches to narrowing down bugs. Each technique is a tool in your debugging toolkit. The skill is knowing which tool to use when.
|
||||
</overview>
|
||||
|
||||
|
||||
<technique name="binary_search">
|
||||
**When to use**: Large codebase, long execution path, or many possible failure points.
|
||||
|
||||
**How it works**: Cut the problem space in half repeatedly until you isolate the issue.
|
||||
|
||||
**In practice**:
|
||||
|
||||
1. **Identify the boundaries**: Where does it work? Where does it fail?
|
||||
2. **Find the midpoint**: Add logging/testing at the middle of the execution path
|
||||
3. **Determine which half**: Does the bug occur before or after the midpoint?
|
||||
4. **Repeat**: Cut that half in half, test again
|
||||
5. **Converge**: Keep halving until you find the exact line
|
||||
|
||||
<example>
|
||||
Problem: API request returns wrong data
|
||||
|
||||
1. Test: Does the data leave the database correctly? YES
|
||||
2. Test: Does the data reach the frontend correctly? NO
|
||||
3. Test: Does the data leave the API route correctly? YES
|
||||
4. Test: Does the data survive serialization? NO
|
||||
5. **Found it**: Bug is in the serialization layer
|
||||
|
||||
You just eliminated 90% of the code in 4 tests.
|
||||
</example>
|
||||
</technique>
|
||||
|
||||
<technique name="comment_out_bisection">
|
||||
**Variant**: Commenting out code to find the breaking change.
|
||||
|
||||
1. Comment out the second half of a function
|
||||
2. Does it work now? The bug is in the commented section
|
||||
3. Uncomment half of that, repeat
|
||||
4. Converge on the problematic lines
|
||||
|
||||
**Warning**: Only works for code you can safely comment out. Don't use for initialization code.
|
||||
</technique>
|
||||
|
||||
|
||||
<technique name="rubber_duck">
|
||||
**When to use**: You're stuck, confused, or your mental model doesn't match reality.
|
||||
|
||||
**How it works**: Explain the problem out loud (to a rubber duck, a colleague, or in writing) in complete detail.
|
||||
|
||||
**Why it works**: Articulating forces you to:
|
||||
- Make assumptions explicit
|
||||
- Notice gaps in your understanding
|
||||
- Hear how convoluted your explanation sounds
|
||||
- Realize what you haven't actually verified
|
||||
|
||||
**In practice**:
|
||||
|
||||
Write or say out loud:
|
||||
1. "The system should do X"
|
||||
2. "Instead it does Y"
|
||||
3. "I think this is because Z"
|
||||
4. "The code path is: A → B → C → D"
|
||||
5. "I've verified that..." (List what you've actually tested)
|
||||
6. "I'm assuming that..." (List assumptions)
|
||||
|
||||
Often you'll spot the bug mid-explanation: "Wait, I never actually verified that B returns what I think it does."
|
||||
|
||||
<example>
|
||||
"So when the user clicks the button, it calls handleClick, which dispatches an action, which... wait, does the reducer actually handle this action type? Let me check... Oh. The reducer is looking for 'UPDATE_USER' but I'm dispatching 'USER_UPDATE'."
|
||||
</example>
|
||||
</technique>
|
||||
|
||||
|
||||
<technique name="minimal_reproduction">
|
||||
**When to use**: Complex system, many moving parts, unclear which part is failing.
|
||||
|
||||
**How it works**: Strip away everything until you have the smallest possible code that reproduces the bug.
|
||||
|
||||
**Why it works**:
|
||||
- Removes distractions
|
||||
- Isolates the actual issue
|
||||
- Often reveals the bug during the stripping process
|
||||
- Makes it easier to reason about
|
||||
|
||||
**Process**:
|
||||
|
||||
1. **Copy the failing code to a new file**
|
||||
2. **Remove one piece** (a dependency, a function, a feature)
|
||||
3. **Test**: Does it still reproduce?
|
||||
- YES: Keep it removed, continue
|
||||
- NO: Put it back, it's needed
|
||||
4. **Repeat** until you have the bare minimum
|
||||
5. **The bug is now obvious** in the stripped-down code
|
||||
|
||||
<example>
|
||||
Start with: 500-line React component with 15 props, 8 hooks, 3 contexts
|
||||
|
||||
End with:
|
||||
```jsx
|
||||
function MinimalRepro() {
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
setCount(count + 1); // Bug: infinite loop, missing dependency array
|
||||
});
|
||||
|
||||
return <div>{count}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
The bug was hidden in complexity. Minimal reproduction made it obvious.
|
||||
</example>
|
||||
</technique>
|
||||
|
||||
|
||||
<technique name="working_backwards">
|
||||
**When to use**: You know what the correct output should be, but don't know why you're not getting it.
|
||||
|
||||
**How it works**: Start from the desired end state and trace backwards through the execution path.
|
||||
|
||||
**Process**:
|
||||
|
||||
1. **Define the desired output precisely**
|
||||
2. **Ask**: What function produces this output?
|
||||
3. **Test that function**: Give it the input it should receive. Does it produce correct output?
|
||||
- YES: The bug is earlier (wrong input to this function)
|
||||
- NO: The bug is here
|
||||
4. **Repeat backwards** through the call stack
|
||||
5. **Find the divergence point**: Where does expected vs actual first differ?
|
||||
|
||||
<example>
|
||||
Problem: UI shows "User not found" when user exists
|
||||
|
||||
Trace backwards:
|
||||
1. UI displays: `user.error` → Is this the right value to display? YES
|
||||
2. Component receives: `user.error = "User not found"` → Is this correct? NO, should be null
|
||||
3. API returns: `{ error: "User not found" }` → Why?
|
||||
4. Database query: `SELECT * FROM users WHERE id = 'undefined'` → AH!
|
||||
5. **Found it**: The user ID is 'undefined' (string) instead of a number
|
||||
|
||||
Working backwards revealed the bug was in how the ID was passed to the query.
|
||||
</example>
|
||||
</technique>
|
||||
|
||||
|
||||
<technique name="differential_debugging">
|
||||
**When to use**: Something used to work and now doesn't. A feature works in one environment but not another.
|
||||
|
||||
**How it works**: Compare the working vs broken states to find what's different.
|
||||
|
||||
**Questions to ask**:
|
||||
|
||||
**Time-based** (it worked, now it doesn't):
|
||||
- What changed in the code since it worked?
|
||||
- What changed in the environment? (Node version, OS, dependencies)
|
||||
- What changed in the data? (Database schema, API responses)
|
||||
- What changed in the configuration?
|
||||
|
||||
**Environment-based** (works in dev, fails in prod):
|
||||
- What's different between environments?
|
||||
- Configuration values
|
||||
- Environment variables
|
||||
- Network conditions
|
||||
- Data volume
|
||||
- Third-party service behavior
|
||||
|
||||
**Process**:
|
||||
|
||||
1. **Make a list of differences** between working and broken
|
||||
2. **Test each difference** in isolation
|
||||
3. **Find the difference that causes the failure**
|
||||
4. **That difference reveals the root cause**
|
||||
|
||||
<example>
|
||||
Works locally, fails in CI:
|
||||
|
||||
Differences:
|
||||
- Node version: Same ✓
|
||||
- Environment variables: Same ✓
|
||||
- Timezone: Different! ✗
|
||||
|
||||
Test: Set local timezone to UTC (like CI)
|
||||
Result: Now fails locally too
|
||||
|
||||
**Found it**: Date comparison logic assumes local timezone
|
||||
</example>
|
||||
</technique>
|
||||
|
||||
|
||||
<technique name="observability_first">
|
||||
**When to use**: Always. Before making any fix.
|
||||
|
||||
**Why it matters**: You can't fix what you can't see. Add visibility before changing behavior.
|
||||
|
||||
**Approaches**:
|
||||
|
||||
**1. Strategic logging**
|
||||
```javascript
|
||||
// Not this (useless):
|
||||
console.log('in function');
|
||||
|
||||
// This (useful):
|
||||
console.log('[handleSubmit] Input:', { email, password: '***' });
|
||||
console.log('[handleSubmit] Validation result:', validationResult);
|
||||
console.log('[handleSubmit] API response:', response);
|
||||
```
|
||||
|
||||
**2. Assertion checks**
|
||||
```javascript
|
||||
function processUser(user) {
|
||||
console.assert(user !== null, 'User is null!');
|
||||
console.assert(user.id !== undefined, 'User ID is undefined!');
|
||||
// ... rest of function
|
||||
}
|
||||
```
|
||||
|
||||
**3. Timing measurements**
|
||||
```javascript
|
||||
console.time('Database query');
|
||||
const result = await db.query(sql);
|
||||
console.timeEnd('Database query');
|
||||
```
|
||||
|
||||
**4. Stack traces at key points**
|
||||
```javascript
|
||||
console.log('[updateUser] Called from:', new Error().stack);
|
||||
```
|
||||
|
||||
**The workflow**:
|
||||
1. **Add logging/instrumentation** at suspected points
|
||||
2. **Run the code**
|
||||
3. **Observe the output**
|
||||
4. **Form hypothesis** based on what you see
|
||||
5. **Only then** make changes
|
||||
|
||||
Don't code in the dark. Light up the execution path first.
|
||||
</technique>
|
||||
|
||||
|
||||
<technique name="comment_out_everything">
|
||||
**When to use**: Many possible interactions, unclear which code is causing the issue.
|
||||
|
||||
**How it works**:
|
||||
|
||||
1. **Comment out everything** in a function/file
|
||||
2. **Verify the bug is gone**
|
||||
3. **Uncomment one piece at a time**
|
||||
4. **After each uncomment, test**
|
||||
5. **When the bug returns**, you found the culprit
|
||||
|
||||
**Variant**: For config files, reset to defaults and add back one setting at a time.
|
||||
|
||||
<example>
|
||||
Problem: Some middleware breaks requests, but you have 8 middleware functions.
|
||||
|
||||
```javascript
|
||||
app.use(helmet()); // Uncomment, test → works
|
||||
app.use(cors()); // Uncomment, test → works
|
||||
app.use(compression()); // Uncomment, test → works
|
||||
app.use(bodyParser.json({ limit: '50mb' })); // Uncomment, test → BREAKS
|
||||
|
||||
// Found it: Body size limit too high causes memory issues
|
||||
```
|
||||
</example>
|
||||
</technique>
|
||||
|
||||
|
||||
<technique name="git_bisect">
|
||||
**When to use**: Feature worked in the past, broke at some unknown commit.
|
||||
|
||||
**How it works**: Binary search through git history to find the breaking commit.
|
||||
|
||||
**Process**:
|
||||
|
||||
```bash
|
||||
git bisect start
|
||||
|
||||
git bisect bad
|
||||
|
||||
git bisect good abc123
|
||||
|
||||
git bisect bad
|
||||
|
||||
git bisect good
|
||||
|
||||
```
|
||||
|
||||
**Why it's powerful**: Turns "it broke sometime in the last 100 commits" into "it broke in commit abc123" in ~7 tests (log₂ 100 ≈ 7).
|
||||
|
||||
<example>
|
||||
100 commits between working and broken
|
||||
Manual testing: 100 commits to check
|
||||
Git bisect: 7 commits to check
|
||||
|
||||
Time saved: Massive
|
||||
</example>
|
||||
</technique>
|
||||
|
||||
|
||||
<decision_tree>
|
||||
**Large codebase, many files**:
|
||||
→ Binary search / Divide and conquer
|
||||
|
||||
**Confused about what's happening**:
|
||||
→ Rubber duck debugging
|
||||
→ Observability first (add logging)
|
||||
|
||||
**Complex system with many interactions**:
|
||||
→ Minimal reproduction
|
||||
|
||||
**Know the desired output**:
|
||||
→ Working backwards
|
||||
|
||||
**Used to work, now doesn't**:
|
||||
→ Differential debugging
|
||||
→ Git bisect
|
||||
|
||||
**Many possible causes**:
|
||||
→ Comment out everything
|
||||
→ Binary search
|
||||
|
||||
**Always**:
|
||||
→ Observability first before making changes
|
||||
</decision_tree>
|
||||
|
||||
<combining_techniques>
|
||||
Often you'll use multiple techniques together:
|
||||
|
||||
1. **Differential debugging** to identify what changed
|
||||
2. **Binary search** to narrow down where in the code
|
||||
3. **Observability first** to add logging at that point
|
||||
4. **Rubber duck** to articulate what you're seeing
|
||||
5. **Minimal reproduction** to isolate just that behavior
|
||||
6. **Working backwards** to find the root cause
|
||||
|
||||
Techniques compose. Use as many as needed.
|
||||
</combining_techniques>
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
|
||||
<overview>
|
||||
The most common debugging mistake: declaring victory too early. A fix isn't complete until it's verified. This document defines what "verified" means and provides systematic approaches to proving your fix works.
|
||||
</overview>
|
||||
|
||||
|
||||
<definition>
|
||||
A fix is verified when:
|
||||
|
||||
1. **The original issue no longer occurs**
|
||||
- The exact reproduction steps now produce correct behavior
|
||||
- Not "it seems better" - it definitively works
|
||||
|
||||
2. **You understand why the fix works**
|
||||
- You can explain the mechanism
|
||||
- Not "I changed X and it worked" but "X was causing Y, and changing it prevents Y"
|
||||
|
||||
3. **Related functionality still works**
|
||||
- You haven't broken adjacent features
|
||||
- Regression testing passes
|
||||
|
||||
4. **The fix works across environments**
|
||||
- Not just on your machine
|
||||
- In production-like conditions
|
||||
|
||||
5. **The fix is stable**
|
||||
- Works consistently, not intermittently
|
||||
- Not just "worked once" but "works reliably"
|
||||
|
||||
**Anything less than this is not verified.**
|
||||
</definition>
|
||||
|
||||
<examples>
|
||||
❌ **Not verified**:
|
||||
- "I ran it once and it didn't crash"
|
||||
- "It seems to work now"
|
||||
- "The error message is gone" (but is the behavior correct?)
|
||||
- "Works on my machine"
|
||||
|
||||
✅ **Verified**:
|
||||
- "I ran the original reproduction steps 20 times - zero failures"
|
||||
- "The data now saves correctly and I can retrieve it"
|
||||
- "All existing tests pass, plus I added a test for this scenario"
|
||||
- "Verified in dev, staging, and production environments"
|
||||
</examples>
|
||||
|
||||
|
||||
<pattern name="reproduction_verification">
|
||||
**The golden rule**: If you can't reproduce the bug, you can't verify it's fixed.
|
||||
|
||||
**Process**:
|
||||
|
||||
1. **Before fixing**: Document exact steps to reproduce
|
||||
```markdown
|
||||
Reproduction steps:
|
||||
1. Login as admin user
|
||||
2. Navigate to /settings
|
||||
3. Click "Export Data" button
|
||||
4. Observe: Error "Cannot read property 'data' of undefined"
|
||||
```
|
||||
|
||||
2. **After fixing**: Execute the same steps exactly
|
||||
```markdown
|
||||
Verification:
|
||||
1. Login as admin user ✓
|
||||
2. Navigate to /settings ✓
|
||||
3. Click "Export Data" button ✓
|
||||
4. Observe: CSV downloads successfully ✓
|
||||
```
|
||||
|
||||
3. **Test edge cases** related to the bug
|
||||
```markdown
|
||||
Additional tests:
|
||||
- Export with empty data set ✓
|
||||
- Export with 1000+ records ✓
|
||||
- Export while another request is pending ✓
|
||||
```
|
||||
|
||||
**If you can't reproduce the original bug**:
|
||||
- You don't know if your fix worked
|
||||
- Maybe it's still broken
|
||||
- Maybe your "fix" did nothing
|
||||
- Maybe you fixed a different bug
|
||||
|
||||
**Solution**: Revert your fix. If the bug comes back, you've verified your fix addressed it.
|
||||
</pattern>
|
||||
|
||||
|
||||
<pattern name="regression_testing">
|
||||
**The problem**: You fix one thing, break another.
|
||||
|
||||
**Why it happens**:
|
||||
- Your fix changed shared code
|
||||
- Your fix had unintended side effects
|
||||
- Your fix broke an assumption other code relied on
|
||||
|
||||
**Protection strategy**:
|
||||
|
||||
**1. Identify adjacent functionality**
|
||||
- What else uses the code you changed?
|
||||
- What features depend on this behavior?
|
||||
- What workflows include this step?
|
||||
|
||||
**2. Test each adjacent area**
|
||||
- Manually test the happy path
|
||||
- Check error handling
|
||||
- Verify data integrity
|
||||
|
||||
**3. Run existing tests**
|
||||
- Unit tests for the module
|
||||
- Integration tests for the feature
|
||||
- End-to-end tests for the workflow
|
||||
|
||||
<example>
|
||||
**Fix**: Changed how user sessions are stored (from memory to database)
|
||||
|
||||
**Adjacent functionality to verify**:
|
||||
- Login still works ✓
|
||||
- Logout still works ✓
|
||||
- Session timeout still works ✓
|
||||
- Concurrent logins are handled correctly ✓
|
||||
- Session data persists across server restarts ✓ (new capability)
|
||||
- Password reset flow still works ✓
|
||||
- OAuth login still works ✓
|
||||
|
||||
If you only tested "login works", you missed 6 other things that could break.
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
|
||||
<pattern name="test_first_debugging">
|
||||
**Strategy**: Write a failing test that reproduces the bug, then fix until the test passes.
|
||||
|
||||
**Benefits**:
|
||||
- Proves you can reproduce the bug
|
||||
- Provides automatic verification
|
||||
- Prevents regression in the future
|
||||
- Forces you to understand the bug precisely
|
||||
|
||||
**Process**:
|
||||
|
||||
1. **Write a test that reproduces the bug**
|
||||
```javascript
|
||||
test('should handle undefined user data gracefully', () => {
|
||||
const result = processUserData(undefined);
|
||||
expect(result).toBe(null); // Currently throws error
|
||||
});
|
||||
```
|
||||
|
||||
2. **Verify the test fails** (confirms it reproduces the bug)
|
||||
```
|
||||
✗ should handle undefined user data gracefully
|
||||
TypeError: Cannot read property 'name' of undefined
|
||||
```
|
||||
|
||||
3. **Fix the code**
|
||||
```javascript
|
||||
function processUserData(user) {
|
||||
if (!user) return null; // Add defensive check
|
||||
return user.name;
|
||||
}
|
||||
```
|
||||
|
||||
4. **Verify the test passes**
|
||||
```
|
||||
✓ should handle undefined user data gracefully
|
||||
```
|
||||
|
||||
5. **Test is now regression protection**
|
||||
- If someone breaks this again, the test will catch it
|
||||
|
||||
**When to use**:
|
||||
- Clear, reproducible bugs
|
||||
- Code that has test infrastructure
|
||||
- Bugs that could recur
|
||||
|
||||
**When not to use**:
|
||||
- Exploratory debugging (you don't understand the bug yet)
|
||||
- Infrastructure issues (can't easily test)
|
||||
- One-off data issues
|
||||
</pattern>
|
||||
|
||||
|
||||
<pattern name="environment_verification">
|
||||
**The trap**: "Works on my machine"
|
||||
|
||||
**Reality**: Production is different.
|
||||
|
||||
**Differences to consider**:
|
||||
|
||||
**Environment variables**:
|
||||
- `NODE_ENV=development` vs `NODE_ENV=production`
|
||||
- Different API keys
|
||||
- Different database connections
|
||||
- Different feature flags
|
||||
|
||||
**Dependencies**:
|
||||
- Different package versions (if not locked)
|
||||
- Different system libraries
|
||||
- Different Node/Python/etc versions
|
||||
|
||||
**Data**:
|
||||
- Volume (100 records locally, 1M in production)
|
||||
- Quality (clean test data vs messy real data)
|
||||
- Edge cases (nulls, special characters, extreme values)
|
||||
|
||||
**Network**:
|
||||
- Latency (local: 5ms, production: 200ms)
|
||||
- Reliability (local: perfect, production: occasional failures)
|
||||
- Firewalls, proxies, load balancers
|
||||
|
||||
**Verification checklist**:
|
||||
```markdown
|
||||
- [ ] Works locally (dev environment)
|
||||
- [ ] Works in Docker container (mimics production)
|
||||
- [ ] Works in staging (production-like)
|
||||
- [ ] Works in production (the real test)
|
||||
```
|
||||
|
||||
<example>
|
||||
**Bug**: Batch processing fails in production but works locally
|
||||
|
||||
**Investigation**:
|
||||
- Local: 100 test records, completes in 2 seconds
|
||||
- Production: 50,000 records, times out at 30 seconds
|
||||
|
||||
**The difference**: Volume. Local testing didn't catch it.
|
||||
|
||||
**Fix verification**:
|
||||
- Test locally with 50,000 records
|
||||
- Verify performance in staging
|
||||
- Monitor first production run
|
||||
- Confirm all environments work
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
|
||||
<pattern name="stability_testing">
|
||||
**The problem**: It worked once, but will it work reliably?
|
||||
|
||||
**Intermittent bugs are the worst**:
|
||||
- Hard to reproduce
|
||||
- Hard to verify fixes
|
||||
- Easy to declare fixed when they're not
|
||||
|
||||
**Verification strategies**:
|
||||
|
||||
**1. Repeated execution**
|
||||
```bash
|
||||
for i in {1..100}; do
|
||||
npm test -- specific-test.js || echo "Failed on run $i"
|
||||
done
|
||||
```
|
||||
|
||||
If it fails even once, it's not fixed.
|
||||
|
||||
**2. Stress testing**
|
||||
```javascript
|
||||
// Run many instances in parallel
|
||||
const promises = Array(50).fill().map(() =>
|
||||
processData(testInput)
|
||||
);
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
// All results should be correct
|
||||
```
|
||||
|
||||
**3. Soak testing**
|
||||
- Run for extended period (hours, days)
|
||||
- Monitor for memory leaks, performance degradation
|
||||
- Ensure stability over time
|
||||
|
||||
**4. Timing variations**
|
||||
```javascript
|
||||
// For race conditions, add random delays
|
||||
async function testWithRandomTiming() {
|
||||
await randomDelay(0, 100);
|
||||
triggerAction1();
|
||||
await randomDelay(0, 100);
|
||||
triggerAction2();
|
||||
await randomDelay(0, 100);
|
||||
verifyResult();
|
||||
}
|
||||
|
||||
// Run this 1000 times
|
||||
```
|
||||
|
||||
<example>
|
||||
**Bug**: Race condition in file upload
|
||||
|
||||
**Weak verification**:
|
||||
- Upload one file
|
||||
- "It worked!"
|
||||
- Ship it
|
||||
|
||||
**Strong verification**:
|
||||
- Upload 100 files sequentially: all succeed ✓
|
||||
- Upload 20 files in parallel: all succeed ✓
|
||||
- Upload while navigating away: handles correctly ✓
|
||||
- Upload, cancel, upload again: works ✓
|
||||
- Run all tests 50 times: zero failures ✓
|
||||
|
||||
Now it's verified.
|
||||
</example>
|
||||
</pattern>
|
||||
|
||||
|
||||
<checklist>
|
||||
Copy this checklist when verifying a fix:
|
||||
|
||||
```markdown
|
||||
|
||||
### Original Issue
|
||||
- [ ] Can reproduce the original bug before the fix
|
||||
- [ ] Have documented exact reproduction steps
|
||||
|
||||
### Fix Validation
|
||||
- [ ] Original reproduction steps now work correctly
|
||||
- [ ] Can explain WHY the fix works
|
||||
- [ ] Fix is minimal and targeted
|
||||
|
||||
### Regression Testing
|
||||
- [ ] Adjacent feature 1: [name] works
|
||||
- [ ] Adjacent feature 2: [name] works
|
||||
- [ ] Adjacent feature 3: [name] works
|
||||
- [ ] Existing tests pass
|
||||
- [ ] Added test to prevent regression
|
||||
|
||||
### Environment Testing
|
||||
- [ ] Works in development
|
||||
- [ ] Works in staging/QA
|
||||
- [ ] Works in production
|
||||
- [ ] Tested with production-like data volume
|
||||
|
||||
### Stability Testing
|
||||
- [ ] Tested multiple times (n=__): zero failures
|
||||
- [ ] Tested edge cases: [list them]
|
||||
- [ ] Tested under load/stress: stable
|
||||
|
||||
### Documentation
|
||||
- [ ] Code comments explain the fix
|
||||
- [ ] Commit message explains the root cause
|
||||
- [ ] If needed, updated user-facing docs
|
||||
|
||||
### Sign-off
|
||||
- [ ] I understand why this bug occurred
|
||||
- [ ] I understand why this fix works
|
||||
- [ ] I've verified it works in all relevant environments
|
||||
- [ ] I've tested for regressions
|
||||
- [ ] I'm confident this won't recur
|
||||
```
|
||||
|
||||
**Do not merge/deploy until all checkboxes are checked.**
|
||||
</checklist>
|
||||
|
||||
|
||||
<distrust>
|
||||
Your verification might be wrong if:
|
||||
|
||||
**1. You can't reproduce the original bug anymore**
|
||||
- Maybe you forgot how
|
||||
- Maybe the environment changed
|
||||
- Maybe you're testing the wrong thing
|
||||
- **Action**: Document reproduction steps FIRST, before fixing
|
||||
|
||||
**2. The fix is large or complex**
|
||||
- Changed 10 files, modified 200 lines
|
||||
- Too many moving parts
|
||||
- **Action**: Simplify the fix, then verify each piece
|
||||
|
||||
**3. You're not sure why it works**
|
||||
- "I changed X and the bug went away"
|
||||
- But you can't explain the mechanism
|
||||
- **Action**: Investigate until you understand, then verify
|
||||
|
||||
**4. It only works sometimes**
|
||||
- "Usually works now"
|
||||
- "Seems more stable"
|
||||
- **Action**: Not verified. Find and fix the remaining issue
|
||||
|
||||
**5. You can't test in production-like conditions**
|
||||
- Only tested locally
|
||||
- Different data, different scale
|
||||
- **Action**: Set up staging environment or use production data in dev
|
||||
|
||||
**Red flag phrases**:
|
||||
- "It seems to work"
|
||||
- "I think it's fixed"
|
||||
- "Looks good to me"
|
||||
- "Can't reproduce anymore" (but you never could reliably)
|
||||
|
||||
**Trust-building phrases**:
|
||||
- "I've verified 50 times - zero failures"
|
||||
- "All tests pass including new regression test"
|
||||
- "Deployed to staging, tested for 3 days, no issues"
|
||||
- "Root cause was X, fix addresses X directly, verified by Y"
|
||||
</distrust>
|
||||
|
||||
|
||||
<mindset>
|
||||
**Assume your fix is wrong until proven otherwise.**
|
||||
|
||||
This isn't pessimism - it's professionalism.
|
||||
|
||||
**Questions to ask yourself**:
|
||||
- "How could this fix fail?"
|
||||
- "What haven't I tested?"
|
||||
- "What am I assuming?"
|
||||
- "Would this survive production?"
|
||||
|
||||
**The cost of insufficient verification**:
|
||||
- Bug returns in production
|
||||
- User frustration
|
||||
- Lost trust
|
||||
- Emergency debugging sessions
|
||||
- Rollbacks
|
||||
|
||||
**The benefit of thorough verification**:
|
||||
- Confidence in deployment
|
||||
- Prevention of regressions
|
||||
- Trust from team
|
||||
- Learning from the investigation
|
||||
|
||||
**Verification is not optional. It's the most important part of debugging.**
|
||||
</mindset>
|
||||
|
|
@ -0,0 +1,361 @@
|
|||
|
||||
<overview>
|
||||
Debugging requires both reasoning about code and researching external knowledge. The skill is knowing when to use each. This guide helps you recognize signals that indicate you need external knowledge vs when you can reason through the problem with the code in front of you.
|
||||
</overview>
|
||||
|
||||
|
||||
<research_signals>
|
||||
|
||||
**1. Error messages you don't recognize**
|
||||
- Stack traces from libraries you haven't used
|
||||
- Cryptic system errors
|
||||
- Framework-specific error codes
|
||||
|
||||
**Action**: Web search the exact error message in quotes
|
||||
- Often leads to GitHub issues, Stack Overflow, or official docs
|
||||
- Others have likely encountered this
|
||||
|
||||
<example>
|
||||
Error: `EADDRINUSE: address already in use :::3000`
|
||||
|
||||
This is a system-level error. Research it:
|
||||
- Web search: "EADDRINUSE address already in use"
|
||||
- Learn: Port is already occupied by another process
|
||||
- Solution: Find and kill the process, or use different port
|
||||
</example>
|
||||
|
||||
**2. Library/framework behavior doesn't match expectations**
|
||||
- You're using a library correctly (you think) but it's not working
|
||||
- Documentation seems to contradict behavior
|
||||
- Version-specific quirks
|
||||
|
||||
**Action**: Check official documentation and recent issues
|
||||
- Use Context7 MCP for library docs
|
||||
- Search GitHub issues for the library
|
||||
- Check if there are breaking changes in recent versions
|
||||
|
||||
<example>
|
||||
You're using `useEffect` in React but it's running on every render despite empty dependency array.
|
||||
|
||||
Research needed:
|
||||
- Check React docs for useEffect rules
|
||||
- Search: "useEffect running on every render"
|
||||
- Discover: React 18 StrictMode runs effects twice in dev mode
|
||||
</example>
|
||||
|
||||
**3. Domain knowledge gaps**
|
||||
- Debugging authentication: need to understand OAuth flow
|
||||
- Debugging database: need to understand indexes, query optimization
|
||||
- Debugging networking: need to understand HTTP caching, CORS
|
||||
|
||||
**Action**: Research the domain concept, not just the specific bug
|
||||
- Use MCP servers for domain knowledge
|
||||
- Read official specifications
|
||||
- Find authoritative guides
|
||||
|
||||
**4. Platform-specific behavior**
|
||||
- "Works in Chrome but not Safari"
|
||||
- "Works on Mac but not Windows"
|
||||
- "Works in Node 16 but not Node 18"
|
||||
|
||||
**Action**: Research platform differences
|
||||
- Browser compatibility tables
|
||||
- Platform-specific documentation
|
||||
- Known platform bugs
|
||||
|
||||
**5. Recent changes in ecosystem**
|
||||
- Package update broke something
|
||||
- New framework version behaves differently
|
||||
- Deprecated API
|
||||
|
||||
**Action**: Check changelogs and migration guides
|
||||
- Library CHANGELOG.md
|
||||
- Migration guides
|
||||
- "Breaking changes" documentation
|
||||
|
||||
</research_signals>
|
||||
|
||||
|
||||
<reasoning_signals>
|
||||
|
||||
**1. The bug is in YOUR code**
|
||||
- Not library behavior, not system issues
|
||||
- Your business logic, your data structures
|
||||
- Code you or your team wrote
|
||||
|
||||
**Approach**: Read the code, trace execution, add logging
|
||||
- You have full access to the code
|
||||
- You can modify it to add observability
|
||||
- No external documentation will help
|
||||
|
||||
<example>
|
||||
Bug: Shopping cart total calculates incorrectly
|
||||
|
||||
This is your logic:
|
||||
```javascript
|
||||
function calculateTotal(items) {
|
||||
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
|
||||
}
|
||||
```
|
||||
|
||||
Don't research "shopping cart calculation bugs"
|
||||
DO reason through it:
|
||||
- Log each item's price and quantity
|
||||
- Log the running sum
|
||||
- Trace the logic step by step
|
||||
</example>
|
||||
|
||||
**2. You have all the information needed**
|
||||
- The bug is reproducible
|
||||
- You can read all relevant code
|
||||
- No external dependencies involved
|
||||
|
||||
**Approach**: Use investigation techniques
|
||||
- Binary search to narrow down
|
||||
- Minimal reproduction
|
||||
- Working backwards
|
||||
- Add observability
|
||||
|
||||
**3. It's a logic error, not a knowledge gap**
|
||||
- Off-by-one errors
|
||||
- Wrong conditional
|
||||
- State management issue
|
||||
- Data transformation bug
|
||||
|
||||
**Approach**: Trace the logic carefully
|
||||
- Print intermediate values
|
||||
- Check assumptions
|
||||
- Verify each step
|
||||
|
||||
**4. The answer is in the behavior, not the documentation**
|
||||
- "What is this function actually doing?"
|
||||
- "Why is this value null?"
|
||||
- "When does this code execute?"
|
||||
|
||||
**Approach**: Observe the actual behavior
|
||||
- Add logging
|
||||
- Use a debugger
|
||||
- Test with different inputs
|
||||
|
||||
</reasoning_signals>
|
||||
|
||||
|
||||
<research_how>
|
||||
|
||||
**Web Search - When and How**
|
||||
|
||||
**When**:
|
||||
- Error messages
|
||||
- Library-specific questions
|
||||
- "How to X in framework Y"
|
||||
- Troubleshooting platform issues
|
||||
|
||||
**How**:
|
||||
- Use exact error messages in quotes: `"Cannot read property 'map' of undefined"`
|
||||
- Include framework/library version: `"react 18 useEffect behavior"`
|
||||
- Add "github issue" for known bugs: `"prisma connection pool github issue"`
|
||||
- Add year for recent changes: `"nextjs 14 middleware 2024"`
|
||||
|
||||
**Good search queries**:
|
||||
- `"ECONNREFUSED" node.js postgres`
|
||||
- `"Maximum update depth exceeded" react hooks`
|
||||
- `typescript generic constraints examples`
|
||||
|
||||
**Bad search queries**:
|
||||
- `my code doesn't work` (too vague)
|
||||
- `bug in react` (too broad)
|
||||
- `help` (useless)
|
||||
|
||||
**Context7 MCP - When and How**
|
||||
|
||||
**When**:
|
||||
- Need API reference
|
||||
- Understanding library concepts
|
||||
- Finding specific function signatures
|
||||
- Learning correct usage patterns
|
||||
|
||||
**How**:
|
||||
```
|
||||
Use mcp__context7__resolve-library-id with library name
|
||||
Then mcp__context7__get-library-docs with library ID
|
||||
Ask specific questions about the library
|
||||
```
|
||||
|
||||
**Good uses**:
|
||||
- "How do I use Prisma transactions?"
|
||||
- "What are the parameters for stripe.customers.create?"
|
||||
- "How does Express middleware error handling work?"
|
||||
|
||||
**Bad uses**:
|
||||
- "Fix my bug" (too vague, Context7 provides docs not debugging)
|
||||
- "Why isn't my code working?" (need to research specific concepts, not general debugging)
|
||||
|
||||
**GitHub Issues Search**
|
||||
|
||||
**When**:
|
||||
- Experiencing behavior that seems like a bug
|
||||
- Library not working as documented
|
||||
- Looking for workarounds
|
||||
|
||||
**How**:
|
||||
- Search in the library's GitHub repo
|
||||
- Include relevant keywords
|
||||
- Check both open and closed issues
|
||||
- Look for issues with "bug" or "regression" labels
|
||||
|
||||
**Official Documentation**
|
||||
|
||||
**When**:
|
||||
- Learning how something should work
|
||||
- Checking if you're using API correctly
|
||||
- Understanding configuration options
|
||||
- Finding migration guides
|
||||
|
||||
**How**:
|
||||
- Start with official docs, not blog posts
|
||||
- Check version-specific docs
|
||||
- Read examples and guides, not just API reference
|
||||
- Look for "Common Pitfalls" or "Troubleshooting" sections
|
||||
|
||||
</research_how>
|
||||
|
||||
|
||||
<balance>
|
||||
|
||||
**The research trap**: Spending hours reading docs about topics tangential to your bug
|
||||
- You think it's a caching issue, so you read all about cache invalidation
|
||||
- But the actual bug is a typo in a variable name
|
||||
|
||||
**The reasoning trap**: Spending hours reading code when the answer is well-documented
|
||||
- You're debugging why auth doesn't work
|
||||
- The docs clearly explain the setup you missed
|
||||
- You could have found it in 5 minutes of reading
|
||||
|
||||
**The balance**:
|
||||
|
||||
1. **Start with quick research** (5-10 minutes)
|
||||
- Search the error message
|
||||
- Check official docs for the feature you're using
|
||||
- Skim recent issues
|
||||
|
||||
2. **If research doesn't yield answers, switch to reasoning**
|
||||
- Add logging
|
||||
- Trace execution
|
||||
- Form hypotheses
|
||||
|
||||
3. **If reasoning reveals knowledge gaps, research those specific gaps**
|
||||
- "I need to understand how WebSocket reconnection works"
|
||||
- "I need to know if this library supports transactions"
|
||||
|
||||
4. **Alternate as needed**
|
||||
- Research → reveals what to investigate
|
||||
- Reasoning → reveals what to research
|
||||
- Keep switching based on what you learn
|
||||
|
||||
<example>
|
||||
**Bug**: Real-time updates stop working after 1 hour
|
||||
|
||||
**Start with research** (5 min):
|
||||
- Search: "websocket connection drops after 1 hour"
|
||||
- Find: Common issue with load balancers having connection timeouts
|
||||
|
||||
**Switch to reasoning**:
|
||||
- Check if you're using a load balancer: YES
|
||||
- Check load balancer timeout setting: 3600 seconds (1 hour)
|
||||
- Hypothesis: Load balancer is killing the connection
|
||||
|
||||
**Quick research**:
|
||||
- Search: "websocket load balancer timeout fix"
|
||||
- Find: Implement heartbeat/ping to keep connection alive
|
||||
|
||||
**Reasoning**:
|
||||
- Check if library supports heartbeat: YES
|
||||
- Implement ping every 30 seconds
|
||||
- Test: Connection stays alive for 3+ hours
|
||||
|
||||
**Total time**: 20 minutes (research: 10 min, reasoning: 10 min)
|
||||
**Success**: Found and fixed the issue
|
||||
|
||||
vs
|
||||
|
||||
**Wrong approach**: Spend 2 hours reading WebSocket spec
|
||||
- Learned a lot about WebSocket protocol
|
||||
- Didn't solve the problem (it was a config issue)
|
||||
</example>
|
||||
|
||||
</balance>
|
||||
|
||||
|
||||
<decision_tree>
|
||||
```
|
||||
Is this a error message I don't recognize?
|
||||
├─ YES → Web search the error message
|
||||
└─ NO ↓
|
||||
|
||||
Is this library/framework behavior I don't understand?
|
||||
├─ YES → Check docs (Context7 or official docs)
|
||||
└─ NO ↓
|
||||
|
||||
Is this code I/my team wrote?
|
||||
├─ YES → Reason through it (logging, tracing, hypothesis testing)
|
||||
└─ NO ↓
|
||||
|
||||
Is this a platform/environment difference?
|
||||
├─ YES → Research platform-specific behavior
|
||||
└─ NO ↓
|
||||
|
||||
Can I observe the behavior directly?
|
||||
├─ YES → Add observability and reason through it
|
||||
└─ NO → Research the domain/concept first, then reason
|
||||
```
|
||||
</decision_tree>
|
||||
|
||||
|
||||
<red_flags>
|
||||
|
||||
**You're researching too much if**:
|
||||
- You've read 20 blog posts but haven't looked at your code
|
||||
- You understand the theory but haven't traced your actual execution
|
||||
- You're learning about edge cases that don't apply to your situation
|
||||
- You've been reading for 30+ minutes without testing anything
|
||||
|
||||
**You're reasoning too much if**:
|
||||
- You've been staring at code for an hour without progress
|
||||
- You keep finding things you don't understand and guessing
|
||||
- You're debugging library internals (that's research territory)
|
||||
- The error message is clearly from a library you don't know
|
||||
|
||||
**You're doing it right if**:
|
||||
- You alternate between research and reasoning
|
||||
- Each research session answers a specific question
|
||||
- Each reasoning session tests a specific hypothesis
|
||||
- You're making steady progress toward understanding
|
||||
|
||||
</red_flags>
|
||||
|
||||
|
||||
<mindset>
|
||||
|
||||
**Good researchers ask**:
|
||||
- "What specific question do I need answered?"
|
||||
- "Where is the authoritative source for this?"
|
||||
- "Is this a known issue or unique to my code?"
|
||||
- "What version-specific information do I need?"
|
||||
|
||||
**Good reasoners ask**:
|
||||
- "What is actually happening in my code?"
|
||||
- "What am I assuming that might be wrong?"
|
||||
- "How can I observe this behavior directly?"
|
||||
- "What experiment would test my hypothesis?"
|
||||
|
||||
**Great debuggers do both**:
|
||||
- Research to fill knowledge gaps
|
||||
- Reason to understand actual behavior
|
||||
- Switch fluidly based on what they learn
|
||||
- Never stuck in one mode
|
||||
|
||||
**The goal**: Minimum time to maximum understanding.
|
||||
- Research what you don't know
|
||||
- Reason through what you can observe
|
||||
- Fix what you understand
|
||||
</mindset>
|
||||
45
src/resources/skills/frontend-design/SKILL.md
Normal file
45
src/resources/skills/frontend-design/SKILL.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
|
||||
license: Complete terms in LICENSE.txt
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
|
||||
|
||||
## Design Thinking
|
||||
|
||||
Before coding, understand the context and commit to a BOLD aesthetic direction:
|
||||
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
|
||||
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
Focus on:
|
||||
|
||||
- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
|
||||
- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
|
||||
- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
|
||||
- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
|
||||
|
||||
NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
|
||||
|
||||
**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
208
src/resources/skills/swiftui/SKILL.md
Normal file
208
src/resources/skills/swiftui/SKILL.md
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
---
|
||||
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>
|
||||
921
src/resources/skills/swiftui/references/animations.md
Normal file
921
src/resources/skills/swiftui/references/animations.md
Normal file
|
|
@ -0,0 +1,921 @@
|
|||
<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>
|
||||
1561
src/resources/skills/swiftui/references/architecture.md
Normal file
1561
src/resources/skills/swiftui/references/architecture.md
Normal file
File diff suppressed because it is too large
Load diff
1186
src/resources/skills/swiftui/references/layout-system.md
Normal file
1186
src/resources/skills/swiftui/references/layout-system.md
Normal file
File diff suppressed because it is too large
Load diff
1492
src/resources/skills/swiftui/references/navigation.md
Normal file
1492
src/resources/skills/swiftui/references/navigation.md
Normal file
File diff suppressed because it is too large
Load diff
214
src/resources/skills/swiftui/references/networking-async.md
Normal file
214
src/resources/skills/swiftui/references/networking-async.md
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<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>
|
||||
1706
src/resources/skills/swiftui/references/performance.md
Normal file
1706
src/resources/skills/swiftui/references/performance.md
Normal file
File diff suppressed because it is too large
Load diff
204
src/resources/skills/swiftui/references/platform-integration.md
Normal file
204
src/resources/skills/swiftui/references/platform-integration.md
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
<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>
|
||||
1443
src/resources/skills/swiftui/references/state-management.md
Normal file
1443
src/resources/skills/swiftui/references/state-management.md
Normal file
File diff suppressed because it is too large
Load diff
297
src/resources/skills/swiftui/references/swiftdata.md
Normal file
297
src/resources/skills/swiftui/references/swiftdata.md
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
<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>
|
||||
247
src/resources/skills/swiftui/references/testing-debugging.md
Normal file
247
src/resources/skills/swiftui/references/testing-debugging.md
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
<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>
|
||||
218
src/resources/skills/swiftui/references/uikit-appkit-interop.md
Normal file
218
src/resources/skills/swiftui/references/uikit-appkit-interop.md
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
<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>
|
||||
191
src/resources/skills/swiftui/workflows/add-feature.md
Normal file
191
src/resources/skills/swiftui/workflows/add-feature.md
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<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>
|
||||
311
src/resources/skills/swiftui/workflows/build-new-app.md
Normal file
311
src/resources/skills/swiftui/workflows/build-new-app.md
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
<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>
|
||||
192
src/resources/skills/swiftui/workflows/debug-swiftui.md
Normal file
192
src/resources/skills/swiftui/workflows/debug-swiftui.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
<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>
|
||||
197
src/resources/skills/swiftui/workflows/optimize-performance.md
Normal file
197
src/resources/skills/swiftui/workflows/optimize-performance.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<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>
|
||||
203
src/resources/skills/swiftui/workflows/ship-app.md
Normal file
203
src/resources/skills/swiftui/workflows/ship-app.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
<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>
|
||||
235
src/resources/skills/swiftui/workflows/write-tests.md
Normal file
235
src/resources/skills/swiftui/workflows/write-tests.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<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>
|
||||
Loading…
Add table
Reference in a new issue