Initial commit
This commit is contained in:
145
skills/expertise/macos-apps/workflows/add-feature.md
Normal file
145
skills/expertise/macos-apps/workflows/add-feature.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# Workflow: Add a Feature to an Existing App
|
||||
|
||||
<required_reading>
|
||||
**Read these reference files NOW:**
|
||||
1. references/app-architecture.md
|
||||
2. references/swiftui-patterns.md
|
||||
|
||||
**Plus relevant refs based on feature type** (see Step 2).
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
## Step 1: Understand the Feature
|
||||
|
||||
Ask the user:
|
||||
- What should the feature do?
|
||||
- Where in the app does it belong?
|
||||
- Any specific requirements or constraints?
|
||||
|
||||
## Step 2: Read Relevant References
|
||||
|
||||
Based on feature type, read additional references:
|
||||
|
||||
| Feature Type | Additional References |
|
||||
|--------------|----------------------|
|
||||
| Data persistence | references/data-persistence.md |
|
||||
| Networking/API | references/networking.md |
|
||||
| File handling | references/document-apps.md |
|
||||
| Background tasks | references/concurrency-patterns.md |
|
||||
| System integration | references/system-apis.md |
|
||||
| Menu bar | references/menu-bar-apps.md |
|
||||
| Extensions | references/app-extensions.md |
|
||||
| UI polish | references/design-system.md, references/macos-polish.md |
|
||||
|
||||
## Step 3: Understand Existing Code
|
||||
|
||||
Read the relevant parts of the existing codebase:
|
||||
- App entry point (usually AppName.swift or AppNameApp.swift)
|
||||
- State management (AppState, models)
|
||||
- Existing views related to the feature area
|
||||
|
||||
Identify:
|
||||
- How state flows through the app
|
||||
- Existing patterns to follow
|
||||
- Where the new feature fits
|
||||
|
||||
## Step 4: Plan the Implementation
|
||||
|
||||
Before writing code:
|
||||
1. Identify new files/types needed
|
||||
2. Identify existing files to modify
|
||||
3. Plan the data flow
|
||||
4. Consider edge cases
|
||||
|
||||
## Step 5: Implement with TDD
|
||||
|
||||
Follow test-driven development:
|
||||
1. Write failing test for new behavior
|
||||
2. Run → RED
|
||||
3. Implement minimal code
|
||||
4. Run → GREEN
|
||||
5. Refactor
|
||||
6. Repeat
|
||||
|
||||
## Step 6: Integrate
|
||||
|
||||
- Wire up new views to navigation
|
||||
- Connect to existing state management
|
||||
- Add menu items/shortcuts if applicable
|
||||
- Handle errors gracefully
|
||||
|
||||
## Step 7: Build and Test
|
||||
|
||||
```bash
|
||||
# Build
|
||||
xcodebuild -project AppName.xcodeproj -scheme AppName build 2>&1 | xcsift
|
||||
|
||||
# Run tests
|
||||
xcodebuild test -project AppName.xcodeproj -scheme AppName
|
||||
|
||||
# Launch for manual testing
|
||||
open ./build/Build/Products/Debug/AppName.app
|
||||
```
|
||||
|
||||
## Step 8: Polish
|
||||
|
||||
- Add keyboard shortcuts (references/macos-polish.md)
|
||||
- Ensure accessibility
|
||||
- Match existing UI patterns
|
||||
</process>
|
||||
|
||||
<integration_patterns>
|
||||
**Adding to state:**
|
||||
```swift
|
||||
// In AppState
|
||||
@Observable
|
||||
class AppState {
|
||||
// Add new property
|
||||
var newFeatureData: [NewType] = []
|
||||
|
||||
// Add new methods
|
||||
func performNewFeature() { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**Adding a new view:**
|
||||
```swift
|
||||
struct NewFeatureView: View {
|
||||
@Environment(AppState.self) private var appState
|
||||
|
||||
var body: some View {
|
||||
// Use existing patterns from app
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Adding to navigation:**
|
||||
```swift
|
||||
// In existing NavigationSplitView or similar
|
||||
NavigationLink("New Feature", destination: NewFeatureView())
|
||||
```
|
||||
|
||||
**Adding menu command:**
|
||||
```swift
|
||||
struct AppCommands: Commands {
|
||||
var body: some Commands {
|
||||
CommandGroup(after: .newItem) {
|
||||
Button("New Feature Action") {
|
||||
// action
|
||||
}
|
||||
.keyboardShortcut("N", modifiers: [.command, .shift])
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
</integration_patterns>
|
||||
|
||||
<success_criteria>
|
||||
Feature is complete when:
|
||||
- Functionality works as specified
|
||||
- Tests pass
|
||||
- Follows existing code patterns
|
||||
- UI matches app style
|
||||
- Keyboard shortcuts work
|
||||
- No regressions in existing features
|
||||
</success_criteria>
|
||||
98
skills/expertise/macos-apps/workflows/build-new-app.md
Normal file
98
skills/expertise/macos-apps/workflows/build-new-app.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# Workflow: Build a New macOS App
|
||||
|
||||
<required_reading>
|
||||
**Read these reference files NOW before writing any code:**
|
||||
1. references/project-scaffolding.md
|
||||
2. references/cli-workflow.md
|
||||
3. references/app-architecture.md
|
||||
4. references/swiftui-patterns.md
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
## Step 1: Clarify Requirements
|
||||
|
||||
Ask the user:
|
||||
- What does the app do? (core functionality)
|
||||
- What type of app? (document-based, shoebox/library, menu bar utility, single-window)
|
||||
- Any specific features needed? (persistence, networking, system integration)
|
||||
|
||||
## Step 2: Choose App Archetype
|
||||
|
||||
Based on requirements, select:
|
||||
|
||||
| Type | When to Use | Reference |
|
||||
|------|-------------|-----------|
|
||||
| Document-based | User creates/saves files | references/document-apps.md |
|
||||
| Shoebox/Library | Internal database, no explicit save | references/shoebox-apps.md |
|
||||
| Menu bar utility | Background functionality, quick actions | references/menu-bar-apps.md |
|
||||
| Single-window | Focused task, simple UI | (use base patterns) |
|
||||
|
||||
Read the relevant app type reference if not single-window.
|
||||
|
||||
## Step 3: Scaffold Project
|
||||
|
||||
Use XcodeGen (recommended):
|
||||
|
||||
```bash
|
||||
# Create project structure
|
||||
mkdir -p AppName/Sources
|
||||
cd AppName
|
||||
|
||||
# Create project.yml (see references/project-scaffolding.md for template)
|
||||
# Create Swift files
|
||||
# Generate xcodeproj
|
||||
xcodegen generate
|
||||
```
|
||||
|
||||
## Step 4: Implement with TDD
|
||||
|
||||
Follow test-driven development:
|
||||
1. Write failing test
|
||||
2. Run → RED
|
||||
3. Implement minimal code
|
||||
4. Run → GREEN
|
||||
5. Refactor
|
||||
6. Repeat
|
||||
|
||||
See references/testing-tdd.md for patterns.
|
||||
|
||||
## Step 5: Build and Verify
|
||||
|
||||
```bash
|
||||
# Build
|
||||
xcodebuild -project AppName.xcodeproj -scheme AppName build 2>&1 | xcsift
|
||||
|
||||
# Run
|
||||
open ./build/Build/Products/Debug/AppName.app
|
||||
```
|
||||
|
||||
## Step 6: Polish
|
||||
|
||||
Read references/macos-polish.md for:
|
||||
- Keyboard shortcuts
|
||||
- Menu bar integration
|
||||
- Accessibility
|
||||
- State restoration
|
||||
</process>
|
||||
|
||||
<anti_patterns>
|
||||
Avoid:
|
||||
- Massive view models - views ARE the view model in SwiftUI
|
||||
- Fighting SwiftUI - use declarative patterns
|
||||
- Ignoring platform conventions - standard shortcuts, menus, windows
|
||||
- Blocking main thread - async/await for all I/O
|
||||
- Hard-coded paths - use FileManager APIs
|
||||
- Retain cycles - use `[weak self]` in escaping closures
|
||||
</anti_patterns>
|
||||
|
||||
<success_criteria>
|
||||
A well-built macOS app:
|
||||
- Follows macOS conventions (menu bar, shortcuts, window behavior)
|
||||
- Uses SwiftUI for UI with AppKit integration where needed
|
||||
- Manages state with @Observable and environment
|
||||
- Persists data appropriately
|
||||
- Handles errors gracefully
|
||||
- Supports accessibility
|
||||
- Builds and runs from CLI without opening Xcode
|
||||
- Feels native and responsive
|
||||
</success_criteria>
|
||||
198
skills/expertise/macos-apps/workflows/debug-app.md
Normal file
198
skills/expertise/macos-apps/workflows/debug-app.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Workflow: Debug an Existing macOS App
|
||||
|
||||
<required_reading>
|
||||
**Read these reference files NOW:**
|
||||
1. references/cli-observability.md
|
||||
2. references/testing-debugging.md
|
||||
</required_reading>
|
||||
|
||||
<philosophy>
|
||||
Debugging is iterative. Use whatever gets you to the root cause fastest:
|
||||
- Small app, obvious symptom → read relevant code
|
||||
- Large codebase, unclear cause → use tools to narrow down
|
||||
- Code looks correct but fails → tools reveal runtime behavior
|
||||
- After fixing → tools verify the fix
|
||||
|
||||
The goal is root cause, not following a ritual.
|
||||
</philosophy>
|
||||
|
||||
<process>
|
||||
## Step 1: Understand the Symptom
|
||||
|
||||
Ask the user or observe:
|
||||
- What's the actual behavior vs expected?
|
||||
- When does it happen? (startup, after action, under load)
|
||||
- Is it reproducible?
|
||||
- Any error messages?
|
||||
|
||||
## Step 2: Build and Check for Compile Errors
|
||||
|
||||
```bash
|
||||
cd /path/to/app
|
||||
xcodebuild -project AppName.xcodeproj -scheme AppName -derivedDataPath ./build build 2>&1 | xcsift
|
||||
```
|
||||
|
||||
Fix any compile errors first. They're the easiest wins.
|
||||
|
||||
## Step 3: Choose Your Approach
|
||||
|
||||
**If you know roughly where the problem is:**
|
||||
→ Read that code, form hypothesis, test it
|
||||
|
||||
**If you have no idea where to start:**
|
||||
→ Use tools to narrow down (Step 4)
|
||||
|
||||
**If code looks correct but behavior is wrong:**
|
||||
→ Runtime observation (Step 4) reveals what's actually happening
|
||||
|
||||
## Step 4: Runtime Diagnostics
|
||||
|
||||
Launch with log streaming:
|
||||
```bash
|
||||
# Terminal 1: stream logs
|
||||
log stream --level debug --predicate 'subsystem == "com.company.AppName"'
|
||||
|
||||
# Terminal 2: launch
|
||||
open ./build/Build/Products/Debug/AppName.app
|
||||
```
|
||||
|
||||
**Match symptom to tool:**
|
||||
|
||||
| Symptom | Tool | Command |
|
||||
|---------|------|---------|
|
||||
| Memory growing / leak suspected | leaks | `leaks AppName` |
|
||||
| UI freezes / hangs | spindump | `spindump AppName -o /tmp/hang.txt` |
|
||||
| Crash | crash report | `cat ~/Library/Logs/DiagnosticReports/AppName_*.ips` |
|
||||
| Slow performance | time profiler | `xcrun xctrace record --template 'Time Profiler' --attach AppName` |
|
||||
| Race condition suspected | thread sanitizer | Build with `-enableThreadSanitizer YES` |
|
||||
| Nothing happens / silent failure | logs | Check log stream output |
|
||||
|
||||
**Interact with the app** to trigger the issue. Use `cliclick` if available:
|
||||
```bash
|
||||
cliclick c:500,300 # click at coordinates
|
||||
```
|
||||
|
||||
## Step 5: Interpret Tool Output
|
||||
|
||||
| Tool Shows | Likely Cause | Where to Look |
|
||||
|------------|--------------|---------------|
|
||||
| Leaked object: DataService | Retain cycle | Closures capturing self in DataService |
|
||||
| Main thread blocked in computeX | Sync work on main | That function - needs async |
|
||||
| Crash at force unwrap | Nil where unexpected | The unwrap site + data flow to it |
|
||||
| Thread sanitizer warning | Data race | Shared mutable state without sync |
|
||||
| High CPU in function X | Hot path | That function - algorithm or loop issue |
|
||||
|
||||
## Step 6: Read Relevant Code
|
||||
|
||||
Now you know where to look. Read that specific code:
|
||||
- Understand what it's trying to do
|
||||
- Identify the flaw
|
||||
- Consider edge cases
|
||||
|
||||
## Step 7: Fix the Root Cause
|
||||
|
||||
Not the symptom. The actual cause.
|
||||
|
||||
**Bad:** Add nil check to prevent crash
|
||||
**Good:** Fix why the value is nil in the first place
|
||||
|
||||
**Bad:** Add try/catch to swallow error
|
||||
**Good:** Fix what's causing the error
|
||||
|
||||
## Step 8: Verify the Fix
|
||||
|
||||
Use the same diagnostic that found the issue:
|
||||
```bash
|
||||
# Rebuild
|
||||
xcodebuild -project AppName.xcodeproj -scheme AppName build
|
||||
|
||||
# Launch and test
|
||||
open ./build/Build/Products/Debug/AppName.app
|
||||
|
||||
# Run same diagnostic
|
||||
leaks AppName # should show 0 leaks now
|
||||
```
|
||||
|
||||
## Step 9: Prevent Regression
|
||||
|
||||
If the bug was significant, write a test:
|
||||
```bash
|
||||
xcodebuild test -project AppName.xcodeproj -scheme AppName
|
||||
```
|
||||
</process>
|
||||
|
||||
<common_patterns>
|
||||
## Memory Leaks
|
||||
**Symptom:** Memory grows over time, `leaks` shows retained objects
|
||||
**Common causes:**
|
||||
- Closure captures `self` strongly: `{ self.doThing() }`
|
||||
- Delegate not weak: `var delegate: SomeProtocol`
|
||||
- Timer not invalidated
|
||||
**Fix:** `[weak self]`, `weak var delegate`, `timer.invalidate()`
|
||||
|
||||
## UI Freezes
|
||||
**Symptom:** App hangs, spinning beachball, spindump shows main thread blocked
|
||||
**Common causes:**
|
||||
- Sync network call on main thread
|
||||
- Heavy computation on main thread
|
||||
- Deadlock from incorrect async/await usage
|
||||
**Fix:** `Task { }`, `Task.detached { }`, check actor isolation
|
||||
|
||||
## Crashes
|
||||
**Symptom:** App terminates, crash report generated
|
||||
**Common causes:**
|
||||
- Force unwrap of nil: `value!`
|
||||
- Array index out of bounds
|
||||
- Unhandled error
|
||||
**Fix:** `guard let`, bounds checking, proper error handling
|
||||
|
||||
## Silent Failures
|
||||
**Symptom:** Nothing happens, no error, no crash
|
||||
**Common causes:**
|
||||
- Error silently caught and ignored
|
||||
- Async task never awaited
|
||||
- Condition always false
|
||||
**Fix:** Add logging, check control flow, verify async chains
|
||||
|
||||
## Performance Issues
|
||||
**Symptom:** Slow, high CPU, laggy UI
|
||||
**Common causes:**
|
||||
- O(n²) or worse algorithm
|
||||
- Unnecessary re-renders in SwiftUI
|
||||
- Repeated expensive operations
|
||||
**Fix:** Better algorithm, memoization, `let _ = Self._printChanges()`
|
||||
</common_patterns>
|
||||
|
||||
<tools_quick_reference>
|
||||
```bash
|
||||
# Build errors (structured JSON)
|
||||
xcodebuild build 2>&1 | xcsift
|
||||
|
||||
# Real-time logs
|
||||
log stream --level debug --predicate 'subsystem == "com.company.App"'
|
||||
|
||||
# Memory leaks
|
||||
leaks AppName
|
||||
|
||||
# UI hangs
|
||||
spindump AppName -o /tmp/hang.txt
|
||||
|
||||
# Crash reports
|
||||
cat ~/Library/Logs/DiagnosticReports/AppName_*.ips | head -100
|
||||
|
||||
# Memory regions
|
||||
vmmap --summary AppName
|
||||
|
||||
# Heap analysis
|
||||
heap AppName
|
||||
|
||||
# Attach debugger
|
||||
lldb -n AppName
|
||||
|
||||
# CPU profiling
|
||||
xcrun xctrace record --template 'Time Profiler' --attach AppName
|
||||
|
||||
# Thread issues (build flag)
|
||||
xcodebuild build -enableThreadSanitizer YES
|
||||
```
|
||||
</tools_quick_reference>
|
||||
244
skills/expertise/macos-apps/workflows/optimize-performance.md
Normal file
244
skills/expertise/macos-apps/workflows/optimize-performance.md
Normal file
@@ -0,0 +1,244 @@
|
||||
# Workflow: Optimize App Performance
|
||||
|
||||
<required_reading>
|
||||
**Read these reference files NOW:**
|
||||
1. references/cli-observability.md
|
||||
2. references/concurrency-patterns.md
|
||||
3. references/swiftui-patterns.md
|
||||
</required_reading>
|
||||
|
||||
<philosophy>
|
||||
Measure first, optimize second. Never optimize based on assumptions.
|
||||
Profile → Identify bottleneck → Fix → Measure again → Repeat
|
||||
</philosophy>
|
||||
|
||||
<process>
|
||||
## Step 1: Define the Problem
|
||||
|
||||
Ask the user:
|
||||
- What feels slow? (startup, specific action, scrolling, etc.)
|
||||
- How slow? (seconds, milliseconds, "laggy")
|
||||
- When did it start? (always, after recent change, with more data)
|
||||
|
||||
## Step 2: Measure Current Performance
|
||||
|
||||
**CPU Profiling:**
|
||||
```bash
|
||||
# Record 30 seconds of activity
|
||||
xcrun xctrace record \
|
||||
--template 'Time Profiler' \
|
||||
--time-limit 30s \
|
||||
--output profile.trace \
|
||||
--launch -- ./build/Build/Products/Debug/AppName.app/Contents/MacOS/AppName
|
||||
```
|
||||
|
||||
**Memory:**
|
||||
```bash
|
||||
# While app is running
|
||||
vmmap --summary AppName
|
||||
heap AppName
|
||||
leaks AppName
|
||||
```
|
||||
|
||||
**Startup time:**
|
||||
```bash
|
||||
# Measure launch to first frame
|
||||
time open -W ./build/Build/Products/Debug/AppName.app
|
||||
```
|
||||
|
||||
## Step 3: Identify Bottlenecks
|
||||
|
||||
**From Time Profiler:**
|
||||
- Look for functions with high "self time"
|
||||
- Check main thread for blocking operations
|
||||
- Look for repeated calls that could be cached
|
||||
|
||||
**From memory tools:**
|
||||
- Large allocations that could be lazy-loaded
|
||||
- Objects retained longer than needed
|
||||
- Duplicate data in memory
|
||||
|
||||
**SwiftUI re-renders:**
|
||||
```swift
|
||||
// Add to any view to see why it re-renders
|
||||
var body: some View {
|
||||
let _ = Self._printChanges()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Common Optimizations
|
||||
|
||||
### Main Thread
|
||||
|
||||
**Problem:** Heavy work on main thread
|
||||
```swift
|
||||
// Bad
|
||||
func loadData() {
|
||||
let data = expensiveComputation() // blocks UI
|
||||
self.items = data
|
||||
}
|
||||
|
||||
// Good
|
||||
func loadData() async {
|
||||
let data = await Task.detached {
|
||||
expensiveComputation()
|
||||
}.value
|
||||
await MainActor.run {
|
||||
self.items = data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SwiftUI
|
||||
|
||||
**Problem:** Unnecessary re-renders
|
||||
```swift
|
||||
// Bad - entire view rebuilds when any state changes
|
||||
struct ListView: View {
|
||||
@State var items: [Item]
|
||||
@State var searchText: String
|
||||
// ...
|
||||
}
|
||||
|
||||
// Good - extract subviews with their own state
|
||||
struct ListView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
SearchBar() // has its own @State
|
||||
ItemList() // only rebuilds when items change
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** Expensive computation in body
|
||||
```swift
|
||||
// Bad
|
||||
var body: some View {
|
||||
List(items.sorted().filtered()) // runs every render
|
||||
|
||||
// Good
|
||||
var sortedItems: [Item] { // or use .task modifier
|
||||
items.sorted().filtered()
|
||||
}
|
||||
var body: some View {
|
||||
List(sortedItems)
|
||||
}
|
||||
```
|
||||
|
||||
### Data Loading
|
||||
|
||||
**Problem:** Loading all data upfront
|
||||
```swift
|
||||
// Bad
|
||||
init() {
|
||||
self.allItems = loadEverything() // slow startup
|
||||
}
|
||||
|
||||
// Good - lazy loading
|
||||
func loadItemsIfNeeded() async {
|
||||
guard items.isEmpty else { return }
|
||||
items = await loadItems()
|
||||
}
|
||||
```
|
||||
|
||||
**Problem:** No caching
|
||||
```swift
|
||||
// Bad
|
||||
func getImage(for url: URL) async -> NSImage {
|
||||
return await downloadImage(url) // downloads every time
|
||||
}
|
||||
|
||||
// Good
|
||||
private var imageCache: [URL: NSImage] = [:]
|
||||
func getImage(for url: URL) async -> NSImage {
|
||||
if let cached = imageCache[url] { return cached }
|
||||
let image = await downloadImage(url)
|
||||
imageCache[url] = image
|
||||
return image
|
||||
}
|
||||
```
|
||||
|
||||
### Collections
|
||||
|
||||
**Problem:** O(n²) operations
|
||||
```swift
|
||||
// Bad - O(n) lookup in array
|
||||
items.first { $0.id == targetId }
|
||||
|
||||
// Good - O(1) lookup with dictionary
|
||||
itemsById[targetId]
|
||||
```
|
||||
|
||||
**Problem:** Repeated filtering
|
||||
```swift
|
||||
// Bad
|
||||
let activeItems = items.filter { $0.isActive } // called repeatedly
|
||||
|
||||
// Good - compute once, update when needed
|
||||
@Published var activeItems: [Item] = []
|
||||
func updateActiveItems() {
|
||||
activeItems = items.filter { $0.isActive }
|
||||
}
|
||||
```
|
||||
|
||||
## Step 5: Measure Again
|
||||
|
||||
After each optimization:
|
||||
```bash
|
||||
# Re-run profiler
|
||||
xcrun xctrace record --template 'Time Profiler' ...
|
||||
|
||||
# Compare metrics
|
||||
```
|
||||
|
||||
Did it actually improve? If not, revert and try different approach.
|
||||
|
||||
## Step 6: Prevent Regression
|
||||
|
||||
Add performance tests:
|
||||
```swift
|
||||
func testStartupPerformance() {
|
||||
measure {
|
||||
// startup code
|
||||
}
|
||||
}
|
||||
|
||||
func testScrollingPerformance() {
|
||||
measure(metrics: [XCTCPUMetric(), XCTMemoryMetric()]) {
|
||||
// scroll simulation
|
||||
}
|
||||
}
|
||||
```
|
||||
</process>
|
||||
|
||||
<performance_targets>
|
||||
| Metric | Target | Unacceptable |
|
||||
|--------|--------|--------------|
|
||||
| App launch | < 1 second | > 3 seconds |
|
||||
| Button response | < 100ms | > 500ms |
|
||||
| List scrolling | 60 fps | < 30 fps |
|
||||
| Memory (idle) | < 100MB | > 500MB |
|
||||
| Memory growth | Stable | Unbounded |
|
||||
</performance_targets>
|
||||
|
||||
<tools_reference>
|
||||
```bash
|
||||
# CPU profiling
|
||||
xcrun xctrace record --template 'Time Profiler' --attach AppName
|
||||
|
||||
# Memory snapshot
|
||||
vmmap --summary AppName
|
||||
heap AppName
|
||||
|
||||
# Allocations over time
|
||||
xcrun xctrace record --template 'Allocations' --attach AppName
|
||||
|
||||
# Energy impact
|
||||
xcrun xctrace record --template 'Energy Log' --attach AppName
|
||||
|
||||
# System trace (comprehensive)
|
||||
xcrun xctrace record --template 'System Trace' --attach AppName
|
||||
```
|
||||
</tools_reference>
|
||||
159
skills/expertise/macos-apps/workflows/ship-app.md
Normal file
159
skills/expertise/macos-apps/workflows/ship-app.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# Workflow: Ship/Release a macOS App
|
||||
|
||||
<required_reading>
|
||||
**Read these reference files NOW:**
|
||||
1. references/security-code-signing.md
|
||||
2. references/cli-workflow.md
|
||||
</required_reading>
|
||||
|
||||
<process>
|
||||
## Step 1: Prepare for Release
|
||||
|
||||
Ensure the app is ready:
|
||||
- All features complete and tested
|
||||
- No debug code or test data
|
||||
- Version and build numbers updated in Info.plist
|
||||
- App icon and assets finalized
|
||||
|
||||
```bash
|
||||
# Verify build succeeds
|
||||
xcodebuild -project AppName.xcodeproj -scheme AppName -configuration Release build
|
||||
```
|
||||
|
||||
## Step 2: Choose Distribution Method
|
||||
|
||||
| Method | Use When | Requires |
|
||||
|--------|----------|----------|
|
||||
| Direct distribution | Sharing with specific users, beta testing | Developer ID signing + notarization |
|
||||
| App Store | Public distribution, paid apps | App Store Connect account, review |
|
||||
| TestFlight | Beta testing at scale | App Store Connect |
|
||||
|
||||
## Step 3: Code Signing
|
||||
|
||||
**For Direct Distribution (Developer ID):**
|
||||
```bash
|
||||
# Archive
|
||||
xcodebuild -project AppName.xcodeproj \
|
||||
-scheme AppName \
|
||||
-configuration Release \
|
||||
-archivePath ./build/AppName.xcarchive \
|
||||
archive
|
||||
|
||||
# Export with Developer ID
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath ./build/AppName.xcarchive \
|
||||
-exportPath ./build/export \
|
||||
-exportOptionsPlist ExportOptions.plist
|
||||
```
|
||||
|
||||
ExportOptions.plist for Developer ID:
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>developer-id</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
**For App Store:**
|
||||
```xml
|
||||
<key>method</key>
|
||||
<string>app-store</string>
|
||||
```
|
||||
|
||||
## Step 4: Notarization (Direct Distribution)
|
||||
|
||||
Required for apps distributed outside the App Store:
|
||||
|
||||
```bash
|
||||
# Submit for notarization
|
||||
xcrun notarytool submit ./build/export/AppName.app.zip \
|
||||
--apple-id "your@email.com" \
|
||||
--team-id "TEAMID" \
|
||||
--password "@keychain:AC_PASSWORD" \
|
||||
--wait
|
||||
|
||||
# Staple the ticket
|
||||
xcrun stapler staple ./build/export/AppName.app
|
||||
```
|
||||
|
||||
## Step 5: Create DMG (Direct Distribution)
|
||||
|
||||
```bash
|
||||
# Create DMG
|
||||
hdiutil create -volname "AppName" \
|
||||
-srcfolder ./build/export/AppName.app \
|
||||
-ov -format UDZO \
|
||||
./build/AppName.dmg
|
||||
|
||||
# Notarize the DMG too
|
||||
xcrun notarytool submit ./build/AppName.dmg \
|
||||
--apple-id "your@email.com" \
|
||||
--team-id "TEAMID" \
|
||||
--password "@keychain:AC_PASSWORD" \
|
||||
--wait
|
||||
|
||||
xcrun stapler staple ./build/AppName.dmg
|
||||
```
|
||||
|
||||
## Step 6: App Store Submission
|
||||
|
||||
```bash
|
||||
# Validate
|
||||
xcrun altool --validate-app \
|
||||
-f ./build/export/AppName.pkg \
|
||||
-t macos \
|
||||
--apiKey KEY_ID \
|
||||
--apiIssuer ISSUER_ID
|
||||
|
||||
# Upload
|
||||
xcrun altool --upload-app \
|
||||
-f ./build/export/AppName.pkg \
|
||||
-t macos \
|
||||
--apiKey KEY_ID \
|
||||
--apiIssuer ISSUER_ID
|
||||
```
|
||||
|
||||
Then complete submission in App Store Connect.
|
||||
|
||||
## Step 7: Verify Release
|
||||
|
||||
**For direct distribution:**
|
||||
```bash
|
||||
# Verify signature
|
||||
codesign -dv --verbose=4 ./build/export/AppName.app
|
||||
|
||||
# Verify notarization
|
||||
spctl -a -vv ./build/export/AppName.app
|
||||
```
|
||||
|
||||
**For App Store:**
|
||||
- Check App Store Connect for review status
|
||||
- Test TestFlight build if applicable
|
||||
</process>
|
||||
|
||||
<checklist>
|
||||
Before shipping:
|
||||
- [ ] Version number incremented
|
||||
- [ ] Release notes written
|
||||
- [ ] Debug logging disabled or minimized
|
||||
- [ ] All entitlements correct and minimal
|
||||
- [ ] Privacy descriptions in Info.plist
|
||||
- [ ] App icon complete (all sizes)
|
||||
- [ ] Screenshots prepared (if App Store)
|
||||
- [ ] Tested on clean install
|
||||
</checklist>
|
||||
|
||||
<common_issues>
|
||||
| Issue | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| Notarization fails | Unsigned frameworks, hardened runtime issues | Check all embedded binaries are signed |
|
||||
| "App is damaged" | Not notarized or stapled | Run notarytool and stapler |
|
||||
| Gatekeeper blocks | Missing Developer ID | Sign with Developer ID certificate |
|
||||
| App Store rejection | Missing entitlement descriptions, privacy issues | Add usage descriptions to Info.plist |
|
||||
</common_issues>
|
||||
258
skills/expertise/macos-apps/workflows/write-tests.md
Normal file
258
skills/expertise/macos-apps/workflows/write-tests.md
Normal file
@@ -0,0 +1,258 @@
|
||||
# Workflow: Write and Run Tests
|
||||
|
||||
<required_reading>
|
||||
**Read these reference files NOW:**
|
||||
1. references/testing-tdd.md
|
||||
2. references/testing-debugging.md
|
||||
</required_reading>
|
||||
|
||||
<philosophy>
|
||||
Tests are documentation that runs. Write tests that:
|
||||
- Describe what the code should do
|
||||
- Catch regressions before users do
|
||||
- Enable confident refactoring
|
||||
</philosophy>
|
||||
|
||||
<process>
|
||||
## Step 1: Understand What to Test
|
||||
|
||||
Ask the user:
|
||||
- New tests for existing code?
|
||||
- Tests for new feature (TDD)?
|
||||
- Fix a bug with regression test?
|
||||
|
||||
**What Claude tests (automated):**
|
||||
- Core logic (data transforms, calculations, algorithms)
|
||||
- State management (models, relationships)
|
||||
- Service layer (mocked dependencies)
|
||||
- Edge cases (nil, empty, boundaries)
|
||||
|
||||
**What user tests (manual):**
|
||||
- UX feel and visual polish
|
||||
- Real hardware/device integration
|
||||
- Performance under real conditions
|
||||
|
||||
## Step 2: Set Up Test Target
|
||||
|
||||
If tests don't exist yet:
|
||||
```bash
|
||||
# Add test target to project.yml (XcodeGen)
|
||||
targets:
|
||||
AppNameTests:
|
||||
type: bundle.unit-test
|
||||
platform: macOS
|
||||
sources:
|
||||
- path: Tests
|
||||
dependencies:
|
||||
- target: AppName
|
||||
```
|
||||
|
||||
Or create test files manually in Xcode's test target.
|
||||
|
||||
## Step 3: Write Tests
|
||||
|
||||
### Unit Tests (Logic)
|
||||
|
||||
```swift
|
||||
import Testing
|
||||
@testable import AppName
|
||||
|
||||
struct ItemTests {
|
||||
@Test func itemCreation() {
|
||||
let item = Item(name: "Test", value: 42)
|
||||
#expect(item.name == "Test")
|
||||
#expect(item.value == 42)
|
||||
}
|
||||
|
||||
@Test func itemValidation() {
|
||||
let emptyItem = Item(name: "", value: 0)
|
||||
#expect(!emptyItem.isValid)
|
||||
}
|
||||
|
||||
@Test(arguments: [0, -1, 1000001])
|
||||
func invalidValues(value: Int) {
|
||||
let item = Item(name: "Test", value: value)
|
||||
#expect(!item.isValid)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Tests
|
||||
|
||||
```swift
|
||||
struct AppStateTests {
|
||||
@Test func addItem() {
|
||||
let state = AppState()
|
||||
let item = Item(name: "New", value: 10)
|
||||
|
||||
state.addItem(item)
|
||||
|
||||
#expect(state.items.count == 1)
|
||||
#expect(state.items.first?.name == "New")
|
||||
}
|
||||
|
||||
@Test func deleteItem() {
|
||||
let state = AppState()
|
||||
let item = Item(name: "ToDelete", value: 1)
|
||||
state.addItem(item)
|
||||
|
||||
state.deleteItem(item)
|
||||
|
||||
#expect(state.items.isEmpty)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Async Tests
|
||||
|
||||
```swift
|
||||
struct NetworkTests {
|
||||
@Test func fetchItems() async throws {
|
||||
let service = MockDataService()
|
||||
service.mockItems = [Item(name: "Fetched", value: 5)]
|
||||
|
||||
let items = try await service.fetchItems()
|
||||
|
||||
#expect(items.count == 1)
|
||||
}
|
||||
|
||||
@Test func fetchHandlesError() async {
|
||||
let service = MockDataService()
|
||||
service.shouldFail = true
|
||||
|
||||
await #expect(throws: NetworkError.self) {
|
||||
try await service.fetchItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Edge Cases
|
||||
|
||||
```swift
|
||||
struct EdgeCaseTests {
|
||||
@Test func emptyList() {
|
||||
let state = AppState()
|
||||
#expect(state.items.isEmpty)
|
||||
#expect(state.selectedItem == nil)
|
||||
}
|
||||
|
||||
@Test func nilHandling() {
|
||||
let item: Item? = nil
|
||||
#expect(item?.name == nil)
|
||||
}
|
||||
|
||||
@Test func boundaryValues() {
|
||||
let item = Item(name: String(repeating: "a", count: 10000), value: Int.max)
|
||||
#expect(item.isValid) // or test truncation behavior
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Step 4: Run Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
xcodebuild test \
|
||||
-project AppName.xcodeproj \
|
||||
-scheme AppName \
|
||||
-resultBundlePath TestResults.xcresult
|
||||
|
||||
# Run specific test
|
||||
xcodebuild test \
|
||||
-project AppName.xcodeproj \
|
||||
-scheme AppName \
|
||||
-only-testing:AppNameTests/ItemTests/testItemCreation
|
||||
|
||||
# View results
|
||||
xcrun xcresulttool get test-results summary --path TestResults.xcresult
|
||||
```
|
||||
|
||||
## Step 5: Coverage Report
|
||||
|
||||
```bash
|
||||
# Generate coverage
|
||||
xcodebuild test \
|
||||
-project AppName.xcodeproj \
|
||||
-scheme AppName \
|
||||
-enableCodeCoverage YES \
|
||||
-resultBundlePath TestResults.xcresult
|
||||
|
||||
# View coverage
|
||||
xcrun xccov view --report TestResults.xcresult
|
||||
|
||||
# Coverage as JSON
|
||||
xcrun xccov view --report --json TestResults.xcresult > coverage.json
|
||||
```
|
||||
|
||||
## Step 6: TDD Cycle
|
||||
|
||||
For new features:
|
||||
1. **Red:** Write failing test for desired behavior
|
||||
2. **Green:** Write minimum code to pass
|
||||
3. **Refactor:** Clean up while keeping tests green
|
||||
4. **Repeat:** Next behavior
|
||||
</process>
|
||||
|
||||
<test_patterns>
|
||||
### Arrange-Act-Assert
|
||||
```swift
|
||||
@Test func pattern() {
|
||||
// Arrange
|
||||
let state = AppState()
|
||||
let item = Item(name: "Test", value: 1)
|
||||
|
||||
// Act
|
||||
state.addItem(item)
|
||||
|
||||
// Assert
|
||||
#expect(state.items.contains(item))
|
||||
}
|
||||
```
|
||||
|
||||
### Mocking Dependencies
|
||||
```swift
|
||||
protocol DataServiceProtocol {
|
||||
func fetchItems() async throws -> [Item]
|
||||
}
|
||||
|
||||
class MockDataService: DataServiceProtocol {
|
||||
var mockItems: [Item] = []
|
||||
var shouldFail = false
|
||||
|
||||
func fetchItems() async throws -> [Item] {
|
||||
if shouldFail { throw TestError.mock }
|
||||
return mockItems
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing SwiftUI State
|
||||
```swift
|
||||
@Test func viewModelState() {
|
||||
let state = AppState()
|
||||
state.items = [Item(name: "A", value: 1), Item(name: "B", value: 2)]
|
||||
|
||||
state.selectedItem = state.items.first
|
||||
|
||||
#expect(state.selectedItem?.name == "A")
|
||||
}
|
||||
```
|
||||
</test_patterns>
|
||||
|
||||
<what_not_to_test>
|
||||
- SwiftUI view rendering (use previews + manual testing)
|
||||
- Apple framework behavior
|
||||
- Simple getters/setters with no logic
|
||||
- Private implementation details (test via public interface)
|
||||
</what_not_to_test>
|
||||
|
||||
<coverage_targets>
|
||||
| Code Type | Target Coverage |
|
||||
|-----------|-----------------|
|
||||
| Business logic | 80-100% |
|
||||
| State management | 70-90% |
|
||||
| Utilities/helpers | 60-80% |
|
||||
| Views | 0% (manual test) |
|
||||
| Generated code | 0% |
|
||||
</coverage_targets>
|
||||
Reference in New Issue
Block a user