mirror of
https://github.com/harivansh-afk/claude-code-vertical.git
synced 2026-04-15 06:04:40 +00:00
skills
This commit is contained in:
parent
087b195b4c
commit
ae037f7bec
24 changed files with 15692 additions and 0 deletions
282
skill-index/index.yaml
Normal file
282
skill-index/index.yaml
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
# Skill Index
|
||||
# Orchestrator uses this to match skill_hints from specs to actual skills
|
||||
#
|
||||
# Format:
|
||||
# id: unique identifier (matches directory name)
|
||||
# path: relative path to SKILL.md
|
||||
# description: what the skill does
|
||||
# triggers: keywords that activate this skill
|
||||
# domains: broad categories
|
||||
|
||||
skills:
|
||||
# === iOS/Swift Development ===
|
||||
|
||||
- id: axiom-swift-concurrency
|
||||
path: skill-index/skills/axiom-swift-concurrency/SKILL.md
|
||||
description: "Swift 6 strict concurrency patterns with actor isolation and async/await"
|
||||
triggers:
|
||||
- actor-isolated
|
||||
- Sendable
|
||||
- data race
|
||||
- MainActor
|
||||
- async/await
|
||||
- swift concurrency
|
||||
- thread safe
|
||||
domains:
|
||||
- ios
|
||||
- swift
|
||||
- concurrency
|
||||
|
||||
- id: axiom-swiftui-performance
|
||||
path: skill-index/skills/axiom-swiftui-performance/SKILL.md
|
||||
description: "SwiftUI performance optimization, view identity, and rendering efficiency"
|
||||
triggers:
|
||||
- swiftui slow
|
||||
- view redraws
|
||||
- swiftui performance
|
||||
- body called
|
||||
- swiftui optimization
|
||||
domains:
|
||||
- ios
|
||||
- swiftui
|
||||
- performance
|
||||
|
||||
- id: axiom-swiftui-debugging
|
||||
path: skill-index/skills/axiom-swiftui-debugging/SKILL.md
|
||||
description: "SwiftUI debugging techniques and common issue resolution"
|
||||
triggers:
|
||||
- swiftui not updating
|
||||
- view not refreshing
|
||||
- swiftui bug
|
||||
- swiftui debug
|
||||
domains:
|
||||
- ios
|
||||
- swiftui
|
||||
- debugging
|
||||
|
||||
- id: axiom-swiftui-architecture
|
||||
path: skill-index/skills/axiom-swiftui-architecture/SKILL.md
|
||||
description: "SwiftUI app architecture patterns and best practices"
|
||||
triggers:
|
||||
- swiftui architecture
|
||||
- swiftui patterns
|
||||
- view model
|
||||
- observable
|
||||
domains:
|
||||
- ios
|
||||
- swiftui
|
||||
- architecture
|
||||
|
||||
- id: axiom-swift-performance
|
||||
path: skill-index/skills/axiom-swift-performance/SKILL.md
|
||||
description: "Swift performance optimization and profiling"
|
||||
triggers:
|
||||
- swift slow
|
||||
- swift performance
|
||||
- optimize swift
|
||||
- profiling
|
||||
domains:
|
||||
- ios
|
||||
- swift
|
||||
- performance
|
||||
|
||||
- id: axiom-swift-testing
|
||||
path: skill-index/skills/axiom-swift-testing/SKILL.md
|
||||
description: "Swift Testing framework patterns and best practices"
|
||||
triggers:
|
||||
- swift testing
|
||||
- unit test
|
||||
- xctest
|
||||
- test swift
|
||||
domains:
|
||||
- ios
|
||||
- swift
|
||||
- testing
|
||||
|
||||
- id: axiom-xcode-debugging
|
||||
path: skill-index/skills/axiom-xcode-debugging/SKILL.md
|
||||
description: "Xcode debugging, build issues, and environment troubleshooting"
|
||||
triggers:
|
||||
- xcode error
|
||||
- build failed
|
||||
- xcode debug
|
||||
- simulator
|
||||
- provisioning
|
||||
domains:
|
||||
- ios
|
||||
- xcode
|
||||
- debugging
|
||||
|
||||
- id: axiom-build-debugging
|
||||
path: skill-index/skills/axiom-build-debugging/SKILL.md
|
||||
description: "Build system debugging, SPM conflicts, dependency issues"
|
||||
triggers:
|
||||
- build error
|
||||
- spm conflict
|
||||
- dependency
|
||||
- linker error
|
||||
- module not found
|
||||
domains:
|
||||
- ios
|
||||
- build
|
||||
- debugging
|
||||
|
||||
- id: axiom-memory-debugging
|
||||
path: skill-index/skills/axiom-memory-debugging/SKILL.md
|
||||
description: "Memory leak detection, retain cycles, and memory optimization"
|
||||
triggers:
|
||||
- memory leak
|
||||
- retain cycle
|
||||
- memory warning
|
||||
- heap
|
||||
- instruments memory
|
||||
domains:
|
||||
- ios
|
||||
- memory
|
||||
- debugging
|
||||
|
||||
- id: axiom-networking
|
||||
path: skill-index/skills/axiom-networking/SKILL.md
|
||||
description: "Network.framework patterns and URLSession best practices"
|
||||
triggers:
|
||||
- networking
|
||||
- urlsession
|
||||
- api call
|
||||
- http request
|
||||
- network error
|
||||
domains:
|
||||
- ios
|
||||
- networking
|
||||
|
||||
- id: axiom-swiftdata
|
||||
path: skill-index/skills/axiom-swiftdata/SKILL.md
|
||||
description: "SwiftData persistence patterns and best practices"
|
||||
triggers:
|
||||
- swiftdata
|
||||
- modelcontainer
|
||||
- modelcontext
|
||||
- persistence
|
||||
domains:
|
||||
- ios
|
||||
- data
|
||||
- persistence
|
||||
|
||||
- id: axiom-database-migration
|
||||
path: skill-index/skills/axiom-database-migration/SKILL.md
|
||||
description: "Safe database schema migrations for SwiftData, Core Data, GRDB"
|
||||
triggers:
|
||||
- migration
|
||||
- schema change
|
||||
- database upgrade
|
||||
- model version
|
||||
domains:
|
||||
- ios
|
||||
- data
|
||||
- migration
|
||||
|
||||
- id: axiom-grdb
|
||||
path: skill-index/skills/axiom-grdb/SKILL.md
|
||||
description: "GRDB SQLite patterns and best practices"
|
||||
triggers:
|
||||
- grdb
|
||||
- sqlite
|
||||
- database query
|
||||
- sql
|
||||
domains:
|
||||
- ios
|
||||
- data
|
||||
- sqlite
|
||||
|
||||
- id: axiom-ui-testing
|
||||
path: skill-index/skills/axiom-ui-testing/SKILL.md
|
||||
description: "UI testing with XCUITest and accessibility identifiers"
|
||||
triggers:
|
||||
- ui test
|
||||
- xcuitest
|
||||
- automation
|
||||
- accessibility identifier
|
||||
- ui recording
|
||||
domains:
|
||||
- ios
|
||||
- testing
|
||||
- ui
|
||||
|
||||
# === General Development ===
|
||||
|
||||
- id: coding-agent
|
||||
path: skill-index/skills/coding-agent/SKILL.md
|
||||
description: "Run Claude Code, Codex, or other coding agents in background"
|
||||
triggers:
|
||||
- spawn agent
|
||||
- background agent
|
||||
- coding agent
|
||||
- codex
|
||||
domains:
|
||||
- orchestration
|
||||
- agents
|
||||
|
||||
- id: tmux
|
||||
path: skill-index/skills/tmux/SKILL.md
|
||||
description: "Remote-control tmux sessions for interactive CLIs"
|
||||
triggers:
|
||||
- tmux
|
||||
- terminal session
|
||||
- pane
|
||||
- session management
|
||||
domains:
|
||||
- terminal
|
||||
- orchestration
|
||||
|
||||
# === Communication ===
|
||||
|
||||
- id: slack
|
||||
path: skill-index/skills/slack/SKILL.md
|
||||
description: "Slack messaging and workspace integration"
|
||||
triggers:
|
||||
- slack
|
||||
- message slack
|
||||
- slack channel
|
||||
domains:
|
||||
- communication
|
||||
|
||||
# === Utilities ===
|
||||
|
||||
- id: summarize
|
||||
path: skill-index/skills/summarize/SKILL.md
|
||||
description: "Summarize URLs, podcasts, and documents"
|
||||
triggers:
|
||||
- summarize
|
||||
- transcript
|
||||
- tldr
|
||||
domains:
|
||||
- utility
|
||||
|
||||
- id: oracle
|
||||
path: skill-index/skills/oracle/SKILL.md
|
||||
description: "Oracle CLI for prompt bundling and file attachments"
|
||||
triggers:
|
||||
- oracle
|
||||
- prompt bundle
|
||||
domains:
|
||||
- utility
|
||||
|
||||
- id: clawdhub
|
||||
path: skill-index/skills/clawdhub/SKILL.md
|
||||
description: "Search and install agent skills from clawdhub.com"
|
||||
triggers:
|
||||
- clawdhub
|
||||
- install skill
|
||||
- skill registry
|
||||
domains:
|
||||
- skills
|
||||
- registry
|
||||
|
||||
- id: bird
|
||||
path: skill-index/skills/bird/SKILL.md
|
||||
description: "X/Twitter CLI for reading, searching, and posting"
|
||||
triggers:
|
||||
- twitter
|
||||
- tweet
|
||||
- x post
|
||||
domains:
|
||||
- social
|
||||
513
skill-index/skills/axiom-build-debugging/SKILL.md
Normal file
513
skill-index/skills/axiom-build-debugging/SKILL.md
Normal file
|
|
@ -0,0 +1,513 @@
|
|||
---
|
||||
name: axiom-build-debugging
|
||||
description: Use when encountering dependency conflicts, CocoaPods/SPM resolution failures, "Multiple commands produce" errors, or framework version mismatches - systematic dependency and build configuration debugging for iOS projects. Includes pressure scenario guidance for resisting quick fixes under time constraints
|
||||
skill_type: discipline
|
||||
version: 1.1.0
|
||||
last_updated: TDD-tested with production crisis scenarios
|
||||
---
|
||||
|
||||
# Build Debugging
|
||||
|
||||
## Overview
|
||||
|
||||
Check dependencies BEFORE blaming code. **Core principle** 80% of persistent build failures are dependency resolution issues (CocoaPods, SPM, framework conflicts), not code bugs.
|
||||
|
||||
## Example Prompts
|
||||
|
||||
These are real questions developers ask that this skill is designed to answer:
|
||||
|
||||
#### 1. "I added a Swift Package but I'm getting 'No such module' errors. The package is in my Xcode project but won't compile."
|
||||
→ The skill covers SPM resolution workflows, package cache clearing, and framework search path diagnostics
|
||||
|
||||
#### 2. "The build is failing with 'Multiple commands produce' the same output file. How do I figure out which files are duplicated?"
|
||||
→ The skill shows how to identify duplicate target membership and resolve file conflicts in build settings
|
||||
|
||||
#### 3. "CocoaPods installed dependencies successfully but the build still fails. How do I debug CocoaPods issues?"
|
||||
→ The skill covers Podfile.lock conflict resolution, linking errors, and version constraint debugging
|
||||
|
||||
#### 4. "My build works on my Mac but fails on the CI server. Both machines have the latest Xcode. What's different?"
|
||||
→ The skill explains dependency caching differences, environment-specific paths, and reproducible build strategies
|
||||
|
||||
#### 5. "I'm getting framework version conflicts and I don't know which dependency is causing it. How do I resolve this?"
|
||||
→ The skill demonstrates dependency graph analysis and version constraint resolution strategies for complex dependency trees
|
||||
|
||||
---
|
||||
|
||||
## Red Flags — Dependency/Build Issues
|
||||
|
||||
If you see ANY of these, suspect dependency problem:
|
||||
- "No such module" after adding package
|
||||
- "Multiple commands produce" same output file
|
||||
- Build succeeds on one machine, fails on another
|
||||
- CocoaPods install succeeds but build fails
|
||||
- SPM resolution takes forever or times out
|
||||
- Framework version conflicts in error logs
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Build failing?
|
||||
├─ "No such module XYZ"?
|
||||
│ ├─ After adding SPM package?
|
||||
│ │ └─ Clean build folder + reset package caches
|
||||
│ ├─ After pod install?
|
||||
│ │ └─ Check Podfile.lock conflicts
|
||||
│ └─ Framework not found?
|
||||
│ └─ Check FRAMEWORK_SEARCH_PATHS
|
||||
├─ "Multiple commands produce"?
|
||||
│ └─ Duplicate files in target membership
|
||||
├─ SPM resolution hangs?
|
||||
│ └─ Clear package caches + derived data
|
||||
└─ Version conflicts?
|
||||
└─ Use dependency resolution strategies below
|
||||
```
|
||||
|
||||
## Common Build Issues
|
||||
|
||||
### Issue 1: SPM Package Not Found
|
||||
|
||||
**Symptom**: "No such module PackageName" after adding Swift Package
|
||||
|
||||
**❌ WRONG**:
|
||||
```bash
|
||||
# Rebuilding without cleaning
|
||||
xcodebuild build
|
||||
```
|
||||
|
||||
**✅ CORRECT**:
|
||||
```bash
|
||||
# Reset package caches first
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData
|
||||
rm -rf ~/Library/Caches/org.swift.swiftpm
|
||||
|
||||
# Reset packages in project
|
||||
xcodebuild -resolvePackageDependencies
|
||||
|
||||
# Clean build
|
||||
xcodebuild clean build -scheme YourScheme
|
||||
```
|
||||
|
||||
### Issue 2: CocoaPods Conflicts
|
||||
|
||||
**Symptom**: Pod install succeeds but build fails with framework errors
|
||||
|
||||
**Check Podfile.lock**:
|
||||
```bash
|
||||
# See what versions were actually installed
|
||||
cat Podfile.lock | grep -A 2 "PODS:"
|
||||
|
||||
# Compare with Podfile requirements
|
||||
cat Podfile | grep "pod "
|
||||
```
|
||||
|
||||
**Fix version conflicts**:
|
||||
```ruby
|
||||
# Podfile - be explicit about versions
|
||||
pod 'Alamofire', '~> 5.8.0' # Not just 'Alamofire'
|
||||
pod 'SwiftyJSON', '5.0.1' # Exact version if needed
|
||||
```
|
||||
|
||||
**Clean reinstall**:
|
||||
```bash
|
||||
# Remove all pods
|
||||
rm -rf Pods/
|
||||
rm Podfile.lock
|
||||
|
||||
# Reinstall
|
||||
pod install
|
||||
|
||||
# Open workspace (not project!)
|
||||
open YourApp.xcworkspace
|
||||
```
|
||||
|
||||
### Issue 3: Multiple Commands Produce Error
|
||||
|
||||
**Symptom**: "Multiple commands produce '/path/to/file'"
|
||||
|
||||
**Cause**: Same file added to multiple targets or build phases
|
||||
|
||||
**Fix**:
|
||||
1. Open Xcode
|
||||
2. Select file in navigator
|
||||
3. File Inspector → Target Membership
|
||||
4. Uncheck duplicate targets
|
||||
5. Or: Build Phases → Copy Bundle Resources → remove duplicates
|
||||
|
||||
### Issue 4: Framework Search Paths
|
||||
|
||||
**Symptom**: "Framework not found" or "Linker command failed"
|
||||
|
||||
**Check build settings**:
|
||||
```bash
|
||||
# Show all build settings
|
||||
xcodebuild -showBuildSettings -scheme YourScheme | grep FRAMEWORK_SEARCH_PATHS
|
||||
```
|
||||
|
||||
**Fix in Xcode**:
|
||||
1. Target → Build Settings
|
||||
2. Search "Framework Search Paths"
|
||||
3. Add path: `$(PROJECT_DIR)/Frameworks` (recursive)
|
||||
4. Or: `$(inherited)` to inherit from project
|
||||
|
||||
### Issue 5: SPM Version Conflicts
|
||||
|
||||
**Symptom**: Package resolution fails with version conflicts
|
||||
|
||||
**See dependency graph**:
|
||||
```bash
|
||||
# In project directory
|
||||
swift package show-dependencies
|
||||
|
||||
# Or see resolved versions
|
||||
cat Package.resolved
|
||||
```
|
||||
|
||||
**Fix conflicts**:
|
||||
```swift
|
||||
// Package.swift - be explicit
|
||||
.package(url: "https://github.com/owner/repo", exact: "1.2.3") // Exact version
|
||||
.package(url: "https://github.com/owner/repo", from: "1.2.0") // Minimum version
|
||||
.package(url: "https://github.com/owner/repo", .upToNextMajor(from: "1.0.0")) // SemVer
|
||||
```
|
||||
|
||||
**Reset resolution**:
|
||||
```bash
|
||||
# Clear package caches
|
||||
rm -rf .build
|
||||
rm Package.resolved
|
||||
|
||||
# Re-resolve
|
||||
swift package resolve
|
||||
```
|
||||
|
||||
## Dependency Resolution Strategies
|
||||
|
||||
### Strategy 1: Lock to Specific Versions
|
||||
|
||||
When stability matters more than latest features:
|
||||
|
||||
**CocoaPods**:
|
||||
```ruby
|
||||
pod 'Alamofire', '5.8.0' # Exact version
|
||||
pod 'SwiftyJSON', '~> 5.0.0' # Any 5.0.x
|
||||
```
|
||||
|
||||
**SPM**:
|
||||
```swift
|
||||
.package(url: "...", exact: "1.2.3")
|
||||
```
|
||||
|
||||
### Strategy 2: Use Version Ranges
|
||||
|
||||
When you want bug fixes but not breaking changes:
|
||||
|
||||
**CocoaPods**:
|
||||
```ruby
|
||||
pod 'Alamofire', '~> 5.8' # 5.8.x but not 5.9
|
||||
pod 'SwiftyJSON', '>= 5.0', '< 6.0' # Range
|
||||
```
|
||||
|
||||
**SPM**:
|
||||
```swift
|
||||
.package(url: "...", from: "1.2.0") // 1.2.0 and higher
|
||||
.package(url: "...", .upToNextMajor(from: "1.0.0")) // 1.x.x but not 2.0.0
|
||||
```
|
||||
|
||||
### Strategy 3: Fork and Pin
|
||||
|
||||
When you need custom modifications:
|
||||
|
||||
```bash
|
||||
# Fork repo on GitHub
|
||||
# Clone your fork
|
||||
git clone https://github.com/yourname/package.git
|
||||
|
||||
# In Package.swift, use your fork
|
||||
.package(url: "https://github.com/yourname/package", branch: "custom-fixes")
|
||||
```
|
||||
|
||||
### Strategy 4: Exclude Transitive Dependencies
|
||||
|
||||
When a dependency's dependency conflicts:
|
||||
|
||||
**SPM (not directly supported, use workarounds)**:
|
||||
```swift
|
||||
// Instead of this:
|
||||
.package(url: "https://github.com/problematic/package")
|
||||
|
||||
// Fork it and remove the conflicting dependency from its Package.swift
|
||||
```
|
||||
|
||||
**CocoaPods**:
|
||||
```ruby
|
||||
# Exclude specific subspecs
|
||||
pod 'Firebase/Core' # Not all of Firebase
|
||||
pod 'Firebase/Analytics'
|
||||
```
|
||||
|
||||
## Build Configuration Issues
|
||||
|
||||
### Debug vs Release Differences
|
||||
|
||||
**Symptom**: Builds in Debug, fails in Release (or vice versa)
|
||||
|
||||
**Check optimization settings**:
|
||||
```bash
|
||||
# Compare Debug and Release settings
|
||||
xcodebuild -showBuildSettings -configuration Debug > debug.txt
|
||||
xcodebuild -showBuildSettings -configuration Release > release.txt
|
||||
diff debug.txt release.txt
|
||||
```
|
||||
|
||||
**Common culprits**:
|
||||
- SWIFT_OPTIMIZATION_LEVEL (-Onone vs -O)
|
||||
- ENABLE_TESTABILITY (YES in Debug, NO in Release)
|
||||
- DEBUG preprocessor flag
|
||||
- Code signing settings
|
||||
|
||||
### Workspace vs Project
|
||||
|
||||
**Always open workspace with CocoaPods**:
|
||||
```bash
|
||||
# ❌ WRONG
|
||||
open YourApp.xcodeproj
|
||||
|
||||
# ✅ CORRECT
|
||||
open YourApp.xcworkspace
|
||||
```
|
||||
|
||||
**Check which you're building**:
|
||||
```bash
|
||||
# For workspace
|
||||
xcodebuild -workspace YourApp.xcworkspace -scheme YourScheme build
|
||||
|
||||
# For project only (no CocoaPods)
|
||||
xcodebuild -project YourApp.xcodeproj -scheme YourScheme build
|
||||
```
|
||||
|
||||
## Pressure Scenarios: When to Resist "Quick Fix" Advice
|
||||
|
||||
### The Problem
|
||||
|
||||
Under deadline pressure, senior engineers and teammates provide "quick fixes" based on pattern-matching:
|
||||
- "Just regenerate the lock file"
|
||||
- "Increment the build number"
|
||||
- "Delete DerivedData and rebuild"
|
||||
|
||||
These feel safe because they come from experience. **But if the diagnosis is wrong, the fix wastes time you don't have.**
|
||||
|
||||
**Critical insight** Time pressure makes authority bias STRONGER. You're more likely to trust advice when stressed.
|
||||
|
||||
### Red Flags — STOP Before Acting
|
||||
|
||||
If you hear ANY of these, pause 5 minutes before executing:
|
||||
|
||||
- ❌ **"This smells like..."** (pattern-matching, not diagnosis)
|
||||
- ❌ **"Just..."** (underestimating complexity)
|
||||
- ❌ **"This usually fixes it"** (worked once ≠ works always)
|
||||
- ❌ **"You have plenty of time"** (overconfidence about 24-hour turnaround)
|
||||
- ❌ **"This is safe"** (regenerating lock files CAN break things)
|
||||
|
||||
**Your brain under pressure** Trusts these phrases because they sound confident. Doesn't ask "but do they have evidence THIS is the root cause?"
|
||||
|
||||
### Mandatory Diagnosis Before "Quick Fix"
|
||||
|
||||
When someone senior suggests a fix under time pressure:
|
||||
|
||||
#### Step 1: Ask (Don't argue)
|
||||
```
|
||||
"I understand the pressure. Before we regenerate lock files,
|
||||
can we spend 5 minutes comparing the broken build to our
|
||||
working build? I want to know what we're fixing."
|
||||
```
|
||||
|
||||
#### Step 2: Demand Evidence
|
||||
- "What makes you think it's a lock file issue?"
|
||||
- "What changed between our last successful build and this failure?"
|
||||
- "Can we see the actual error from App Store build vs our build?"
|
||||
|
||||
#### Step 3: Document the Gamble
|
||||
```
|
||||
If we try "pod install":
|
||||
- Time to execute: 10 minutes
|
||||
- Time to learn it failed: 24 hours (next submission cycle)
|
||||
- Remaining time if it fails: 6 days
|
||||
- Alternative: Spend 1-2 hours diagnosing first
|
||||
|
||||
Cost of being wrong with quick fix: High
|
||||
Cost of spending 1 hour on diagnosis: Low
|
||||
```
|
||||
|
||||
#### Step 4: Push Back Professionally
|
||||
```
|
||||
"I want to move fast too. A 1-hour diagnosis now means we
|
||||
won't waste another 24-hour cycle. Let's document what we're
|
||||
testing before we submit."
|
||||
```
|
||||
|
||||
#### Why this works
|
||||
- You're not questioning their expertise
|
||||
- You're asking for evidence (legitimate request)
|
||||
- You're showing you understand the pressure
|
||||
- You're making the time math visible
|
||||
|
||||
### Real-World Example: App Store Review Blocker
|
||||
|
||||
**Scenario** App rejected in App Store build, passes locally.
|
||||
|
||||
**Senior says** "Regenerate lock file and resubmit (7 days buffer)"
|
||||
|
||||
#### What you do
|
||||
1. ❌ WRONG: Execute immediately, fail after 24 hours, now 6 days left
|
||||
2. ✅ RIGHT: Spend 1 hour comparing builds first
|
||||
|
||||
#### Comparison checklist
|
||||
```
|
||||
Local build that works:
|
||||
- Pod versions in Podfile.lock: [list them]
|
||||
- Xcode version: [version]
|
||||
- Derived Data: [timestamp]
|
||||
- CocoaPods version: [version]
|
||||
|
||||
App Store build that fails:
|
||||
- Pod versions used: [from error message]
|
||||
- Build system: [App Store's environment]
|
||||
- Differences: [explicitly document]
|
||||
```
|
||||
|
||||
#### After comparison
|
||||
- If versions match: Lock file isn't the issue. Skip the quick fix.
|
||||
- If versions differ: Now you understand what to fix.
|
||||
|
||||
**Time saved** 24 hours of wasted iteration.
|
||||
|
||||
### When to Trust Quick Fixes (Rare)
|
||||
|
||||
Quick fixes are safe ONLY when:
|
||||
|
||||
- [ ] You've seen this EXACT error before (not "similar")
|
||||
- [ ] You know the root cause (not "this usually works")
|
||||
- [ ] You can reproduce it locally (so you know if fix worked)
|
||||
- [ ] You have >48 hours buffer (so failure costs less)
|
||||
- [ ] You documented the fix in case you need to explain it later
|
||||
|
||||
#### In production crises, NONE of these are usually true.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### When Adding Dependencies
|
||||
- [ ] Specify exact versions or ranges (not just latest)
|
||||
- [ ] Check for known conflicts with existing deps
|
||||
- [ ] Test clean build after adding
|
||||
- [ ] Commit lockfile (Podfile.lock or Package.resolved)
|
||||
|
||||
### When Builds Fail
|
||||
- [ ] Run mandatory environment checks (xcode-debugging skill)
|
||||
- [ ] Check dependency lockfiles for changes
|
||||
- [ ] Verify using correct workspace/project file
|
||||
- [ ] Compare working vs broken build settings
|
||||
|
||||
### Before Shipping
|
||||
- [ ] Test both Debug and Release builds
|
||||
- [ ] Verify all dependencies have compatible licenses
|
||||
- [ ] Check binary size impact of dependencies
|
||||
- [ ] Test on clean machine or CI
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Not Committing Lockfiles
|
||||
```bash
|
||||
# ❌ BAD: .gitignore includes lockfiles
|
||||
Podfile.lock
|
||||
Package.resolved
|
||||
```
|
||||
|
||||
**Why**: Team members get different versions, builds differ
|
||||
|
||||
### ❌ Using "Latest" Version
|
||||
```ruby
|
||||
# ❌ BAD: No version specified
|
||||
pod 'Alamofire'
|
||||
```
|
||||
|
||||
**Why**: Breaking changes when dependency updates
|
||||
|
||||
### ❌ Mixing Package Managers
|
||||
```
|
||||
Project uses both:
|
||||
- CocoaPods (Podfile)
|
||||
- Carthage (Cartfile)
|
||||
- SPM (Package.swift)
|
||||
```
|
||||
|
||||
**Why**: Conflicts are inevitable, pick one primary manager
|
||||
|
||||
### ❌ Not Cleaning After Dependency Changes
|
||||
```bash
|
||||
# ❌ BAD: Just rebuild
|
||||
xcodebuild build
|
||||
|
||||
# ✅ GOOD: Clean first
|
||||
xcodebuild clean build
|
||||
```
|
||||
|
||||
### ❌ Opening Project Instead of Workspace
|
||||
When using CocoaPods, always open .xcworkspace not .xcodeproj
|
||||
|
||||
## Command Reference
|
||||
|
||||
```bash
|
||||
# CocoaPods
|
||||
pod install # Install dependencies
|
||||
pod update # Update to latest versions
|
||||
pod update PodName # Update specific pod
|
||||
pod outdated # Check for updates
|
||||
pod deintegrate # Remove CocoaPods from project
|
||||
|
||||
# Swift Package Manager
|
||||
swift package resolve # Resolve dependencies
|
||||
swift package update # Update dependencies
|
||||
swift package show-dependencies # Show dependency tree
|
||||
swift package reset # Reset package cache
|
||||
xcodebuild -resolvePackageDependencies # Xcode's SPM resolve
|
||||
|
||||
# Carthage
|
||||
carthage update # Update dependencies
|
||||
carthage bootstrap # Download pre-built frameworks
|
||||
carthage build --platform iOS # Build for specific platform
|
||||
|
||||
# Xcode Build
|
||||
xcodebuild clean # Clean build folder
|
||||
xcodebuild -list # List schemes and targets
|
||||
xcodebuild -showBuildSettings # Show all build settings
|
||||
```
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
**Before** (trial-and-error with dependencies):
|
||||
- Dependency issue: 2-4 hours debugging
|
||||
- Clean builds not run consistently
|
||||
- Version conflicts surprise team
|
||||
- CI failures from dependency mismatches
|
||||
|
||||
**After** (systematic dependency management):
|
||||
- Dependency issue: 15-30 minutes (check lockfile → resolve)
|
||||
- Clean builds mandatory after dep changes
|
||||
- Explicit version constraints prevent surprises
|
||||
- CI matches local builds (committed lockfiles)
|
||||
|
||||
**Key insight** Lock down dependency versions early. Flexibility causes more problems than it solves.
|
||||
|
||||
## Resources
|
||||
|
||||
**Docs**: swift.org/package-manager, /xcode/build-system
|
||||
|
||||
**GitHub**: Carthage/Carthage
|
||||
|
||||
**Skills**: axiom-xcode-debugging
|
||||
|
||||
---
|
||||
|
||||
**History:** See git log for changes
|
||||
430
skill-index/skills/axiom-database-migration/SKILL.md
Normal file
430
skill-index/skills/axiom-database-migration/SKILL.md
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
---
|
||||
name: axiom-database-migration
|
||||
description: Use when adding/modifying database columns, encountering "FOREIGN KEY constraint failed", "no such column", "cannot add NOT NULL column" errors, or creating schema migrations for SQLite/GRDB/SQLiteData - prevents data loss with safe migration patterns and testing workflows for iOS/macOS apps
|
||||
skill_type: discipline
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Database Migration
|
||||
|
||||
## Overview
|
||||
|
||||
Safe database schema evolution for production apps with user data. **Core principle** Migrations are immutable after shipping. Make them additive, idempotent, and thoroughly tested.
|
||||
|
||||
## Example Prompts
|
||||
|
||||
These are real questions developers ask that this skill is designed to answer:
|
||||
|
||||
#### 1. "I need to add a new column to store user preferences, but the app is already live with user data. How do I do this safely?"
|
||||
→ The skill covers safe additive patterns for adding columns without losing existing data, including idempotency checks
|
||||
|
||||
#### 2. "I'm getting 'cannot add NOT NULL column' errors when I try to migrate. What does this mean and how do I fix it?"
|
||||
→ The skill explains why NOT NULL columns fail with existing rows, and shows the safe pattern (nullable first, backfill later)
|
||||
|
||||
#### 3. "I need to change a column from text to integer. Can I just ALTER the column type?"
|
||||
→ The skill demonstrates the safe pattern: add new column → migrate data → deprecate old (NEVER delete)
|
||||
|
||||
#### 4. "I'm adding a foreign key relationship between tables. How do I add the relationship without breaking existing data?"
|
||||
→ The skill covers safe foreign key patterns: add column → populate data → add index (SQLite limitations explained)
|
||||
|
||||
#### 5. "Users are reporting crashes after the last update. I changed a migration but the app is already in production. What do I do?"
|
||||
→ The skill explains migrations are immutable after shipping; shows how to create a new migration to fix the issue rather than modifying the old one
|
||||
|
||||
---
|
||||
|
||||
## ⛔ NEVER Do These (Data Loss Risk)
|
||||
|
||||
#### These actions DESTROY user data in production
|
||||
|
||||
❌ **NEVER use DROP TABLE** with user data
|
||||
❌ **NEVER modify shipped migrations** (create new one instead)
|
||||
❌ **NEVER recreate tables** to change schema (loses data)
|
||||
❌ **NEVER add NOT NULL column** without DEFAULT value
|
||||
❌ **NEVER delete columns** (SQLite doesn't support DROP COLUMN safely)
|
||||
|
||||
#### If you're tempted to do any of these, STOP and use the safe patterns below.
|
||||
|
||||
## Mandatory Rules
|
||||
|
||||
#### ALWAYS follow these
|
||||
|
||||
1. **Additive only** Add new columns/tables, never delete
|
||||
2. **Idempotent** Check existence before creating (safe to run twice)
|
||||
3. **Transactional** Wrap entire migration in single transaction
|
||||
4. **Test both paths** Fresh install AND migration from previous version
|
||||
5. **Nullable first** Add columns as NULL, backfill later if needed
|
||||
6. **Immutable** Once shipped to users, migrations cannot be changed
|
||||
|
||||
## Safe Patterns
|
||||
|
||||
### Adding Column (Most Common)
|
||||
|
||||
```swift
|
||||
// ✅ Safe pattern
|
||||
func migration00X_AddNewColumn() throws {
|
||||
try database.write { db in
|
||||
// 1. Check if column exists (idempotency)
|
||||
let hasColumn = try db.columns(in: "tableName")
|
||||
.contains { $0.name == "newColumn" }
|
||||
|
||||
if !hasColumn {
|
||||
// 2. Add as nullable (works with existing rows)
|
||||
try db.execute(sql: """
|
||||
ALTER TABLE tableName
|
||||
ADD COLUMN newColumn TEXT
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Why this works
|
||||
- Nullable columns don't require DEFAULT
|
||||
- Existing rows get NULL automatically
|
||||
- No data transformation needed
|
||||
- Safe for users upgrading from old versions
|
||||
|
||||
### Adding Column with Default Value
|
||||
|
||||
```swift
|
||||
// ✅ Safe pattern with default
|
||||
func migration00X_AddColumnWithDefault() throws {
|
||||
try database.write { db in
|
||||
let hasColumn = try db.columns(in: "tracks")
|
||||
.contains { $0.name == "playCount" }
|
||||
|
||||
if !hasColumn {
|
||||
try db.execute(sql: """
|
||||
ALTER TABLE tracks
|
||||
ADD COLUMN playCount INTEGER DEFAULT 0
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Changing Column Type (Advanced)
|
||||
|
||||
**Pattern**: Add new column → migrate data → deprecate old (NEVER delete)
|
||||
|
||||
```swift
|
||||
// ✅ Safe pattern for type change
|
||||
func migration00X_ChangeColumnType() throws {
|
||||
try database.write { db in
|
||||
// Step 1: Add new column with new type
|
||||
try db.execute(sql: """
|
||||
ALTER TABLE users
|
||||
ADD COLUMN age_new INTEGER
|
||||
""")
|
||||
|
||||
// Step 2: Migrate existing data
|
||||
try db.execute(sql: """
|
||||
UPDATE users
|
||||
SET age_new = CAST(age_old AS INTEGER)
|
||||
WHERE age_old IS NOT NULL
|
||||
""")
|
||||
|
||||
// Step 3: Application code uses age_new going forward
|
||||
// (Never delete age_old column - just stop using it)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Adding Foreign Key Constraint
|
||||
|
||||
```swift
|
||||
// ✅ Safe pattern for foreign keys
|
||||
func migration00X_AddForeignKey() throws {
|
||||
try database.write { db in
|
||||
// Step 1: Add new column (nullable initially)
|
||||
try db.execute(sql: """
|
||||
ALTER TABLE tracks
|
||||
ADD COLUMN album_id TEXT
|
||||
""")
|
||||
|
||||
// Step 2: Populate the data
|
||||
try db.execute(sql: """
|
||||
UPDATE tracks
|
||||
SET album_id = (
|
||||
SELECT id FROM albums
|
||||
WHERE albums.title = tracks.album_name
|
||||
)
|
||||
""")
|
||||
|
||||
// Step 3: Add index (helps query performance)
|
||||
try db.execute(sql: """
|
||||
CREATE INDEX IF NOT EXISTS idx_tracks_album_id
|
||||
ON tracks(album_id)
|
||||
""")
|
||||
|
||||
// Note: SQLite doesn't allow adding FK constraints to existing tables
|
||||
// The foreign key relationship is enforced at the application level
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Complex Schema Refactoring
|
||||
|
||||
**Pattern**: Break into multiple migrations
|
||||
|
||||
```swift
|
||||
// Migration 1: Add new structure
|
||||
func migration010_AddNewTable() throws {
|
||||
try database.write { db in
|
||||
try db.execute(sql: """
|
||||
CREATE TABLE IF NOT EXISTS new_structure (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT
|
||||
)
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 2: Copy data
|
||||
func migration011_MigrateData() throws {
|
||||
try database.write { db in
|
||||
try db.execute(sql: """
|
||||
INSERT INTO new_structure (id, data)
|
||||
SELECT id, data FROM old_structure
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 3: Add indexes
|
||||
func migration012_AddIndexes() throws {
|
||||
try database.write { db in
|
||||
try db.execute(sql: """
|
||||
CREATE INDEX IF NOT EXISTS idx_new_structure_data
|
||||
ON new_structure(data)
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
// Old structure stays around (deprecated in code)
|
||||
```
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
#### BEFORE deploying any migration
|
||||
|
||||
```swift
|
||||
// Test 1: Migration path (CRITICAL - tests data preservation)
|
||||
@Test func migrationFromV1ToV2Succeeds() async throws {
|
||||
let db = try Database(inMemory: true)
|
||||
|
||||
// Simulate v1 schema
|
||||
try db.write { db in
|
||||
try db.execute(sql: "CREATE TABLE tableName (id TEXT PRIMARY KEY)")
|
||||
try db.execute(sql: "INSERT INTO tableName (id) VALUES ('test1')")
|
||||
}
|
||||
|
||||
// Run v2 migration
|
||||
try db.runMigrations()
|
||||
|
||||
// Verify data survived + new column exists
|
||||
try db.read { db in
|
||||
let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tableName")
|
||||
#expect(count == 1) // Data preserved
|
||||
|
||||
let columns = try db.columns(in: "tableName").map { $0.name }
|
||||
#expect(columns.contains("newColumn")) // New column exists
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test 2** Fresh install (run all migrations, verify final schema)
|
||||
```swift
|
||||
@Test func freshInstallCreatesCorrectSchema() async throws {
|
||||
let db = try Database(inMemory: true)
|
||||
|
||||
// Run all migrations
|
||||
try db.runMigrations()
|
||||
|
||||
// Verify final schema
|
||||
try db.read { db in
|
||||
let tables = try db.tables()
|
||||
#expect(tables.contains("tableName"))
|
||||
|
||||
let columns = try db.columns(in: "tableName").map { $0.name }
|
||||
#expect(columns.contains("id"))
|
||||
#expect(columns.contains("newColumn"))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Test 3** Idempotency (run migrations twice, should not throw)
|
||||
```swift
|
||||
@Test func migrationsAreIdempotent() async throws {
|
||||
let db = try Database(inMemory: true)
|
||||
|
||||
// Run migrations twice
|
||||
try db.runMigrations()
|
||||
try db.runMigrations() // Should not throw
|
||||
|
||||
// Verify still correct
|
||||
try db.read { db in
|
||||
let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tableName")
|
||||
#expect(count == 0) // No duplicate data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Manual testing (before TestFlight)
|
||||
1. Install v(n-1) build on device → add real user data
|
||||
2. Install v(n) build (with new migration)
|
||||
3. Verify: App launches, data visible, no crashes
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
What are you trying to do?
|
||||
├─ Add new column?
|
||||
│ └─ ALTER TABLE ADD COLUMN (nullable) → Done
|
||||
├─ Add column with default?
|
||||
│ └─ ALTER TABLE ADD COLUMN ... DEFAULT value → Done
|
||||
├─ Change column type?
|
||||
│ └─ Add new column → Migrate data → Deprecate old → Done
|
||||
├─ Delete column?
|
||||
│ └─ Mark as deprecated in code → Never delete from schema → Done
|
||||
├─ Rename column?
|
||||
│ └─ Add new column → Migrate data → Deprecate old → Done
|
||||
├─ Add foreign key?
|
||||
│ └─ Add column → Populate data → Add index → Done
|
||||
└─ Complex refactor?
|
||||
└─ Break into multiple migrations → Test each step → Done
|
||||
```
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| `FOREIGN KEY constraint failed` | Check parent row exists, or disable FK temporarily |
|
||||
| `no such column: columnName` | Add migration to create column |
|
||||
| `cannot add NOT NULL column` | Use nullable column first, backfill in separate migration |
|
||||
| `table tableName already exists` | Add `IF NOT EXISTS` clause |
|
||||
| `duplicate column name` | Check if column exists before adding (idempotency) |
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
❌ **Adding NOT NULL without DEFAULT**
|
||||
```swift
|
||||
// ❌ Fails on existing data
|
||||
ALTER TABLE albums ADD COLUMN rating INTEGER NOT NULL
|
||||
```
|
||||
|
||||
✅ **Correct: Add as nullable first**
|
||||
```swift
|
||||
ALTER TABLE albums ADD COLUMN rating INTEGER // NULL allowed
|
||||
// Backfill in separate migration if needed
|
||||
UPDATE albums SET rating = 0 WHERE rating IS NULL
|
||||
```
|
||||
|
||||
❌ **Forgetting to check for existence** — Always add `IF NOT EXISTS` or manual check
|
||||
|
||||
❌ **Modifying shipped migrations** — Create new migration instead
|
||||
|
||||
❌ **Not testing migration path** — Always test upgrade from previous version
|
||||
|
||||
## GRDB-Specific Patterns
|
||||
|
||||
### DatabaseMigrator Setup
|
||||
|
||||
```swift
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
// Migration 1
|
||||
migrator.registerMigration("v1") { db in
|
||||
try db.execute(sql: """
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
}
|
||||
|
||||
// Migration 2
|
||||
migrator.registerMigration("v2") { db in
|
||||
let hasColumn = try db.columns(in: "users")
|
||||
.contains { $0.name == "email" }
|
||||
|
||||
if !hasColumn {
|
||||
try db.execute(sql: """
|
||||
ALTER TABLE users
|
||||
ADD COLUMN email TEXT
|
||||
""")
|
||||
}
|
||||
}
|
||||
|
||||
// Apply migrations
|
||||
try migrator.migrate(dbQueue)
|
||||
```
|
||||
|
||||
### Checking Migration Status
|
||||
|
||||
```swift
|
||||
// Check which migrations have been applied
|
||||
let appliedMigrations = try dbQueue.read { db in
|
||||
try migrator.appliedMigrations(db)
|
||||
}
|
||||
print("Applied migrations: \(appliedMigrations)")
|
||||
|
||||
// Check if migrations are needed
|
||||
let hasBeenMigrated = try dbQueue.read { db in
|
||||
try migrator.hasBeenMigrated(db)
|
||||
}
|
||||
```
|
||||
|
||||
## SwiftData Migrations
|
||||
|
||||
For SwiftData (iOS 17+), use `VersionedSchema` and `SchemaMigrationPlan`:
|
||||
|
||||
```swift
|
||||
// Define schema versions
|
||||
enum MyAppSchemaV1: VersionedSchema {
|
||||
static var versionIdentifier = Schema.Version(1, 0, 0)
|
||||
static var models: [any PersistentModel.Type] {
|
||||
[Track.self, Album.self]
|
||||
}
|
||||
}
|
||||
|
||||
enum MyAppSchemaV2: VersionedSchema {
|
||||
static var versionIdentifier = Schema.Version(2, 0, 0)
|
||||
static var models: [any PersistentModel.Type] {
|
||||
[Track.self, Album.self, Playlist.self] // Added Playlist
|
||||
}
|
||||
}
|
||||
|
||||
// Define migration plan
|
||||
enum MyAppMigrationPlan: SchemaMigrationPlan {
|
||||
static var schemas: [any VersionedSchema.Type] {
|
||||
[MyAppSchemaV1.self, MyAppSchemaV2.self]
|
||||
}
|
||||
|
||||
static var stages: [MigrationStage] {
|
||||
[migrateV1toV2]
|
||||
}
|
||||
|
||||
static let migrateV1toV2 = MigrationStage.custom(
|
||||
fromVersion: MyAppSchemaV1.self,
|
||||
toVersion: MyAppSchemaV2.self,
|
||||
willMigrate: nil,
|
||||
didMigrate: { context in
|
||||
// Custom migration logic here
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
**Before** Developer adds NOT NULL column → migration fails for 50% of users → emergency rollback → data inconsistency
|
||||
|
||||
**After** Developer adds nullable column → tests both paths → smooth deployment → backfills data in v2
|
||||
|
||||
**Key insight** Migrations can't be rolled back in production. Get them right the first time through thorough testing.
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-11-28
|
||||
**Frameworks**: SQLite, GRDB, SwiftData
|
||||
**Status**: Production-ready patterns for safe schema evolution
|
||||
670
skill-index/skills/axiom-grdb/SKILL.md
Normal file
670
skill-index/skills/axiom-grdb/SKILL.md
Normal file
|
|
@ -0,0 +1,670 @@
|
|||
---
|
||||
name: axiom-grdb
|
||||
description: Use when writing raw SQL queries with GRDB, complex joins, ValueObservation for reactive queries, DatabaseMigrator patterns, query profiling under performance pressure, or dropping down from SQLiteData for performance - direct SQLite access for iOS/macOS
|
||||
skill_type: discipline
|
||||
version: 1.1.0
|
||||
last_updated: TDD-tested with complex query performance scenarios
|
||||
---
|
||||
|
||||
# GRDB
|
||||
|
||||
## Overview
|
||||
|
||||
Direct SQLite access using [GRDB.swift](https://github.com/groue/GRDB.swift) — a toolkit for SQLite databases with type-safe queries, migrations, and reactive observation.
|
||||
|
||||
**Core principle** Type-safe Swift wrapper around raw SQL with full SQLite power when you need it.
|
||||
|
||||
**Requires** iOS 13+, Swift 5.7+
|
||||
**License** MIT (free and open source)
|
||||
|
||||
## When to Use GRDB
|
||||
|
||||
#### Use raw GRDB when you need
|
||||
- ✅ Complex SQL joins across 4+ tables
|
||||
- ✅ Window functions (ROW_NUMBER, RANK, LAG/LEAD)
|
||||
- ✅ Reactive queries with ValueObservation
|
||||
- ✅ Full control over SQL for performance
|
||||
- ✅ Advanced migration logic beyond schema changes
|
||||
|
||||
**Note:** SQLiteData now supports GROUP BY (`.group(by:)`) and HAVING (`.having()`) via the query builder — see the `axiom-sqlitedata-ref` skill.
|
||||
|
||||
#### Use SQLiteData instead when
|
||||
- Type-safe `@Table` models are sufficient
|
||||
- CloudKit sync needed
|
||||
- Prefer declarative queries over SQL
|
||||
|
||||
#### Use SwiftData when
|
||||
- Simple CRUD with native Apple integration
|
||||
- Don't need raw SQL control
|
||||
|
||||
**For migrations** See the `axiom-database-migration` skill for safe schema evolution patterns.
|
||||
|
||||
## Example Prompts
|
||||
|
||||
These are real questions developers ask that this skill is designed to answer:
|
||||
|
||||
#### 1. "I need to query messages with their authors and count of reactions in one query. How do I write the JOIN?"
|
||||
→ The skill shows complex JOIN queries with multiple tables and aggregations
|
||||
|
||||
#### 2. "I want to observe a filtered list and update the UI whenever notes with a specific tag change."
|
||||
→ The skill covers ValueObservation patterns for reactive query updates
|
||||
|
||||
#### 3. "I'm importing thousands of chat records and need custom migration logic. How do I use DatabaseMigrator?"
|
||||
→ The skill explains migration registration, data transforms, and safe rollback patterns
|
||||
|
||||
#### 4. "My query is slow (takes 10+ seconds). How do I profile and optimize it?"
|
||||
→ The skill covers EXPLAIN QUERY PLAN, database.trace for profiling, and index creation
|
||||
|
||||
#### 5. "I need to fetch tasks grouped by due date with completion counts, ordered by priority. Raw SQL seems easier than type-safe queries."
|
||||
→ The skill demonstrates when GRDB's raw SQL is clearer than type-safe wrappers
|
||||
|
||||
---
|
||||
|
||||
## Database Setup
|
||||
|
||||
### DatabaseQueue (Single Connection)
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
|
||||
// File-based database
|
||||
let dbPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
|
||||
let dbQueue = try DatabaseQueue(path: "\(dbPath)/db.sqlite")
|
||||
|
||||
// In-memory database (tests)
|
||||
let dbQueue = try DatabaseQueue()
|
||||
```
|
||||
|
||||
### DatabasePool (Connection Pool)
|
||||
|
||||
```swift
|
||||
// For apps with heavy concurrent access
|
||||
let dbPool = try DatabasePool(path: dbPath)
|
||||
```
|
||||
|
||||
**Use Queue for** Most apps (simpler, sufficient)
|
||||
**Use Pool for** Heavy concurrent writes from multiple threads
|
||||
|
||||
## Record Types
|
||||
|
||||
### Using Codable
|
||||
|
||||
```swift
|
||||
struct Track: Codable {
|
||||
var id: String
|
||||
var title: String
|
||||
var artist: String
|
||||
var duration: TimeInterval
|
||||
}
|
||||
|
||||
// Fetch
|
||||
let tracks = try dbQueue.read { db in
|
||||
try Track.fetchAll(db, sql: "SELECT * FROM tracks")
|
||||
}
|
||||
|
||||
// Insert
|
||||
try dbQueue.write { db in
|
||||
try track.insert(db) // Codable conformance provides insert
|
||||
}
|
||||
```
|
||||
|
||||
### FetchableRecord (Read-Only)
|
||||
|
||||
```swift
|
||||
struct TrackInfo: FetchableRecord {
|
||||
var title: String
|
||||
var artist: String
|
||||
var albumTitle: String
|
||||
|
||||
init(row: Row) {
|
||||
title = row["title"]
|
||||
artist = row["artist"]
|
||||
albumTitle = row["album_title"]
|
||||
}
|
||||
}
|
||||
|
||||
let results = try dbQueue.read { db in
|
||||
try TrackInfo.fetchAll(db, sql: """
|
||||
SELECT tracks.title, tracks.artist, albums.title as album_title
|
||||
FROM tracks
|
||||
JOIN albums ON tracks.albumId = albums.id
|
||||
""")
|
||||
}
|
||||
```
|
||||
|
||||
### PersistableRecord (Write)
|
||||
|
||||
```swift
|
||||
struct Track: Codable, PersistableRecord {
|
||||
var id: String
|
||||
var title: String
|
||||
|
||||
// Customize table name
|
||||
static let databaseTableName = "tracks"
|
||||
}
|
||||
|
||||
try dbQueue.write { db in
|
||||
var track = Track(id: "1", title: "Song")
|
||||
try track.insert(db)
|
||||
|
||||
track.title = "Updated"
|
||||
try track.update(db)
|
||||
|
||||
try track.delete(db)
|
||||
}
|
||||
```
|
||||
|
||||
## Raw SQL Queries
|
||||
|
||||
### Reading Data
|
||||
|
||||
```swift
|
||||
// Fetch all rows
|
||||
let rows = try dbQueue.read { db in
|
||||
try Row.fetchAll(db, sql: "SELECT * FROM tracks WHERE genre = ?", arguments: ["Rock"])
|
||||
}
|
||||
|
||||
// Fetch single value
|
||||
let count = try dbQueue.read { db in
|
||||
try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM tracks")
|
||||
}
|
||||
|
||||
// Fetch into Codable
|
||||
let tracks = try dbQueue.read { db in
|
||||
try Track.fetchAll(db, sql: "SELECT * FROM tracks ORDER BY title")
|
||||
}
|
||||
```
|
||||
|
||||
### Writing Data
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
try db.execute(sql: """
|
||||
INSERT INTO tracks (id, title, artist, duration)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", arguments: ["1", "Song", "Artist", 240])
|
||||
}
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
// Automatic transaction - all or nothing
|
||||
for track in tracks {
|
||||
try track.insert(db)
|
||||
}
|
||||
// Commits automatically on success, rolls back on error
|
||||
}
|
||||
```
|
||||
|
||||
## Type-Safe Query Interface
|
||||
|
||||
### Filtering
|
||||
|
||||
```swift
|
||||
let request = Track
|
||||
.filter(Column("genre") == "Rock")
|
||||
.filter(Column("duration") > 180)
|
||||
|
||||
let tracks = try dbQueue.read { db in
|
||||
try request.fetchAll(db)
|
||||
}
|
||||
```
|
||||
|
||||
### Sorting
|
||||
|
||||
```swift
|
||||
let request = Track
|
||||
.order(Column("title").asc)
|
||||
.limit(10)
|
||||
```
|
||||
|
||||
### Joins
|
||||
|
||||
```swift
|
||||
struct TrackWithAlbum: FetchableRecord {
|
||||
var trackTitle: String
|
||||
var albumTitle: String
|
||||
}
|
||||
|
||||
let request = Track
|
||||
.joining(required: Track.belongsTo(Album.self))
|
||||
.select(Column("title").forKey("trackTitle"), Column("album_title").forKey("albumTitle"))
|
||||
|
||||
let results = try dbQueue.read { db in
|
||||
try TrackWithAlbum.fetchAll(db, request)
|
||||
}
|
||||
```
|
||||
|
||||
## Complex Joins
|
||||
|
||||
```swift
|
||||
let sql = """
|
||||
SELECT
|
||||
tracks.title as track_title,
|
||||
albums.title as album_title,
|
||||
artists.name as artist_name,
|
||||
COUNT(plays.id) as play_count
|
||||
FROM tracks
|
||||
JOIN albums ON tracks.albumId = albums.id
|
||||
JOIN artists ON albums.artistId = artists.id
|
||||
LEFT JOIN plays ON plays.trackId = tracks.id
|
||||
WHERE artists.genre = ?
|
||||
GROUP BY tracks.id
|
||||
HAVING play_count > 10
|
||||
ORDER BY play_count DESC
|
||||
LIMIT 50
|
||||
"""
|
||||
|
||||
struct TrackStats: FetchableRecord {
|
||||
var trackTitle: String
|
||||
var albumTitle: String
|
||||
var artistName: String
|
||||
var playCount: Int
|
||||
|
||||
init(row: Row) {
|
||||
trackTitle = row["track_title"]
|
||||
albumTitle = row["album_title"]
|
||||
artistName = row["artist_name"]
|
||||
playCount = row["play_count"]
|
||||
}
|
||||
}
|
||||
|
||||
let stats = try dbQueue.read { db in
|
||||
try TrackStats.fetchAll(db, sql: sql, arguments: ["Rock"])
|
||||
}
|
||||
```
|
||||
|
||||
## ValueObservation (Reactive Queries)
|
||||
|
||||
### Basic Observation
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
import Combine
|
||||
|
||||
let observation = ValueObservation.tracking { db in
|
||||
try Track.fetchAll(db)
|
||||
}
|
||||
|
||||
// Start observing with Combine
|
||||
let cancellable = observation.publisher(in: dbQueue)
|
||||
.sink(
|
||||
receiveCompletion: { _ in },
|
||||
receiveValue: { tracks in
|
||||
print("Tracks updated: \(tracks.count)")
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### SwiftUI Integration
|
||||
|
||||
```swift
|
||||
import GRDB
|
||||
import GRDBQuery // https://github.com/groue/GRDBQuery
|
||||
|
||||
@Query(Tracks())
|
||||
var tracks: [Track]
|
||||
|
||||
struct Tracks: Queryable {
|
||||
static var defaultValue: [Track] { [] }
|
||||
|
||||
func publisher(in dbQueue: DatabaseQueue) -> AnyPublisher<[Track], Error> {
|
||||
ValueObservation
|
||||
.tracking { db in try Track.fetchAll(db) }
|
||||
.publisher(in: dbQueue)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**See** [GRDBQuery documentation](https://github.com/groue/GRDBQuery) for SwiftUI reactive bindings.
|
||||
|
||||
### Filtered Observation
|
||||
|
||||
```swift
|
||||
func observeGenre(_ genre: String) -> ValueObservation<[Track]> {
|
||||
ValueObservation.tracking { db in
|
||||
try Track
|
||||
.filter(Column("genre") == genre)
|
||||
.fetchAll(db)
|
||||
}
|
||||
}
|
||||
|
||||
let cancellable = observeGenre("Rock")
|
||||
.publisher(in: dbQueue)
|
||||
.sink { tracks in
|
||||
print("Rock tracks: \(tracks.count)")
|
||||
}
|
||||
```
|
||||
|
||||
## Migrations
|
||||
|
||||
### DatabaseMigrator
|
||||
|
||||
```swift
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
// Migration 1: Create tables
|
||||
migrator.registerMigration("v1") { db in
|
||||
try db.create(table: "tracks") { t in
|
||||
t.column("id", .text).primaryKey()
|
||||
t.column("title", .text).notNull()
|
||||
t.column("artist", .text).notNull()
|
||||
t.column("duration", .real).notNull()
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 2: Add column
|
||||
migrator.registerMigration("v2_add_genre") { db in
|
||||
try db.alter(table: "tracks") { t in
|
||||
t.add(column: "genre", .text)
|
||||
}
|
||||
}
|
||||
|
||||
// Migration 3: Add index
|
||||
migrator.registerMigration("v3_add_indexes") { db in
|
||||
try db.create(index: "idx_genre", on: "tracks", columns: ["genre"])
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
try migrator.migrate(dbQueue)
|
||||
```
|
||||
|
||||
**For migration safety patterns** See the `axiom-database-migration` skill.
|
||||
|
||||
### Migration with Data Transform
|
||||
|
||||
```swift
|
||||
migrator.registerMigration("v4_normalize_artists") { db in
|
||||
// 1. Create new table
|
||||
try db.create(table: "artists") { t in
|
||||
t.column("id", .text).primaryKey()
|
||||
t.column("name", .text).notNull()
|
||||
}
|
||||
|
||||
// 2. Extract unique artists
|
||||
try db.execute(sql: """
|
||||
INSERT INTO artists (id, name)
|
||||
SELECT DISTINCT
|
||||
lower(replace(artist, ' ', '_')) as id,
|
||||
artist as name
|
||||
FROM tracks
|
||||
""")
|
||||
|
||||
// 3. Add foreign key to tracks
|
||||
try db.alter(table: "tracks") { t in
|
||||
t.add(column: "artistId", .text)
|
||||
.references("artists", onDelete: .cascade)
|
||||
}
|
||||
|
||||
// 4. Populate foreign keys
|
||||
try db.execute(sql: """
|
||||
UPDATE tracks
|
||||
SET artistId = (
|
||||
SELECT id FROM artists
|
||||
WHERE artists.name = tracks.artist
|
||||
)
|
||||
""")
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Batch Writes
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
for batch in tracks.chunked(into: 500) {
|
||||
for track in batch {
|
||||
try track.insert(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Prepared Statements
|
||||
|
||||
```swift
|
||||
try dbQueue.write { db in
|
||||
let statement = try db.makeStatement(sql: """
|
||||
INSERT INTO tracks (id, title, artist, duration)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""")
|
||||
|
||||
for track in tracks {
|
||||
try statement.execute(arguments: [track.id, track.title, track.artist, track.duration])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Indexes
|
||||
|
||||
```swift
|
||||
try db.create(index: "idx_tracks_artist", on: "tracks", columns: ["artist"])
|
||||
try db.create(index: "idx_tracks_genre_duration", on: "tracks", columns: ["genre", "duration"])
|
||||
|
||||
// Unique index
|
||||
try db.create(index: "idx_tracks_unique_title", on: "tracks", columns: ["title"], unique: true)
|
||||
```
|
||||
|
||||
### Query Planning
|
||||
|
||||
```swift
|
||||
// Analyze query performance
|
||||
let explanation = try dbQueue.read { db in
|
||||
try String.fetchOne(db, sql: "EXPLAIN QUERY PLAN SELECT * FROM tracks WHERE artist = ?", arguments: ["Artist"])
|
||||
}
|
||||
print(explanation)
|
||||
```
|
||||
|
||||
## Dropping Down from SQLiteData
|
||||
|
||||
When using SQLiteData but need GRDB for specific operations:
|
||||
|
||||
```swift
|
||||
import SQLiteData
|
||||
import GRDB
|
||||
|
||||
@Dependency(\.database) var database // SQLiteData Database
|
||||
|
||||
// Access underlying GRDB DatabaseQueue
|
||||
try await database.database.write { db in
|
||||
// Full GRDB power here
|
||||
try db.execute(sql: "CREATE INDEX idx_genre ON tracks(genre)")
|
||||
}
|
||||
```
|
||||
|
||||
#### Common scenarios
|
||||
- Complex JOIN queries
|
||||
- Custom migrations
|
||||
- Bulk SQL operations
|
||||
- ValueObservation setup
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Common Operations
|
||||
|
||||
```swift
|
||||
// Read single value
|
||||
let count = try db.fetchOne(Int.self, sql: "SELECT COUNT(*) FROM tracks")
|
||||
|
||||
// Read all rows
|
||||
let rows = try Row.fetchAll(db, sql: "SELECT * FROM tracks WHERE genre = ?", arguments: ["Rock"])
|
||||
|
||||
// Write
|
||||
try db.execute(sql: "INSERT INTO tracks VALUES (?, ?, ?)", arguments: [id, title, artist])
|
||||
|
||||
// Transaction
|
||||
try dbQueue.write { db in
|
||||
// All or nothing
|
||||
}
|
||||
|
||||
// Observe changes
|
||||
ValueObservation.tracking { db in
|
||||
try Track.fetchAll(db)
|
||||
}.publisher(in: dbQueue)
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
**GitHub**: groue/GRDB.swift, groue/GRDBQuery
|
||||
|
||||
**Docs**: sqlite.org/docs.html
|
||||
|
||||
**Skills**: axiom-database-migration, axiom-sqlitedata, axiom-swiftdata
|
||||
|
||||
## Production Performance: Query Optimization Under Pressure
|
||||
|
||||
### Red Flags — When GRDB Queries Slow Down
|
||||
|
||||
If you see ANY of these symptoms:
|
||||
- ❌ Complex JOIN query takes 10+ seconds
|
||||
- ❌ ValueObservation runs on every single change (battery drain)
|
||||
- ❌ Can't explain why migration ran twice on old version
|
||||
|
||||
#### DO NOT
|
||||
1. Blindly add indexes (don't know which columns help)
|
||||
2. Move logic to Swift (premature escape from database)
|
||||
3. Over-engineer migrations (distrust the system)
|
||||
|
||||
#### DO
|
||||
1. Profile with `database.trace`
|
||||
2. Use `EXPLAIN QUERY PLAN` to understand execution
|
||||
3. Trust GRDB's migration versioning system
|
||||
|
||||
### Profiling Complex Queries
|
||||
|
||||
#### When query is slow (10+ seconds)
|
||||
|
||||
```swift
|
||||
var database = try DatabaseQueue(path: dbPath)
|
||||
|
||||
// Enable tracing to see SQL execution
|
||||
database.trace { print($0) }
|
||||
|
||||
// Run the slow query
|
||||
try database.read { db in
|
||||
let results = try Track.fetchAll(db) // Watch output for execution time
|
||||
}
|
||||
|
||||
// Use EXPLAIN QUERY PLAN to understand execution:
|
||||
try database.read { db in
|
||||
let plan = try String(fetching: db, sql: "EXPLAIN QUERY PLAN SELECT ...")
|
||||
print(plan)
|
||||
// Look for SCAN (slow, full table) vs SEARCH (fast, indexed)
|
||||
}
|
||||
```
|
||||
|
||||
#### Add indexes strategically
|
||||
|
||||
```swift
|
||||
// Add index on frequently queried column
|
||||
try database.write { db in
|
||||
try db.execute(sql: "CREATE INDEX idx_plays_track_id ON plays(track_id)")
|
||||
}
|
||||
```
|
||||
|
||||
#### Time cost
|
||||
- Profile: 10 min (enable trace, run query, read output)
|
||||
- Understand: 5 min (interpret EXPLAIN QUERY PLAN)
|
||||
- Fix: 5 min (add index)
|
||||
- **Total: 20 minutes** (vs 30+ min blindly trying solutions)
|
||||
|
||||
### ValueObservation Performance
|
||||
|
||||
#### When using reactive queries, know the costs
|
||||
|
||||
```swift
|
||||
// Re-evaluates query on ANY write to database
|
||||
ValueObservation.tracking { db in
|
||||
try Track.fetchAll(db)
|
||||
}.start(in: database, onError: { }, onChange: { tracks in
|
||||
// Called for every change — CPU spike!
|
||||
})
|
||||
```
|
||||
|
||||
#### Optimization patterns
|
||||
|
||||
```swift
|
||||
// Coalesce rapid updates (recommended)
|
||||
ValueObservation.tracking { db in
|
||||
try Track.fetchAll(db)
|
||||
}.removeDuplicates() // Skip duplicate results
|
||||
.debounce(for: 0.5, scheduler: DispatchQueue.main) // Batch updates
|
||||
.start(in: database, ...)
|
||||
```
|
||||
|
||||
#### Decision framework
|
||||
- Small datasets (<1000 records): Use plain `.tracking`
|
||||
- Medium datasets (1-10k records): Add `.removeDuplicates()` + `.debounce()`
|
||||
- Large datasets (10k+ records): Use explicit table dependencies or predicates
|
||||
|
||||
### Migration Versioning Guarantees
|
||||
|
||||
#### Trust GRDB's DatabaseMigrator - it prevents re-running migrations
|
||||
|
||||
```swift
|
||||
var migrator = DatabaseMigrator()
|
||||
|
||||
migrator.registerMigration("v1_initial") { db in
|
||||
try db.execute(sql: "CREATE TABLE tracks (...)")
|
||||
}
|
||||
|
||||
migrator.registerMigration("v2_add_plays") { db in
|
||||
try db.execute(sql: "CREATE TABLE plays (...)")
|
||||
}
|
||||
|
||||
// GRDB guarantees:
|
||||
// - Each migration runs exactly ONCE
|
||||
// - In order (v1, then v2)
|
||||
// - Safe to call migrate() multiple times
|
||||
try migrator.migrate(dbQueue)
|
||||
```
|
||||
|
||||
#### You don't need defensive SQL (IF NOT EXISTS)
|
||||
- GRDB tracks which migrations have run
|
||||
- Running `migrate()` twice only executes new ones
|
||||
- Over-engineering adds complexity without benefit
|
||||
|
||||
#### Trust it.
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Not using transactions for batch writes
|
||||
```swift
|
||||
for track in 50000Tracks {
|
||||
try dbQueue.write { db in try track.insert(db) } // 50k transactions!
|
||||
}
|
||||
```
|
||||
**Fix** Single transaction with batches
|
||||
|
||||
### ❌ Synchronous database access on main thread
|
||||
```swift
|
||||
let tracks = try dbQueue.read { db in try Track.fetchAll(db) } // Blocks UI
|
||||
```
|
||||
**Fix** Use async/await or dispatch to background queue
|
||||
|
||||
### ❌ Forgetting to add indexes
|
||||
```swift
|
||||
// Slow query without index
|
||||
try Track.filter(Column("genre") == "Rock").fetchAll(db)
|
||||
```
|
||||
**Fix** Create indexes on frequently queried columns
|
||||
|
||||
### ❌ N+1 queries
|
||||
```swift
|
||||
for track in tracks {
|
||||
let album = try Album.fetchOne(db, key: track.albumId) // N queries!
|
||||
}
|
||||
```
|
||||
**Fix** Use JOIN or batch fetch
|
||||
|
||||
---
|
||||
|
||||
**Targets:** iOS 13+, Swift 5.7+
|
||||
**Framework:** GRDB.swift 6.0+
|
||||
**History:** See git log for changes
|
||||
1219
skill-index/skills/axiom-memory-debugging/SKILL.md
Normal file
1219
skill-index/skills/axiom-memory-debugging/SKILL.md
Normal file
File diff suppressed because it is too large
Load diff
375
skill-index/skills/axiom-networking/LEGACY-IOS12-25.md
Normal file
375
skill-index/skills/axiom-networking/LEGACY-IOS12-25.md
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
# Legacy iOS 12-25 NWConnection Patterns
|
||||
|
||||
These patterns use NWConnection with completion handlers for apps supporting iOS 12-25. If your app targets iOS 26+, use NetworkConnection with async/await instead (see main SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2a: NWConnection with TLS (iOS 12-25)
|
||||
|
||||
**Use when** Supporting iOS 12-25, need TLS encryption, can't use async/await yet
|
||||
|
||||
**Time cost** 10-15 minutes
|
||||
|
||||
#### ✅ GOOD: NWConnection with Completion Handlers
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// Create connection with TLS
|
||||
let connection = NWConnection(
|
||||
host: NWEndpoint.Host("mail.example.com"),
|
||||
port: NWEndpoint.Port(integerLiteral: 993),
|
||||
using: .tls // TCP inferred
|
||||
)
|
||||
|
||||
// Handle connection state changes
|
||||
connection.stateUpdateHandler = { [weak self] state in
|
||||
switch state {
|
||||
case .ready:
|
||||
print("Connection established")
|
||||
self?.sendInitialData()
|
||||
case .waiting(let error):
|
||||
print("Waiting for network: \(error)")
|
||||
// Show "Waiting..." UI, don't fail immediately
|
||||
case .failed(let error):
|
||||
print("Connection failed: \(error)")
|
||||
case .cancelled:
|
||||
print("Connection cancelled")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Start connection
|
||||
connection.start(queue: .main)
|
||||
|
||||
// Send data with pacing
|
||||
func sendData() {
|
||||
let data = Data("Hello, world!".utf8)
|
||||
connection.send(content: data, completion: .contentProcessed { [weak self] error in
|
||||
if let error = error {
|
||||
print("Send error: \(error)")
|
||||
return
|
||||
}
|
||||
// contentProcessed callback = network stack consumed data
|
||||
// This is when you should send next chunk (pacing)
|
||||
self?.sendNextChunk()
|
||||
})
|
||||
}
|
||||
|
||||
// Receive exact byte count
|
||||
func receiveData() {
|
||||
connection.receive(minimumIncompleteLength: 10, maximumLength: 10) { [weak self] (data, context, isComplete, error) in
|
||||
if let error = error {
|
||||
print("Receive error: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
print("Received \(data.count) bytes")
|
||||
// Process data...
|
||||
self?.receiveData() // Continue receiving
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Key differences from NetworkConnection
|
||||
- Must use `[weak self]` in all completion handlers to prevent retain cycles
|
||||
- stateUpdateHandler receives state, not async sequence
|
||||
- send/receive use completion callbacks, not async/await
|
||||
|
||||
#### When to use
|
||||
- Supporting iOS 12-15 (70% of devices as of 2024)
|
||||
- Codebases not yet using async/await
|
||||
- Libraries needing backward compatibility
|
||||
|
||||
#### Migration to NetworkConnection (iOS 26+)
|
||||
- stateUpdateHandler → connection.states async sequence
|
||||
- Completion handlers → try await calls
|
||||
- [weak self] → No longer needed (async/await handles cancellation)
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2b: NWConnection UDP Batch (iOS 12-25)
|
||||
|
||||
**Use when** Supporting iOS 12-25, sending multiple UDP datagrams efficiently, need ~30% CPU reduction
|
||||
|
||||
**Time cost** 10-15 minutes
|
||||
|
||||
**Background** Traditional UDP sockets send one datagram per syscall. If you're sending 100 small packets, that's 100 context switches. Batching reduces this to ~1 syscall.
|
||||
|
||||
#### ❌ BAD: Individual UDP Sends (High CPU)
|
||||
```swift
|
||||
// WRONG — 100 context switches for 100 packets
|
||||
for frame in videoFrames {
|
||||
sendto(socket, frame.bytes, frame.count, 0, &addr, addrlen)
|
||||
// Each send = context switch to kernel
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ GOOD: Batched UDP Sends (30% Lower CPU)
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// UDP connection
|
||||
let connection = NWConnection(
|
||||
host: NWEndpoint.Host("stream-server.example.com"),
|
||||
port: NWEndpoint.Port(integerLiteral: 9000),
|
||||
using: .udp
|
||||
)
|
||||
|
||||
connection.stateUpdateHandler = { state in
|
||||
if case .ready = state {
|
||||
print("Ready to send UDP")
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .main)
|
||||
|
||||
// Batch sending for efficiency
|
||||
func sendVideoFrames(_ frames: [Data]) {
|
||||
connection.batch {
|
||||
for frame in frames {
|
||||
connection.send(content: frame, completion: .contentProcessed { error in
|
||||
if let error = error {
|
||||
print("Send error: \(error)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// All sends batched into ~1 syscall
|
||||
// 30% lower CPU usage vs individual sends
|
||||
}
|
||||
|
||||
// Receive UDP datagrams
|
||||
func receiveFrames() {
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] (data, context, isComplete, error) in
|
||||
if let error = error {
|
||||
print("Receive error: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
// Process video frame
|
||||
self?.displayFrame(data)
|
||||
self?.receiveFrames() // Continue receiving
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Performance characteristics
|
||||
- **Without batch** 100 datagrams = 100 syscalls = 100 context switches
|
||||
- **With batch** 100 datagrams = ~1 syscall = 1 context switch
|
||||
- **Result** ~30% lower CPU usage (measured with Instruments)
|
||||
|
||||
#### When to use
|
||||
- Real-time video/audio streaming
|
||||
- Gaming with frequent updates (player position)
|
||||
- High-frequency sensor data (IoT)
|
||||
|
||||
**WWDC 2018 demo** Live video streaming showed 30% lower CPU on receiver with user-space networking + batching
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2c: NWListener (iOS 12-25)
|
||||
|
||||
**Use when** Need to accept incoming connections, building servers or peer-to-peer apps, supporting iOS 12-25
|
||||
|
||||
**Time cost** 20-25 minutes
|
||||
|
||||
#### ❌ BAD: Manual Socket Listening
|
||||
```swift
|
||||
// WRONG — Manual socket management
|
||||
let sock = socket(AF_INET, SOCK_STREAM, 0)
|
||||
bind(sock, &addr, addrlen)
|
||||
listen(sock, 5)
|
||||
while true {
|
||||
let client = accept(sock, nil, nil) // Blocks thread
|
||||
// Handle client...
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ GOOD: NWListener with Automatic Connection Handling
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// Create listener with default parameters
|
||||
let listener = try NWListener(using: .tcp, on: 1029)
|
||||
|
||||
// Advertise Bonjour service
|
||||
listener.service = NWListener.Service(name: "MyApp", type: "_myservice._tcp")
|
||||
|
||||
// Handle service registration updates
|
||||
listener.serviceRegistrationUpdateHandler = { update in
|
||||
switch update {
|
||||
case .add(let endpoint):
|
||||
if case .service(let name, let type, let domain, _) = endpoint {
|
||||
print("Advertising as: \(name).\(type)\(domain)")
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming connections
|
||||
listener.newConnectionHandler = { [weak self] newConnection in
|
||||
print("New connection from: \(newConnection.endpoint)")
|
||||
|
||||
// Configure connection
|
||||
newConnection.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
print("Client connected")
|
||||
self?.handleClient(newConnection)
|
||||
case .failed(let error):
|
||||
print("Client connection failed: \(error)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Start handling this connection
|
||||
newConnection.start(queue: .main)
|
||||
}
|
||||
|
||||
// Handle listener state
|
||||
listener.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
print("Listener ready on port \(listener.port ?? 0)")
|
||||
case .failed(let error):
|
||||
print("Listener failed: \(error)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Start listening
|
||||
listener.start(queue: .main)
|
||||
|
||||
// Handle client data
|
||||
func handleClient(_ connection: NWConnection) {
|
||||
connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { [weak self] (data, context, isComplete, error) in
|
||||
if let error = error {
|
||||
print("Receive error: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
if let data = data {
|
||||
print("Received \(data.count) bytes")
|
||||
|
||||
// Echo back
|
||||
connection.send(content: data, completion: .contentProcessed { error in
|
||||
if let error = error {
|
||||
print("Send error: \(error)")
|
||||
}
|
||||
})
|
||||
|
||||
self?.handleClient(connection) // Continue receiving
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### When to use
|
||||
- Peer-to-peer apps (file sharing, messaging)
|
||||
- Local network services
|
||||
- Development/testing servers
|
||||
|
||||
#### Bonjour advertising
|
||||
- Automatic service discovery on local network
|
||||
- No hardcoded IPs needed
|
||||
- Works with NWBrowser for discovery
|
||||
|
||||
#### Security considerations
|
||||
- Use TLS parameters for encryption: `NWListener(using: .tls, on: port)`
|
||||
- Validate client connections before processing data
|
||||
- Set connection limits to prevent DoS
|
||||
|
||||
---
|
||||
|
||||
## Pattern 2d: Network Discovery (iOS 12-25)
|
||||
|
||||
**Use when** Discovering services on local network (Bonjour), building peer-to-peer apps, supporting iOS 12-25
|
||||
|
||||
**Time cost** 25-30 minutes
|
||||
|
||||
#### ❌ BAD: Hardcoded IP Addresses
|
||||
```swift
|
||||
// WRONG — Brittle, requires manual configuration
|
||||
let connection = NWConnection(host: "192.168.1.100", port: 9000, using: .tcp)
|
||||
// What if IP changes? What if multiple devices?
|
||||
```
|
||||
|
||||
#### ✅ GOOD: NWBrowser for Service Discovery
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// Browse for services on local network
|
||||
let browser = NWBrowser(for: .bonjour(type: "_myservice._tcp", domain: nil), using: .tcp)
|
||||
|
||||
// Handle discovered services
|
||||
browser.browseResultsChangedHandler = { results, changes in
|
||||
for result in results {
|
||||
switch result.endpoint {
|
||||
case .service(let name, let type, let domain, _):
|
||||
print("Found service: \(name).\(type)\(domain)")
|
||||
// Connect to this service
|
||||
self.connectToService(result.endpoint)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle browser state
|
||||
browser.stateUpdateHandler = { state in
|
||||
switch state {
|
||||
case .ready:
|
||||
print("Browser ready")
|
||||
case .failed(let error):
|
||||
print("Browser failed: \(error)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Start browsing
|
||||
browser.start(queue: .main)
|
||||
|
||||
// Connect to discovered service
|
||||
func connectToService(_ endpoint: NWEndpoint) {
|
||||
let connection = NWConnection(to: endpoint, using: .tcp)
|
||||
|
||||
connection.stateUpdateHandler = { state in
|
||||
if case .ready = state {
|
||||
print("Connected to service")
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .main)
|
||||
}
|
||||
```
|
||||
|
||||
#### When to use
|
||||
- Peer-to-peer discovery (AirDrop-like features)
|
||||
- Local network printers, media servers
|
||||
- Development/testing (find test servers automatically)
|
||||
|
||||
#### Performance characteristics
|
||||
- mDNS-based (multicast DNS, no central server)
|
||||
- Near-instant discovery on same subnet
|
||||
- Automatic updates when services appear/disappear
|
||||
|
||||
#### iOS 26+ alternative
|
||||
- Use NetworkBrowser with Wi-Fi Aware for peer-to-peer without infrastructure
|
||||
- See Pattern 1d in network-framework-ref skill
|
||||
|
||||
---
|
||||
|
||||
Return to [main networking skill](SKILL.md).
|
||||
235
skill-index/skills/axiom-networking/MIGRATION.md
Normal file
235
skill-index/skills/axiom-networking/MIGRATION.md
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
# Network Framework Migration Guides
|
||||
|
||||
## Migration 1: From BSD Sockets to NWConnection
|
||||
|
||||
### Migration mapping
|
||||
|
||||
| BSD Sockets | NWConnection | Notes |
|
||||
|-------------|--------------|-------|
|
||||
| `socket() + connect()` | `NWConnection(host:port:using:) + start()` | Non-blocking by default |
|
||||
| `send() / sendto()` | `connection.send(content:completion:)` | Async, returns immediately |
|
||||
| `recv() / recvfrom()` | `connection.receive(minimumIncompleteLength:maximumLength:completion:)` | Async, returns immediately |
|
||||
| `bind() + listen()` | `NWListener(using:on:)` | Automatic port binding |
|
||||
| `accept()` | `listener.newConnectionHandler` | Callback for each connection |
|
||||
| `getaddrinfo()` | Let NWConnection handle DNS | Smart resolution with racing |
|
||||
| `SCNetworkReachability` | `connection.stateUpdateHandler` waiting state | No race conditions |
|
||||
| `setsockopt()` | `NWParameters` configuration | Type-safe options |
|
||||
|
||||
### Example migration
|
||||
|
||||
#### Before (BSD Sockets)
|
||||
```c
|
||||
// BEFORE — Blocking, manual DNS, error-prone
|
||||
var hints = addrinfo()
|
||||
hints.ai_family = AF_INET
|
||||
hints.ai_socktype = SOCK_STREAM
|
||||
|
||||
var results: UnsafeMutablePointer<addrinfo>?
|
||||
getaddrinfo("example.com", "443", &hints, &results)
|
||||
|
||||
let sock = socket(results.pointee.ai_family, results.pointee.ai_socktype, 0)
|
||||
connect(sock, results.pointee.ai_addr, results.pointee.ai_addrlen) // BLOCKS
|
||||
|
||||
let data = "Hello".data(using: .utf8)!
|
||||
data.withUnsafeBytes { ptr in
|
||||
send(sock, ptr.baseAddress, data.count, 0)
|
||||
}
|
||||
```
|
||||
|
||||
#### After (NWConnection)
|
||||
```swift
|
||||
// AFTER — Non-blocking, automatic DNS, type-safe
|
||||
let connection = NWConnection(
|
||||
host: NWEndpoint.Host("example.com"),
|
||||
port: NWEndpoint.Port(integerLiteral: 443),
|
||||
using: .tls
|
||||
)
|
||||
|
||||
connection.stateUpdateHandler = { state in
|
||||
if case .ready = state {
|
||||
let data = Data("Hello".utf8)
|
||||
connection.send(content: data, completion: .contentProcessed { error in
|
||||
if let error = error {
|
||||
print("Send failed: \(error)")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .main)
|
||||
```
|
||||
|
||||
### Benefits
|
||||
- 20 lines → 10 lines
|
||||
- No manual DNS, no blocking, no unsafe pointers
|
||||
- Automatic Happy Eyeballs, proxy support, WiFi Assist
|
||||
|
||||
---
|
||||
|
||||
## Migration 2: From NWConnection to NetworkConnection (iOS 26+)
|
||||
|
||||
### Why migrate
|
||||
- Async/await eliminates callback hell
|
||||
- TLV framing and Coder protocol built-in
|
||||
- No [weak self] needed (async/await handles cancellation)
|
||||
- State monitoring via async sequences
|
||||
|
||||
### Migration mapping
|
||||
|
||||
| NWConnection (iOS 12-25) | NetworkConnection (iOS 26+) | Notes |
|
||||
|-------------------------|----------------------------|-------|
|
||||
| `connection.stateUpdateHandler = { state in }` | `for await state in connection.states { }` | Async sequence |
|
||||
| `connection.send(content:completion:)` | `try await connection.send(content)` | Suspending function |
|
||||
| `connection.receive(minimumIncompleteLength:maximumLength:completion:)` | `try await connection.receive(exactly:)` | Suspending function |
|
||||
| Manual JSON encode/decode | `Coder(MyType.self, using: .json)` | Built-in Codable support |
|
||||
| Custom framer | `TLV { TLS() }` | Built-in Type-Length-Value |
|
||||
| `[weak self]` everywhere | No `[weak self]` needed | Task cancellation automatic |
|
||||
|
||||
### Example migration
|
||||
|
||||
#### Before (NWConnection)
|
||||
```swift
|
||||
// BEFORE — Completion handlers, manual memory management
|
||||
let connection = NWConnection(host: "example.com", port: 443, using: .tls)
|
||||
|
||||
connection.stateUpdateHandler = { [weak self] state in
|
||||
switch state {
|
||||
case .ready:
|
||||
self?.sendData()
|
||||
case .waiting(let error):
|
||||
print("Waiting: \(error)")
|
||||
case .failed(let error):
|
||||
print("Failed: \(error)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .main)
|
||||
|
||||
func sendData() {
|
||||
let data = Data("Hello".utf8)
|
||||
connection.send(content: data, completion: .contentProcessed { [weak self] error in
|
||||
if let error = error {
|
||||
print("Send error: \(error)")
|
||||
return
|
||||
}
|
||||
self?.receiveData()
|
||||
})
|
||||
}
|
||||
|
||||
func receiveData() {
|
||||
connection.receive(minimumIncompleteLength: 10, maximumLength: 10) { [weak self] (data, context, isComplete, error) in
|
||||
if let error = error {
|
||||
print("Receive error: \(error)")
|
||||
return
|
||||
}
|
||||
if let data = data {
|
||||
print("Received: \(data)")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After (NetworkConnection)
|
||||
```swift
|
||||
// AFTER — Async/await, automatic memory management
|
||||
let connection = NetworkConnection(
|
||||
to: .hostPort(host: "example.com", port: 443)
|
||||
) {
|
||||
TLS()
|
||||
}
|
||||
|
||||
// Monitor states in background task
|
||||
Task {
|
||||
for await state in connection.states {
|
||||
switch state {
|
||||
case .preparing:
|
||||
print("Connecting...")
|
||||
case .ready:
|
||||
print("Ready")
|
||||
case .waiting(let error):
|
||||
print("Waiting: \(error)")
|
||||
case .failed(let error):
|
||||
print("Failed: \(error)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send and receive with async/await
|
||||
func sendAndReceive() async throws {
|
||||
let data = Data("Hello".utf8)
|
||||
try await connection.send(data)
|
||||
|
||||
let received = try await connection.receive(exactly: 10).content
|
||||
print("Received: \(received)")
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits
|
||||
- 30 lines → 15 lines
|
||||
- No callback nesting, no [weak self]
|
||||
- Errors propagate naturally with throws
|
||||
- Automatic cancellation on Task exit
|
||||
|
||||
---
|
||||
|
||||
## Migration 3: From URLSession StreamTask to NetworkConnection
|
||||
|
||||
### When to migrate
|
||||
- Need UDP (StreamTask only supports TCP)
|
||||
- Need custom protocols beyond TCP/TLS
|
||||
- Need low-level control (packet pacing, ECN, service class)
|
||||
|
||||
### When to STAY with URLSession
|
||||
- Doing HTTP/HTTPS (URLSession optimized for this)
|
||||
- Need WebSocket support
|
||||
- Need built-in caching, cookie handling
|
||||
|
||||
### Example migration
|
||||
|
||||
#### Before (URLSession StreamTask)
|
||||
```swift
|
||||
// BEFORE — URLSession for TCP/TLS stream
|
||||
let task = URLSession.shared.streamTask(withHostName: "example.com", port: 443)
|
||||
|
||||
task.resume()
|
||||
|
||||
task.write(Data("Hello".utf8), timeout: 10) { error in
|
||||
if let error = error {
|
||||
print("Write error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
task.readData(ofMinLength: 10, maxLength: 10, timeout: 10) { data, atEOF, error in
|
||||
if let error = error {
|
||||
print("Read error: \(error)")
|
||||
return
|
||||
}
|
||||
if let data = data {
|
||||
print("Received: \(data)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### After (NetworkConnection)
|
||||
```swift
|
||||
// AFTER — NetworkConnection for TCP/TLS
|
||||
let connection = NetworkConnection(
|
||||
to: .hostPort(host: "example.com", port: 443)
|
||||
) {
|
||||
TLS()
|
||||
}
|
||||
|
||||
func sendAndReceive() async throws {
|
||||
try await connection.send(Data("Hello".utf8))
|
||||
let data = try await connection.receive(exactly: 10).content
|
||||
print("Received: \(data)")
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Return to [main networking skill](SKILL.md).
|
||||
20
skill-index/skills/axiom-networking/REFERENCES.md
Normal file
20
skill-index/skills/axiom-networking/REFERENCES.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Networking References
|
||||
|
||||
## WWDC Sessions
|
||||
|
||||
- **WWDC 2018-715** — Introducing Network.framework: User-space networking demo (30% CPU reduction), deprecation of CFSocket/NSStream/SCNetworkReachability, smart connection establishment, mobility support
|
||||
|
||||
- **WWDC 2025-250** — Use structured concurrency with Network framework: NetworkConnection with async/await (iOS 26+), TLV framing and Coder protocol, NetworkListener and NetworkBrowser, Wi-Fi Aware peer-to-peer discovery
|
||||
|
||||
## Apple Documentation
|
||||
|
||||
- [Network Framework Documentation](https://developer.apple.com/documentation/network)
|
||||
- [NWConnection](https://developer.apple.com/documentation/network/nwconnection)
|
||||
- [NetworkConnection (iOS 26+)](https://developer.apple.com/documentation/Network/NetworkConnection)
|
||||
- [Building a Custom Peer-to-Peer Protocol](https://developer.apple.com/documentation/Network/building-a-custom-peer-to-peer-protocol)
|
||||
|
||||
## Related Axiom Skills
|
||||
|
||||
- **networking-diag** — Systematic troubleshooting for connection timeouts, TLS failures, data not arriving, performance issues
|
||||
- **network-framework-ref** — Comprehensive API reference with all 12 WWDC 2025 code examples, migration strategies, testing checklists
|
||||
- **swift-concurrency** — Async/await patterns, @MainActor usage, Task cancellation (needed for NetworkConnection)
|
||||
978
skill-index/skills/axiom-networking/SKILL.md
Normal file
978
skill-index/skills/axiom-networking/SKILL.md
Normal file
|
|
@ -0,0 +1,978 @@
|
|||
---
|
||||
name: axiom-networking
|
||||
description: Use when implementing Network.framework connections, debugging connection failures, migrating from sockets/URLSession streams, or adopting structured concurrency networking patterns - prevents deprecated API usage, reachability anti-patterns, and thread-safety violations with iOS 12-26+ APIs
|
||||
skill_type: discipline
|
||||
version: 1.0.0
|
||||
last_updated: 2025-12-02
|
||||
apple_platforms: iOS 12+ (NWConnection), iOS 26+ (NetworkConnection)
|
||||
---
|
||||
|
||||
# Network.framework Networking
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use when:
|
||||
- Implementing UDP/TCP connections for gaming, streaming, or messaging apps
|
||||
- Migrating from BSD sockets, CFSocket, NSStream, or SCNetworkReachability
|
||||
- Debugging connection timeouts or TLS handshake failures
|
||||
- Supporting network transitions (WiFi ↔ cellular) gracefully
|
||||
- Adopting structured concurrency networking patterns (iOS 26+)
|
||||
- Implementing custom protocols over TLS/QUIC
|
||||
- Requesting code review of networking implementation before shipping
|
||||
|
||||
#### Related Skills
|
||||
- Use `axiom-networking-diag` for systematic troubleshooting of connection failures, timeouts, and performance issues
|
||||
- Use `axiom-network-framework-ref` for comprehensive API reference with all WWDC examples
|
||||
|
||||
## Example Prompts
|
||||
|
||||
#### 1. "How do I migrate from SCNetworkReachability? My app checks connectivity before connecting."
|
||||
#### 2. "My connection times out after 60 seconds. How do I debug this?"
|
||||
#### 3. "Should I use NWConnection or NetworkConnection? What's the difference?"
|
||||
|
||||
---
|
||||
|
||||
## Red Flags — Anti-Patterns to Prevent
|
||||
|
||||
If you're doing ANY of these, STOP and use the patterns in this skill:
|
||||
|
||||
### ❌ CRITICAL — Never Do These
|
||||
|
||||
#### 1. Using SCNetworkReachability to check connectivity before connecting
|
||||
```swift
|
||||
// ❌ WRONG — Race condition
|
||||
if SCNetworkReachabilityGetFlags(reachability, &flags) {
|
||||
connection.start() // Network may change between check and start
|
||||
}
|
||||
```
|
||||
**Why this fails** Network state changes between reachability check and connect(). You miss Network.framework's smart connection establishment (Happy Eyeballs, proxy handling, WiFi Assist). Apple deprecated this API in 2018.
|
||||
|
||||
#### 2. Blocking socket operations on main thread
|
||||
```swift
|
||||
// ❌ WRONG — Guaranteed ANR (Application Not Responding)
|
||||
let socket = socket(AF_INET, SOCK_STREAM, 0)
|
||||
connect(socket, &addr, addrlen) // Blocks main thread
|
||||
```
|
||||
**Why this fails** Main thread hang → frozen UI → App Store rejection for responsiveness. Even "quick" connects take 200-500ms.
|
||||
|
||||
#### 3. Manual DNS resolution with getaddrinfo
|
||||
```swift
|
||||
// ❌ WRONG — Misses Happy Eyeballs, proxies, VPN
|
||||
var hints = addrinfo(...)
|
||||
getaddrinfo("example.com", "443", &hints, &results)
|
||||
// Now manually try each address...
|
||||
```
|
||||
**Why this fails** You reimplement 10+ years of Apple's connection logic poorly. Misses IPv4/IPv6 racing, proxy evaluation, VPN detection.
|
||||
|
||||
#### 4. Hardcoded IP addresses instead of hostnames
|
||||
```swift
|
||||
// ❌ WRONG — Breaks proxy/VPN compatibility
|
||||
let host = "192.168.1.1" // or any IP literal
|
||||
```
|
||||
**Why this fails** Proxy auto-configuration (PAC) needs hostname to evaluate rules. VPNs can't route properly. DNS-based load balancing broken.
|
||||
|
||||
#### 5. Ignoring waiting state — not handling lack of connectivity
|
||||
```swift
|
||||
// ❌ WRONG — Poor UX
|
||||
connection.stateUpdateHandler = { state in
|
||||
if case .ready = state {
|
||||
// Handle ready
|
||||
}
|
||||
// Missing: .waiting case
|
||||
}
|
||||
```
|
||||
**Why this fails** User sees "Connection failed" in Airplane Mode instead of "Waiting for network." No automatic retry when WiFi returns.
|
||||
|
||||
#### 6. Not using [weak self] in NWConnection completion handlers
|
||||
```swift
|
||||
// ❌ WRONG — Memory leak
|
||||
connection.send(content: data, completion: .contentProcessed { error in
|
||||
self.handleSend(error) // Retain cycle: connection → handler → self → connection
|
||||
})
|
||||
```
|
||||
**Why this fails** Connection retains completion handler, handler captures self strongly, self retains connection → memory leak.
|
||||
|
||||
#### 7. Mixing async/await and completion handlers in NetworkConnection (iOS 26+)
|
||||
```swift
|
||||
// ❌ WRONG — Structured concurrency violation
|
||||
Task {
|
||||
let connection = NetworkConnection(...)
|
||||
connection.send(data) // async/await
|
||||
connection.stateUpdateHandler = { ... } // completion handler — don't mix
|
||||
}
|
||||
```
|
||||
**Why this fails** NetworkConnection designed for pure async/await. Mixing paradigms creates difficult error propagation and cancellation issues.
|
||||
|
||||
#### 8. Not supporting network transitions
|
||||
```swift
|
||||
// ❌ WRONG — Connection fails on WiFi → cellular transition
|
||||
// No viabilityUpdateHandler, no betterPathUpdateHandler
|
||||
// User walks out of building → connection dies
|
||||
```
|
||||
**Why this fails** Modern apps must handle network changes gracefully. 40% of connection failures happen during network transitions.
|
||||
|
||||
---
|
||||
|
||||
## Mandatory First Steps
|
||||
|
||||
**ALWAYS complete these steps** before writing any networking code:
|
||||
|
||||
```swift
|
||||
// Step 1: Identify your use case
|
||||
// Record: "UDP gaming" vs "TLS messaging" vs "Custom protocol over QUIC"
|
||||
// Ask: What data am I sending? Real-time? Reliable delivery needed?
|
||||
|
||||
// Step 2: Check if URLSession is sufficient
|
||||
// URLSession handles: HTTP, HTTPS, WebSocket, TCP/TLS streams (via StreamTask)
|
||||
// Network.framework handles: UDP, custom protocols, low-level control, peer-to-peer
|
||||
|
||||
// If HTTP/HTTPS/WebSocket → STOP, use URLSession instead
|
||||
// Example:
|
||||
URLSession.shared.dataTask(with: url) { ... } // ✅ Correct for HTTP
|
||||
|
||||
// Step 3: Choose API version based on deployment target
|
||||
if #available(iOS 26, *) {
|
||||
// Use NetworkConnection (structured concurrency, async/await)
|
||||
// TLV framing built-in, Coder protocol for Codable types
|
||||
} else {
|
||||
// Use NWConnection (completion handlers)
|
||||
// Manual framing or custom framers
|
||||
}
|
||||
|
||||
// Step 4: Verify you're NOT using deprecated APIs
|
||||
// Search your codebase for these:
|
||||
// - SCNetworkReachability → Use connection waiting state
|
||||
// - CFSocket → Use NWConnection
|
||||
// - NSStream, CFStream → Use NWConnection
|
||||
// - NSNetService → Use NWBrowser or NetworkBrowser
|
||||
// - getaddrinfo → Let Network.framework handle DNS
|
||||
|
||||
// To search:
|
||||
// grep -rn "SCNetworkReachability\|CFSocket\|NSStream\|getaddrinfo" .
|
||||
```
|
||||
|
||||
#### What this tells you
|
||||
- If HTTP/HTTPS: Use URLSession, not Network.framework
|
||||
- If iOS 26+ deployment: Use NetworkConnection with async/await
|
||||
- If iOS 12-25 support needed: Use NWConnection with completion handlers
|
||||
- If any deprecated API found: Must migrate before shipping (App Store review concern)
|
||||
|
||||
---
|
||||
|
||||
## Decision Tree
|
||||
|
||||
Use this to select the correct pattern in 2 minutes:
|
||||
|
||||
```
|
||||
Need networking?
|
||||
├─ HTTP, HTTPS, or WebSocket?
|
||||
│ └─ YES → Use URLSession (NOT Network.framework)
|
||||
│ ✅ URLSession.shared.dataTask(with: url)
|
||||
│ ✅ URLSession.webSocketTask(with: url)
|
||||
│ ✅ URLSession.streamTask(withHostName:port:) for TCP/TLS
|
||||
│
|
||||
├─ iOS 26+ and can use structured concurrency?
|
||||
│ └─ YES → NetworkConnection path (async/await)
|
||||
│ ├─ TCP with TLS security?
|
||||
│ │ └─ Pattern 1a: NetworkConnection + TLS
|
||||
│ │ Time: 10-15 minutes
|
||||
│ │
|
||||
│ ├─ UDP for gaming/streaming?
|
||||
│ │ └─ Pattern 1b: NetworkConnection + UDP
|
||||
│ │ Time: 10-15 minutes
|
||||
│ │
|
||||
│ ├─ Need message boundaries (framing)?
|
||||
│ │ └─ Pattern 1c: TLV Framing
|
||||
│ │ Type-Length-Value for mixed message types
|
||||
│ │ Time: 15-20 minutes
|
||||
│ │
|
||||
│ └─ Send/receive Codable objects directly?
|
||||
│ └─ Pattern 1d: Coder Protocol
|
||||
│ No manual JSON encoding needed
|
||||
│ Time: 10-15 minutes
|
||||
│
|
||||
└─ iOS 12-25 or need completion handlers?
|
||||
└─ YES → NWConnection path (callbacks)
|
||||
├─ TCP with TLS security?
|
||||
│ └─ Pattern 2a: NWConnection + TLS
|
||||
│ stateUpdateHandler, completion-based send/receive
|
||||
│ Time: 15-20 minutes
|
||||
│
|
||||
├─ UDP streaming with batching?
|
||||
│ └─ Pattern 2b: NWConnection + UDP Batch
|
||||
│ connection.batch for 30% CPU reduction
|
||||
│ Time: 10-15 minutes
|
||||
│
|
||||
├─ Listening for incoming connections?
|
||||
│ └─ Pattern 2c: NWListener
|
||||
│ Accept inbound connections, newConnectionHandler
|
||||
│ Time: 20-25 minutes
|
||||
│
|
||||
└─ Network discovery (Bonjour)?
|
||||
└─ Pattern 2d: NWBrowser
|
||||
Discover services on local network
|
||||
Time: 25-30 minutes
|
||||
```
|
||||
|
||||
#### Quick selection guide
|
||||
- Gaming (low latency, some loss OK) → UDP patterns (1b or 2b)
|
||||
- Messaging (reliable, ordered) → TLS patterns (1a or 2a)
|
||||
- Mixed message types → TLV or Coder (1c or 1d)
|
||||
- Peer-to-peer → Discovery patterns (2d) + incoming (2c)
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1a: NetworkConnection with TLS (iOS 26+)
|
||||
|
||||
**Use when** iOS 26+ deployment, need reliable TCP with TLS security, want async/await
|
||||
|
||||
**Time cost** 10-15 minutes
|
||||
|
||||
#### ❌ BAD: Manual DNS, Blocking Socket
|
||||
```swift
|
||||
// WRONG — Don't do this
|
||||
var hints = addrinfo(...)
|
||||
getaddrinfo("www.example.com", "1029", &hints, &results)
|
||||
let sock = socket(AF_INET, SOCK_STREAM, 0)
|
||||
connect(sock, results.pointee.ai_addr, results.pointee.ai_addrlen) // Blocks!
|
||||
```
|
||||
|
||||
#### ✅ GOOD: NetworkConnection with Declarative Stack
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// Basic connection with TLS
|
||||
let connection = NetworkConnection(
|
||||
to: .hostPort(host: "www.example.com", port: 1029)
|
||||
) {
|
||||
TLS() // TCP and IP inferred automatically
|
||||
}
|
||||
|
||||
// Send and receive with async/await
|
||||
public func sendAndReceiveWithTLS() async throws {
|
||||
let outgoingData = Data("Hello, world!".utf8)
|
||||
try await connection.send(outgoingData)
|
||||
|
||||
let incomingData = try await connection.receive(exactly: 98).content
|
||||
print("Received data: \(incomingData)")
|
||||
}
|
||||
|
||||
// Optional: Monitor connection state for UI updates
|
||||
Task {
|
||||
for await state in connection.states {
|
||||
switch state {
|
||||
case .preparing:
|
||||
print("Establishing connection...")
|
||||
case .ready:
|
||||
print("Connected!")
|
||||
case .waiting(let error):
|
||||
print("Waiting for network: \(error)")
|
||||
case .failed(let error):
|
||||
print("Connection failed: \(error)")
|
||||
case .cancelled:
|
||||
print("Connection cancelled")
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom parameters for low data mode
|
||||
|
||||
```swift
|
||||
let connection = NetworkConnection(
|
||||
to: .hostPort(host: "www.example.com", port: 1029),
|
||||
using: .parameters {
|
||||
TLS {
|
||||
TCP {
|
||||
IP()
|
||||
.fragmentationEnabled(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
.constrainedPathsProhibited(true) // Don't use cellular in low data mode
|
||||
)
|
||||
```
|
||||
|
||||
#### When to use
|
||||
- Secure messaging, email protocols (IMAP, SMTP)
|
||||
- Custom protocols requiring encryption
|
||||
- APIs using non-HTTP protocols
|
||||
|
||||
#### Performance characteristics
|
||||
- Smart connection establishment: Happy Eyeballs (IPv4/IPv6 racing), proxy evaluation, VPN detection
|
||||
- TLS 1.3 by default (faster handshake)
|
||||
- User-space networking: ~30% lower CPU usage vs sockets
|
||||
|
||||
#### Debugging
|
||||
- Enable logging: `-NWLoggingEnabled 1 -NWConnectionLoggingEnabled 1`
|
||||
- Check connection.states async sequence for state transitions
|
||||
- Test on real device with Airplane Mode toggle
|
||||
|
||||
---
|
||||
|
||||
### Pattern 1b: NetworkConnection UDP (iOS 26+)
|
||||
|
||||
**Use when** iOS 26+ deployment, need UDP datagrams for gaming or real-time streaming, want async/await
|
||||
|
||||
**Time cost** 10-15 minutes
|
||||
|
||||
#### ❌ BAD: Blocking UDP Socket
|
||||
```swift
|
||||
// WRONG — Don't do this
|
||||
let sock = socket(AF_INET, SOCK_DGRAM, 0)
|
||||
let sent = sendto(sock, buffer, length, 0, &addr, addrlen)
|
||||
// Blocks, no batching, axiom-high CPU overhead
|
||||
```
|
||||
|
||||
#### ✅ GOOD: NetworkConnection with UDP
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// UDP connection for real-time data
|
||||
let connection = NetworkConnection(
|
||||
to: .hostPort(host: "game-server.example.com", port: 9000)
|
||||
) {
|
||||
UDP()
|
||||
}
|
||||
|
||||
// Send game state update
|
||||
public func sendGameUpdate() async throws {
|
||||
let gameState = Data("player_position:100,50".utf8)
|
||||
try await connection.send(gameState)
|
||||
}
|
||||
|
||||
// Receive game updates
|
||||
public func receiveGameUpdates() async throws {
|
||||
while true {
|
||||
let (data, _) = try await connection.receive()
|
||||
processGameState(data)
|
||||
}
|
||||
}
|
||||
|
||||
// Batch multiple datagrams for efficiency (30% CPU reduction)
|
||||
public func sendMultipleUpdates(_ updates: [Data]) async throws {
|
||||
for update in updates {
|
||||
try await connection.send(update)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### When to use
|
||||
- Real-time gaming (player position, game state)
|
||||
- Live streaming (video/audio frames where some loss is acceptable)
|
||||
- IoT telemetry (sensor data)
|
||||
|
||||
#### Performance characteristics
|
||||
- User-space networking: ~30% lower CPU vs sockets
|
||||
- Batching multiple sends reduces context switches
|
||||
- ECN (Explicit Congestion Notification) enabled automatically
|
||||
|
||||
#### Debugging
|
||||
- Use Instruments Network template to profile datagram throughput
|
||||
- Check for packet loss with receive timeouts
|
||||
- Test on cellular network (higher latency/loss)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 1c: TLV Framing (iOS 26+)
|
||||
|
||||
**Use when** Need message boundaries on stream protocols (TCP/TLS), have mixed message types, want type-safe message handling
|
||||
|
||||
**Time cost** 15-20 minutes
|
||||
|
||||
**Background** Stream protocols (TCP/TLS) don't preserve message boundaries. If you send 3 chunks, receiver might get them 1 byte at a time, or all at once. TLV (Type-Length-Value) solves this by encoding each message with its type and length.
|
||||
|
||||
#### ❌ BAD: Manual Length Prefix Parsing
|
||||
```swift
|
||||
// WRONG — Error-prone, boilerplate-heavy
|
||||
let lengthData = try await connection.receive(exactly: 4).content
|
||||
let length = lengthData.withUnsafeBytes { $0.load(as: UInt32.self) }
|
||||
let messageData = try await connection.receive(exactly: Int(length)).content
|
||||
// Now decode manually...
|
||||
```
|
||||
|
||||
#### ✅ GOOD: TLV Framing with Type Safety
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// Define your message types
|
||||
enum GameMessage: Int {
|
||||
case selectedCharacter = 0
|
||||
case move = 1
|
||||
}
|
||||
|
||||
struct GameCharacter: Codable {
|
||||
let character: String
|
||||
}
|
||||
|
||||
struct GameMove: Codable {
|
||||
let row: Int
|
||||
let column: Int
|
||||
}
|
||||
|
||||
// Connection with TLV framing
|
||||
let connection = NetworkConnection(
|
||||
to: .hostPort(host: "www.example.com", port: 1029)
|
||||
) {
|
||||
TLV {
|
||||
TLS()
|
||||
}
|
||||
}
|
||||
|
||||
// Send typed messages
|
||||
public func sendWithTLV() async throws {
|
||||
let characterData = try JSONEncoder().encode(GameCharacter(character: "🐨"))
|
||||
try await connection.send(characterData, type: GameMessage.selectedCharacter.rawValue)
|
||||
}
|
||||
|
||||
// Receive typed messages
|
||||
public func receiveWithTLV() async throws {
|
||||
let (incomingData, metadata) = try await connection.receive()
|
||||
|
||||
switch GameMessage(rawValue: metadata.type) {
|
||||
case .selectedCharacter:
|
||||
let character = try JSONDecoder().decode(GameCharacter.self, from: incomingData)
|
||||
print("Character selected: \(character)")
|
||||
case .move:
|
||||
let move = try JSONDecoder().decode(GameMove.self, from: incomingData)
|
||||
print("Move: row=\(move.row), column=\(move.column)")
|
||||
case .none:
|
||||
print("Unknown message type: \(metadata.type)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### When to use
|
||||
- Mixed message types in same connection (chat + presence + typing indicators)
|
||||
- Existing protocols using TLV (many custom protocols)
|
||||
- Need message boundaries without heavy framing overhead
|
||||
|
||||
#### How it works
|
||||
- Type: UInt32 message identifier (your enum raw value)
|
||||
- Length: UInt32 message size (automatic)
|
||||
- Value: Actual message bytes
|
||||
|
||||
#### Performance characteristics
|
||||
- Minimal overhead: 8 bytes per message (type + length)
|
||||
- No manual parsing: Framework handles framing
|
||||
- Type-safe: Compiler catches message type errors
|
||||
|
||||
---
|
||||
|
||||
### Pattern 1d: Coder Protocol (iOS 26+)
|
||||
|
||||
**Use when** Sending/receiving Codable types, want to eliminate JSON boilerplate, need type-safe message handling
|
||||
|
||||
**Time cost** 10-15 minutes
|
||||
|
||||
**Background** Most apps manually encode Codable types to JSON, send bytes, receive bytes, decode JSON. Coder protocol eliminates this boilerplate by handling serialization automatically.
|
||||
|
||||
#### ❌ BAD: Manual JSON Encoding/Decoding
|
||||
```swift
|
||||
// WRONG — Boilerplate-heavy, error-prone
|
||||
let encoder = JSONEncoder()
|
||||
let data = try encoder.encode(message)
|
||||
try await connection.send(data)
|
||||
|
||||
let receivedData = try await connection.receive().content
|
||||
let decoder = JSONDecoder()
|
||||
let message = try decoder.decode(GameMessage.self, from: receivedData)
|
||||
```
|
||||
|
||||
#### ✅ GOOD: Coder Protocol for Direct Codable Send/Receive
|
||||
|
||||
```swift
|
||||
import Network
|
||||
|
||||
// Define message types as Codable enum
|
||||
enum GameMessage: Codable {
|
||||
case selectedCharacter(String)
|
||||
case move(row: Int, column: Int)
|
||||
}
|
||||
|
||||
// Connection with Coder protocol
|
||||
let connection = NetworkConnection(
|
||||
to: .hostPort(host: "www.example.com", port: 1029)
|
||||
) {
|
||||
Coder(GameMessage.self, using: .json) {
|
||||
TLS()
|
||||
}
|
||||
}
|
||||
|
||||
// Send Codable types directly
|
||||
public func sendWithCoder() async throws {
|
||||
let selectedCharacter: GameMessage = .selectedCharacter("🐨")
|
||||
try await connection.send(selectedCharacter) // No encoding needed!
|
||||
}
|
||||
|
||||
// Receive Codable types directly
|
||||
public func receiveWithCoder() async throws {
|
||||
let gameMessage = try await connection.receive().content // Returns GameMessage!
|
||||
|
||||
switch gameMessage {
|
||||
case .selectedCharacter(let character):
|
||||
print("Character selected: \(character)")
|
||||
case .move(let row, let column):
|
||||
print("Move: (\(row), \(column))")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Supported formats
|
||||
- `.json` — JSON encoding (most common, human-readable)
|
||||
- `.propertyList` — Property list encoding (smaller, faster)
|
||||
|
||||
#### When to use
|
||||
- App-to-app communication (you control both ends)
|
||||
- Prototyping (fastest time to working code)
|
||||
- Type-safe protocols (compiler catches message structure changes)
|
||||
|
||||
#### When NOT to use
|
||||
- Interoperating with non-Swift servers
|
||||
- Need custom wire format
|
||||
- Performance-critical (prefer TLV with manual encoding for control)
|
||||
|
||||
#### Benefits
|
||||
- No JSON boilerplate: ~50 lines → ~10 lines
|
||||
- Type-safe: Compiler catches message structure changes
|
||||
- Automatic framing: Handles message boundaries
|
||||
|
||||
---
|
||||
|
||||
## Legacy iOS 12-25 Patterns
|
||||
|
||||
For apps supporting iOS 12-25 that can't use async/await yet, see [LEGACY-IOS12-25.md](LEGACY-IOS12-25.md):
|
||||
- Pattern 2a: NWConnection with TLS (completion handlers)
|
||||
- Pattern 2b: NWConnection UDP Batch (30% CPU reduction)
|
||||
- Pattern 2c: NWListener (accepting connections, Bonjour)
|
||||
- Pattern 2d: Network Discovery (NWBrowser for service discovery)
|
||||
|
||||
|
||||
## Pressure Scenarios
|
||||
|
||||
### Scenario 1: Reachability Race Condition Under App Store Deadline
|
||||
|
||||
#### Context
|
||||
|
||||
You're 3 days from App Store submission. QA reports connection failures on cellular networks (15% failure rate). Your PM reviews the code and suggests: "Just add a reachability check before connecting. If there's no network, show an error immediately instead of timing out."
|
||||
|
||||
#### Pressure signals
|
||||
- ⏰ **Deadline pressure** "App Store deadline is Friday. We need this fixed by EOD Wednesday."
|
||||
- 👔 **Authority pressure** PM (non-technical) suggesting specific implementation
|
||||
- 💸 **Sunk cost** Already spent 2 hours debugging connection logs, found nothing obvious
|
||||
- 📊 **Customer impact** "15% of users affected, mostly on cellular"
|
||||
|
||||
#### Rationalization trap
|
||||
|
||||
*"SCNetworkReachability is Apple's API, it must be correct. I've seen it in Stack Overflow answers with 500+ upvotes. Adding a quick reachability check will fix the issue today, and I can refactor it properly after launch. The deadline is more important than perfect code right now."*
|
||||
|
||||
#### Why this fails
|
||||
|
||||
1. **Race condition** Network state changes between reachability check and connection start. You check "WiFi available" at 10:00:00.000, but WiFi disconnects at 10:00:00.050, then you call connection.start() at 10:00:00.100. Connection fails, but reachability said it was available.
|
||||
|
||||
2. **Misses smart connection establishment** Network.framework tries multiple strategies (IPv4, IPv6, proxies, WiFi Assist fallback to cellular). SCNetworkReachability gives you "yes/no" but doesn't tell you which strategy will work.
|
||||
|
||||
3. **Deprecated API** Apple explicitly deprecated SCNetworkReachability in WWDC 2018. App Store Review may flag this as using legacy APIs.
|
||||
|
||||
4. **Doesn't solve actual problem** 15% cellular failures likely caused by not handling waiting state, not by absence of reachability check.
|
||||
|
||||
#### MANDATORY response
|
||||
|
||||
```swift
|
||||
// ❌ NEVER check reachability before connecting
|
||||
/*
|
||||
if SCNetworkReachabilityGetFlags(reachability, &flags) {
|
||||
if flags.contains(.reachable) {
|
||||
connection.start()
|
||||
} else {
|
||||
showError("No network") // RACE CONDITION
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// ✅ ALWAYS let Network.framework handle waiting state
|
||||
let connection = NWConnection(
|
||||
host: NWEndpoint.Host("api.example.com"),
|
||||
port: NWEndpoint.Port(integerLiteral: 443),
|
||||
using: .tls
|
||||
)
|
||||
|
||||
connection.stateUpdateHandler = { [weak self] state in
|
||||
switch state {
|
||||
case .preparing:
|
||||
// Show: "Connecting..."
|
||||
self?.showStatus("Connecting...")
|
||||
|
||||
case .ready:
|
||||
// Connection established
|
||||
self?.hideStatus()
|
||||
self?.sendRequest()
|
||||
|
||||
case .waiting(let error):
|
||||
// CRITICAL: Don't fail here, show "Waiting for network"
|
||||
// Network.framework will automatically retry when network returns
|
||||
print("Waiting for network: \(error)")
|
||||
self?.showStatus("Waiting for network...")
|
||||
// User walks out of elevator → WiFi returns → automatic retry
|
||||
|
||||
case .failed(let error):
|
||||
// Only fail after framework exhausts all options
|
||||
// (tried IPv4, IPv6, proxies, WiFi Assist, waited for network)
|
||||
print("Connection failed: \(error)")
|
||||
self?.showError("Connection failed. Please check your network.")
|
||||
|
||||
case .cancelled:
|
||||
self?.hideStatus()
|
||||
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .main)
|
||||
```
|
||||
|
||||
#### Professional push-back template
|
||||
|
||||
*"I understand the deadline pressure. However, adding SCNetworkReachability will create a race condition that will make the 15% failure rate worse, not better. Apple deprecated this API in 2018 specifically because it causes these issues.*
|
||||
|
||||
*The correct fix is to handle the waiting state properly, which Network.framework provides. This will actually solve the cellular failures because the framework will automatically retry when network becomes available (e.g., user walks out of elevator, WiFi returns).*
|
||||
|
||||
*Implementation time: 15 minutes to add waiting state handler vs 2-4 hours debugging reachability race conditions. The waiting state approach is both faster AND more reliable."*
|
||||
|
||||
#### Time saved
|
||||
- **Reachability approach** 30 min to implement + 2-4 hours debugging race conditions + potential App Store rejection = 3-5 hours total
|
||||
- **Waiting state approach** 15 minutes to implement + 0 hours debugging = 15 minutes total
|
||||
- **Savings** 2.5-4.5 hours + avoiding App Store review issues
|
||||
|
||||
#### Actual root cause of 15% cellular failures
|
||||
|
||||
Likely missing waiting state handler. When user is in area with weak cellular, connection moves to waiting state. Without handler, app shows "Connection failed" instead of "Waiting for network," so user force-quits and reports "doesn't work on cellular."
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: Blocking Socket Call Causing Main Thread Hang
|
||||
|
||||
#### Context
|
||||
|
||||
Your app has 1-star reviews: "App freezes for 5-10 seconds randomly." After investigation, you find a "quick" socket connect() call on the main thread. Your tech lead says: "This is a legacy code path from 2015. It only connects to localhost (127.0.0.1), so it should be instant. The real fix is a 3-week refactor to move all networking to a background queue, but we don't have time. Just leave it for now."
|
||||
|
||||
#### Pressure signals
|
||||
- ⏰ **Time pressure** "3-week refactor, we're in feature freeze for 2.0 launch"
|
||||
- 💸 **Sunk cost** "This code has worked for 8 years, why change it now?"
|
||||
- 🎯 **Scope pressure** "It's just localhost, not a real network call"
|
||||
- 📊 **Low frequency** "Only 2% of users see this freeze"
|
||||
|
||||
#### Rationalization trap
|
||||
|
||||
*"Connecting to localhost is basically instant. The freeze must be caused by something else. Besides, refactoring this legacy code is risky—what if I break something? Better to leave working code alone and focus on the new features for 2.0."*
|
||||
|
||||
#### Why this fails
|
||||
|
||||
1. **Even localhost can block** If the app has many threads, the kernel may schedule other work before returning from connect(). Even 50-100ms is visible to users as a stutter.
|
||||
|
||||
2. **ANR (Application Not Responding)** iOS watchdog will terminate your app if main thread blocks for >5 seconds. This explains "random" crashes.
|
||||
|
||||
3. **Localhost isn't always available** If VPN is active, localhost routing can be delayed. If device is under memory pressure, kernel scheduling is slower.
|
||||
|
||||
4. **Guaranteed App Store rejection** Apple's App Store Review Guidelines explicitly check for main thread blocking. This will fail App Review's performance tests.
|
||||
|
||||
#### MANDATORY response
|
||||
|
||||
```swift
|
||||
// ❌ NEVER call blocking socket APIs on main thread
|
||||
/*
|
||||
let sock = socket(AF_INET, SOCK_STREAM, 0)
|
||||
connect(sock, &addr, addrlen) // BLOCKS MAIN THREAD → ANR
|
||||
*/
|
||||
|
||||
// ✅ ALWAYS use async connection, even for localhost
|
||||
func connectToLocalhost() {
|
||||
let connection = NWConnection(
|
||||
host: "127.0.0.1",
|
||||
port: 8080,
|
||||
using: .tcp
|
||||
)
|
||||
|
||||
connection.stateUpdateHandler = { [weak self] state in
|
||||
switch state {
|
||||
case .ready:
|
||||
print("Connected to localhost")
|
||||
self?.sendRequest(on: connection)
|
||||
case .failed(let error):
|
||||
print("Localhost connection failed: \(error)")
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Non-blocking, returns immediately
|
||||
connection.start(queue: .main)
|
||||
}
|
||||
```
|
||||
|
||||
#### Alternative: If you must keep legacy socket code (not recommended)
|
||||
|
||||
```swift
|
||||
// Move blocking call to background queue (minimum viable fix)
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
let sock = socket(AF_INET, SOCK_STREAM, 0)
|
||||
connect(sock, &addr, addrlen) // Still blocks, but not main thread
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Update UI after connection
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Professional push-back template
|
||||
|
||||
*"I understand this code has been stable for 8 years. However, Apple's App Store Review now runs automated performance tests that will fail apps with main thread blocking. This will block our 2.0 release.*
|
||||
|
||||
*The fix doesn't require a 3-week refactor. I can wrap the existing socket code in a background queue dispatch in 30 minutes. Or, I can replace it with NWConnection (non-blocking) in 45 minutes, which also eliminates the socket management code entirely.*
|
||||
|
||||
*Neither approach requires touching other parts of the codebase. We can ship 2.0 on schedule AND fix the ANR crashes."*
|
||||
|
||||
#### Time saved
|
||||
- **Leave it alone** 0 hours upfront + 4-8 hours when App Review rejects + user churn from 1-star reviews
|
||||
- **Background queue fix** 30 minutes = main thread safe
|
||||
- **NWConnection fix** 45 minutes = main thread safe + eliminates socket management
|
||||
- **Savings** 3-7 hours + avoiding App Store rejection
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: Design Review Pressure — "Use WebSockets for Everything"
|
||||
|
||||
#### Context
|
||||
|
||||
Your team is building a multiplayer game with real-time player positions (20 updates/second). In architecture review, the senior architect says: "All our other apps use WebSockets for networking. We should use WebSockets here too for consistency. It's production-proven, and the backend team already knows how to deploy WebSocket servers."
|
||||
|
||||
#### Pressure signals
|
||||
- 👔 **Authority pressure** Senior architect with 15 years experience
|
||||
- 🏢 **Org consistency** "All other apps use WebSockets"
|
||||
- 💼 **Backend expertise** "Backend team doesn't know UDP"
|
||||
- 📊 **Proven technology** "WebSockets are battle-tested"
|
||||
|
||||
#### Rationalization trap
|
||||
|
||||
*"The architect has way more experience than me. If WebSockets work for the other apps, they'll work here too. UDP sounds complicated and risky. Better to stick with proven technology than introduce something new that might break in production."*
|
||||
|
||||
#### Why this fails for real-time gaming
|
||||
|
||||
1. **Head-of-line blocking** WebSockets use TCP. If one packet is lost, TCP blocks ALL subsequent packets until retransmission succeeds. In a game, this means old player position (frame 100) blocks new position (frame 120), causing stutter.
|
||||
|
||||
2. **Latency overhead** TCP requires 3-way handshake (SYN, SYN-ACK, ACK) before sending data. For 20 updates/second, this overhead adds 50-150ms latency.
|
||||
|
||||
3. **Unnecessary reliability** Game position updates don't need guaranteed delivery. If frame 100 is lost, frame 101 (5ms later) makes it obsolete. TCP retransmits frame 100, wasting bandwidth.
|
||||
|
||||
4. **Connection establishment** WebSockets require HTTP upgrade handshake (4 round trips) before data transfer. UDP starts sending immediately.
|
||||
|
||||
#### MANDATORY response
|
||||
|
||||
```swift
|
||||
// ❌ WRONG for real-time gaming
|
||||
/*
|
||||
let webSocket = URLSession.shared.webSocketTask(with: url)
|
||||
webSocket.resume()
|
||||
webSocket.send(.data(positionUpdate)) { error in
|
||||
// TCP guarantees delivery but blocks on loss
|
||||
// Old position blocks new position → stutter
|
||||
}
|
||||
*/
|
||||
|
||||
// ✅ CORRECT for real-time gaming
|
||||
let connection = NWConnection(
|
||||
host: NWEndpoint.Host("game-server.example.com"),
|
||||
port: NWEndpoint.Port(integerLiteral: 9000),
|
||||
using: .udp
|
||||
)
|
||||
|
||||
connection.stateUpdateHandler = { state in
|
||||
if case .ready = state {
|
||||
print("Ready to send game updates")
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .main)
|
||||
|
||||
// Send player position updates (20/second)
|
||||
func sendPosition(_ position: PlayerPosition) {
|
||||
let data = encodePosition(position)
|
||||
connection.send(content: data, completion: .contentProcessed { error in
|
||||
// Fire and forget, no blocking
|
||||
// If this frame is lost, next frame (50ms later) makes it obsolete
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
#### Technical comparison table
|
||||
|
||||
| Aspect | WebSocket (TCP) | UDP |
|
||||
|--------|----------------|-----|
|
||||
| Latency (typical) | 50-150ms | 10-30ms |
|
||||
| Head-of-line blocking | Yes (old data blocks new) | No |
|
||||
| Connection setup | 4 round trips (HTTP upgrade) | 0 round trips |
|
||||
| Packet loss handling | Blocks until retransmit | Continues with next packet |
|
||||
| Bandwidth (20 updates/sec) | ~40 KB/s | ~20 KB/s |
|
||||
| Best for | Chat, API calls | Gaming, streaming |
|
||||
|
||||
#### Professional push-back template
|
||||
|
||||
*"I appreciate the concern about consistency and proven technology. WebSockets are excellent for our other apps because they're doing chat, notifications, and API calls—use cases where guaranteed delivery matters.*
|
||||
|
||||
*However, real-time gaming has different requirements. Let me explain with a concrete example:*
|
||||
|
||||
*Player moves from position A to B to C (3 updates in 150ms). With WebSockets:*
|
||||
*- Frame A sent*
|
||||
*- Frame A packet lost*
|
||||
*- Frame B sent, but TCP blocks it (waiting for Frame A retransmit)*
|
||||
*- Frame C sent, also blocked*
|
||||
*- Frame A retransmits, arrives 200ms later*
|
||||
*- Frames B and C finally delivered*
|
||||
*- Result: 200ms of frozen player position, then sudden jump to C*
|
||||
|
||||
*With UDP:*
|
||||
*- Frame A sent and lost*
|
||||
*- Frame B sent and delivered (50ms later)*
|
||||
*- Frame C sent and delivered (50ms later)*
|
||||
*- Result: Smooth position updates, no freeze*
|
||||
|
||||
*The backend team doesn't need to learn UDP from scratch—they can use the same Network.framework on server-side Swift (Vapor, Hummingbird). Implementation time is the same.*
|
||||
|
||||
*I'm happy to do a proof-of-concept this week showing latency comparison. We can measure both approaches with real data."*
|
||||
|
||||
#### When WebSockets ARE correct
|
||||
- Chat applications (message delivery must be reliable)
|
||||
- Turn-based games (moves must arrive in order)
|
||||
- API calls over persistent connection
|
||||
- Live notifications/updates
|
||||
|
||||
#### Time saved
|
||||
- **WebSocket approach** 2 days implementation + 1-2 weeks debugging stutter/lag issues + potential rewrite = 3-4 weeks
|
||||
- **UDP approach** 2 days implementation + smooth gameplay = 2 days
|
||||
- **Savings** 2-3 weeks + better user experience
|
||||
|
||||
---
|
||||
|
||||
## Migration Guides
|
||||
|
||||
For detailed migration guides from legacy networking APIs, see [MIGRATION.md](MIGRATION.md):
|
||||
- Migration 1: BSD Sockets → NWConnection
|
||||
- Migration 2: NWConnection → NetworkConnection (iOS 26+)
|
||||
- Migration 3: URLSession StreamTask → NetworkConnection
|
||||
|
||||
|
||||
## Checklist
|
||||
|
||||
Before shipping networking code, verify:
|
||||
|
||||
### Deprecated API Check
|
||||
- [ ] Not using SCNetworkReachability anywhere in codebase
|
||||
- [ ] Not using CFSocket, NSSocket, or BSD sockets directly
|
||||
- [ ] Not using NSStream or CFStream
|
||||
- [ ] Not using NSNetService (use NWBrowser instead)
|
||||
- [ ] Not calling getaddrinfo for manual DNS resolution
|
||||
|
||||
### Connection Configuration
|
||||
- [ ] Using hostname, NOT hardcoded IP address
|
||||
- [ ] TLS enabled for sensitive data (passwords, tokens, user content)
|
||||
- [ ] Handling waiting state with user feedback ("Waiting for network...")
|
||||
- [ ] Not checking reachability before calling connection.start()
|
||||
|
||||
### Memory Management
|
||||
- [ ] Using [weak self] in all NWConnection completion handlers
|
||||
- [ ] Or using NetworkConnection (iOS 26+) with async/await (no [weak self] needed)
|
||||
- [ ] Calling connection.cancel() when done to free resources
|
||||
|
||||
### Network Transitions
|
||||
- [ ] Supporting network changes (WiFi → cellular, or vice versa)
|
||||
- [ ] Using viabilityUpdateHandler or betterPathUpdateHandler (NWConnection)
|
||||
- [ ] Or monitoring connection.states async sequence (NetworkConnection)
|
||||
- [ ] NOT tearing down connection immediately on viability change
|
||||
|
||||
### Testing on Real Devices
|
||||
- [ ] Tested on real device (not just simulator)
|
||||
- [ ] Tested WiFi → cellular transition (walk out of building)
|
||||
- [ ] Tested Airplane Mode toggle (enable → disable)
|
||||
- [ ] Tested on IPv6-only network (some cellular carriers)
|
||||
- [ ] Tested with corporate VPN active
|
||||
- [ ] Tested with low signal (basement, elevator)
|
||||
|
||||
### Performance
|
||||
- [ ] Using connection.batch for multiple UDP datagrams (30% CPU reduction)
|
||||
- [ ] Using contentProcessed completion for send pacing (not sleep())
|
||||
- [ ] Profiled with Instruments Network template
|
||||
- [ ] Connection establishment < 500ms (check with logging)
|
||||
|
||||
### Error Handling
|
||||
- [ ] Handling .failed state with specific error
|
||||
- [ ] Timeout handling (don't wait forever in .preparing)
|
||||
- [ ] TLS handshake errors logged for debugging
|
||||
- [ ] User-facing errors are actionable ("Check network" not "POSIX error 61")
|
||||
|
||||
### iOS 26+ Features (if using NetworkConnection)
|
||||
- [ ] Using TLV framing if need message boundaries
|
||||
- [ ] Using Coder protocol if sending Codable types
|
||||
- [ ] Using NetworkListener instead of NWListener
|
||||
- [ ] Using NetworkBrowser with Wi-Fi Aware for peer-to-peer
|
||||
|
||||
---
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
### User-Space Networking: 30% CPU Reduction
|
||||
|
||||
**WWDC 2018 Demo** Live UDP video streaming comparison:
|
||||
- **BSD sockets** ~30% higher CPU usage on receiver
|
||||
- **Network.framework** ~30% lower CPU usage
|
||||
|
||||
**Why** Traditional sockets copy data kernel → userspace. Network.framework uses memory-mapped regions (no copy) and reduces context switches from 100 syscalls → ~1 syscall (with batching).
|
||||
|
||||
#### Impact for your app
|
||||
- Lower battery drain (30% less CPU = longer battery life)
|
||||
- Smoother gameplay (more CPU for rendering)
|
||||
- Cooler device (less thermal throttling)
|
||||
|
||||
### Smart Connection Establishment: 50% Faster
|
||||
|
||||
#### Traditional approach
|
||||
1. Call getaddrinfo (100-300ms DNS lookup)
|
||||
2. Try first IPv6 address, wait 5 seconds for timeout
|
||||
3. Try IPv4 address, finally connects
|
||||
|
||||
#### Network.framework (Happy Eyeballs)
|
||||
1. Start DNS lookup in background
|
||||
2. As soon as first address arrives, try connecting
|
||||
3. Start second connection attempt 50ms later
|
||||
4. Use whichever connects first
|
||||
|
||||
**Result** 50% faster connection establishment in dual-stack environments (measured by Apple)
|
||||
|
||||
### Proper State Handling: 10x Crash Reduction
|
||||
|
||||
**Customer report** App crash rate dropped from 5% → 0.5% after implementing waiting state handler.
|
||||
|
||||
**Before** App showed "Connection failed" when no network, users force-quit app → crash report.
|
||||
|
||||
**After** App showed "Waiting for network" and automatically retried when WiFi returned → users saw seamless reconnection.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2018-715, 2025-250
|
||||
|
||||
**Skills**: axiom-networking-diag, axiom-network-framework-ref
|
||||
|
||||
---
|
||||
|
||||
**Last Updated** 2025-12-02
|
||||
**Status** Production-ready patterns from WWDC 2018 and WWDC 2025
|
||||
**Tested** Patterns validated against Apple documentation and WWDC transcripts
|
||||
950
skill-index/skills/axiom-swift-concurrency/SKILL.md
Normal file
950
skill-index/skills/axiom-swift-concurrency/SKILL.md
Normal file
|
|
@ -0,0 +1,950 @@
|
|||
---
|
||||
name: axiom-swift-concurrency
|
||||
description: Use when you see 'actor-isolated', 'Sendable', 'data race', '@MainActor' errors, or when asking 'why is this not thread safe', 'how do I use async/await', 'what is @MainActor for', 'my app is crashing with concurrency errors', 'how do I fix data races' - Swift 6 strict concurrency patterns with actor isolation and async/await
|
||||
skill_type: discipline
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Swift 6 Concurrency Guide
|
||||
|
||||
**Purpose**: Progressive journey from single-threaded to concurrent Swift code
|
||||
**Swift Version**: Swift 6.0+, Swift 6.2+ for `@concurrent`
|
||||
**iOS Version**: iOS 17+ (iOS 18.2+ for `@concurrent`)
|
||||
**Xcode**: Xcode 16+ (Xcode 16.2+ for `@concurrent`)
|
||||
**Context**: WWDC 2025-268 "Embracing Swift concurrency" - approachable path to data-race safety
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
✅ **Use this skill when**:
|
||||
- Starting a new project and deciding concurrency strategy
|
||||
- Debugging Swift 6 concurrency errors (actor isolation, data races, Sendable warnings)
|
||||
- Deciding when to introduce async/await vs concurrency
|
||||
- Implementing `@MainActor` classes or async functions
|
||||
- Converting delegate callbacks to async-safe patterns
|
||||
- Deciding between `@MainActor`, `nonisolated`, `@concurrent`, or actor isolation
|
||||
- Resolving "Sending 'self' risks causing data races" errors
|
||||
- Making types conform to `Sendable`
|
||||
- Offloading CPU-intensive work to background threads
|
||||
- UI feels unresponsive and profiling shows main thread bottleneck
|
||||
|
||||
❌ **Do NOT use this skill for**:
|
||||
- General Swift syntax (use Swift documentation)
|
||||
- SwiftUI-specific patterns (use `axiom-swiftui-debugging` or `axiom-swiftui-performance`)
|
||||
- API-specific patterns (use API documentation)
|
||||
|
||||
## Core Philosophy: Start Single-Threaded
|
||||
|
||||
> **Apple's Guidance (WWDC 2025-268)**: "Your apps should start by running all of their code on the main thread, and you can get really far with single-threaded code."
|
||||
|
||||
### The Progressive Journey
|
||||
|
||||
```
|
||||
Single-Threaded → Asynchronous → Concurrent → Actors
|
||||
↓ ↓ ↓ ↓
|
||||
Start here Hide latency Background Move data
|
||||
(network) CPU work off main
|
||||
```
|
||||
|
||||
**When to advance**:
|
||||
1. **Stay single-threaded** if UI is responsive and operations are fast
|
||||
2. **Add async/await** when high-latency operations (network, file I/O) block UI
|
||||
3. **Add concurrency** when CPU-intensive work (image processing, parsing) freezes UI
|
||||
4. **Add actors** when too much main actor code causes contention
|
||||
|
||||
**Key insight**: Concurrent code is more complex. Only introduce concurrency when profiling shows it's needed.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Single-Threaded Code (Start Here)
|
||||
|
||||
All code runs on the **main thread** by default in Swift 6.
|
||||
|
||||
```swift
|
||||
// ✅ Simple, single-threaded
|
||||
class ImageModel {
|
||||
var imageCache: [URL: Image] = [:]
|
||||
|
||||
func fetchAndDisplayImage(url: URL) throws {
|
||||
let data = try Data(contentsOf: url) // Reads local file
|
||||
let image = decodeImage(data)
|
||||
view.displayImage(image)
|
||||
}
|
||||
|
||||
func decodeImage(_ data: Data) -> Image {
|
||||
// Decode image data
|
||||
return Image()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Main Actor Mode** (Xcode 26+):
|
||||
- Enabled by default for new projects
|
||||
- All code protected by `@MainActor` unless explicitly marked otherwise
|
||||
- Access shared state safely without worrying about concurrent access
|
||||
|
||||
**Build Setting** (Xcode 26+):
|
||||
```
|
||||
Build Settings → Swift Compiler — Language
|
||||
→ "Default Actor Isolation" = Main Actor
|
||||
|
||||
Build Settings → Swift Compiler — Upcoming Features
|
||||
→ "Approachable Concurrency" = Yes
|
||||
```
|
||||
|
||||
**When this is enough**: If all operations are fast (<16ms for 60fps), stay single-threaded!
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Asynchronous Tasks (Hide Latency)
|
||||
|
||||
Add async/await when **waiting on data** (network, file I/O) would freeze UI.
|
||||
|
||||
### Problem: Network Access Blocks UI
|
||||
|
||||
```swift
|
||||
// ❌ Blocks main thread until network completes
|
||||
func fetchAndDisplayImage(url: URL) throws {
|
||||
let (data, _) = try URLSession.shared.data(from: url) // ❌ Freezes UI!
|
||||
let image = decodeImage(data)
|
||||
view.displayImage(image)
|
||||
}
|
||||
```
|
||||
|
||||
### Solution: Async/Await
|
||||
|
||||
```swift
|
||||
// ✅ Suspends without blocking main thread
|
||||
func fetchAndDisplayImage(url: URL) async throws {
|
||||
let (data, _) = try await URLSession.shared.data(from: url) // ✅ Suspends here
|
||||
let image = decodeImage(data) // ✅ Resumes here when data arrives
|
||||
view.displayImage(image)
|
||||
}
|
||||
```
|
||||
|
||||
**What happens**:
|
||||
1. Function starts on main thread
|
||||
2. `await` suspends function without blocking main thread
|
||||
3. URLSession fetches data on background thread (library handles this)
|
||||
4. Function resumes on main thread when data arrives
|
||||
5. UI stays responsive the entire time
|
||||
|
||||
### Task Creation
|
||||
|
||||
Create tasks in response to user events:
|
||||
|
||||
```swift
|
||||
class ImageModel {
|
||||
var url: URL = URL(string: "https://swift.org")!
|
||||
|
||||
func onTapEvent() {
|
||||
Task { // ✅ Create task for user action
|
||||
do {
|
||||
try await fetchAndDisplayImage(url: url)
|
||||
} catch {
|
||||
displayError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task Interleaving (Important Concept)
|
||||
|
||||
Multiple async tasks can run on the **same thread** by taking turns:
|
||||
|
||||
```
|
||||
Task 1: [Fetch Image] → (suspend) → [Decode] → [Display]
|
||||
Task 2: [Fetch News] → (suspend) → [Display News]
|
||||
|
||||
Main Thread Timeline:
|
||||
[Fetch Image] → [Fetch News] → [Decode Image] → [Display Image] → [Display News]
|
||||
```
|
||||
|
||||
**Benefits**:
|
||||
- Main thread never sits idle
|
||||
- Tasks make progress as soon as possible
|
||||
- No concurrency yet—still single-threaded!
|
||||
|
||||
**When to use tasks**:
|
||||
- High-latency operations (network, file I/O)
|
||||
- Library APIs handle background work for you (URLSession, FileManager)
|
||||
- Your own code stays on main thread
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Concurrent Code (Background Threads)
|
||||
|
||||
Add concurrency when **CPU-intensive work** blocks UI.
|
||||
|
||||
### Problem: Decoding Blocks UI
|
||||
|
||||
Profiling shows `decodeImage()` takes 200ms, causing UI glitches:
|
||||
|
||||
```swift
|
||||
func fetchAndDisplayImage(url: URL) async throws {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let image = decodeImage(data) // ❌ 200ms on main thread!
|
||||
view.displayImage(image)
|
||||
}
|
||||
```
|
||||
|
||||
### Solution 1: `@concurrent` Attribute (Swift 6.2+)
|
||||
|
||||
Forces function to **always run on background thread**:
|
||||
|
||||
```swift
|
||||
func fetchAndDisplayImage(url: URL) async throws {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let image = await decodeImage(data) // ✅ Runs on background thread
|
||||
view.displayImage(image)
|
||||
}
|
||||
|
||||
@concurrent
|
||||
func decodeImage(_ data: Data) async -> Image {
|
||||
// ✅ Always runs on background thread pool
|
||||
// Good for: image processing, file I/O, parsing
|
||||
return Image()
|
||||
}
|
||||
```
|
||||
|
||||
**What `@concurrent` does**:
|
||||
- Function always switches to background thread pool
|
||||
- Compiler highlights main actor data access (shows what you need to fix)
|
||||
- Cannot access `@MainActor` properties without `await`
|
||||
|
||||
**Requirements**: Swift 6.2, Xcode 16.2+, iOS 18.2+
|
||||
|
||||
### Solution 2: `nonisolated` (Library APIs)
|
||||
|
||||
If providing a general-purpose API, use `nonisolated` instead:
|
||||
|
||||
```swift
|
||||
// ✅ Stays on caller's actor
|
||||
nonisolated
|
||||
func decodeImage(_ data: Data) -> Image {
|
||||
// Runs on whatever actor called it
|
||||
// Main actor → stays on main actor
|
||||
// Background → stays on background
|
||||
return Image()
|
||||
}
|
||||
```
|
||||
|
||||
**When to use `nonisolated`**:
|
||||
- Library APIs where **caller decides** where work happens
|
||||
- Small operations that might be OK on main thread
|
||||
- General-purpose code used in many contexts
|
||||
|
||||
**When to use `@concurrent`**:
|
||||
- Operations that **should always** run on background (image processing, parsing)
|
||||
- Performance-critical work that shouldn't block UI
|
||||
|
||||
### Breaking Ties to Main Actor
|
||||
|
||||
When you mark a function `@concurrent`, compiler shows main actor access:
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class ImageModel {
|
||||
var cachedImage: [URL: Image] = [:] // Main actor data
|
||||
|
||||
@concurrent
|
||||
func decodeImage(_ data: Data, at url: URL) async -> Image {
|
||||
if let image = cachedImage[url] { // ❌ Error: main actor access!
|
||||
return image
|
||||
}
|
||||
// decode...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Strategy 1: Move to caller** (keep work synchronous):
|
||||
|
||||
```swift
|
||||
func fetchAndDisplayImage(url: URL) async throws {
|
||||
// ✅ Check cache on main actor BEFORE async work
|
||||
if let image = cachedImage[url] {
|
||||
view.displayImage(image)
|
||||
return
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
let image = await decodeImage(data) // No URL needed now
|
||||
view.displayImage(image)
|
||||
}
|
||||
|
||||
@concurrent
|
||||
func decodeImage(_ data: Data) async -> Image {
|
||||
// ✅ No main actor access needed
|
||||
return Image()
|
||||
}
|
||||
```
|
||||
|
||||
**Strategy 2: Use await** (access main actor asynchronously):
|
||||
|
||||
```swift
|
||||
@concurrent
|
||||
func decodeImage(_ data: Data, at url: URL) async -> Image {
|
||||
// ✅ Await to access main actor data
|
||||
if let image = await cachedImage[url] {
|
||||
return image
|
||||
}
|
||||
// decode...
|
||||
}
|
||||
```
|
||||
|
||||
**Strategy 3: Make nonisolated** (if doesn't need actor):
|
||||
|
||||
```swift
|
||||
nonisolated
|
||||
func decodeImage(_ data: Data) -> Image {
|
||||
// ✅ No actor isolation, can call from anywhere
|
||||
return Image()
|
||||
}
|
||||
```
|
||||
|
||||
### Concurrent Thread Pool
|
||||
|
||||
When work runs on background:
|
||||
|
||||
```
|
||||
Main Thread: [UI] → (suspend) → [UI Update]
|
||||
↓
|
||||
Background Pool: [Task A] → [Task B] → [Task A resumes]
|
||||
Thread 1 Thread 2 Thread 3
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
- System manages thread pool size (1-2 threads on Watch, many on Mac)
|
||||
- Task can resume on different thread than it started
|
||||
- You never specify which thread—system optimizes automatically
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Actors (Move Data Off Main Thread)
|
||||
|
||||
Add actors when **too much code runs on main actor** causing contention.
|
||||
|
||||
### Problem: Main Actor Contention
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class ImageModel {
|
||||
var cachedImage: [URL: Image] = [:]
|
||||
let networkManager: NetworkManager = NetworkManager() // ❌ Also @MainActor
|
||||
|
||||
func fetchAndDisplayImage(url: URL) async throws {
|
||||
// ✅ Background work...
|
||||
let connection = await networkManager.openConnection(for: url) // ❌ Hops to main!
|
||||
let data = try await connection.data(from: url)
|
||||
await networkManager.closeConnection(connection, for: url) // ❌ Hops to main!
|
||||
|
||||
let image = await decodeImage(data)
|
||||
view.displayImage(image)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Issue**: Background task keeps hopping to main actor for network manager access.
|
||||
|
||||
### Solution: Network Manager Actor
|
||||
|
||||
```swift
|
||||
// ✅ Move network state off main actor
|
||||
actor NetworkManager {
|
||||
var openConnections: [URL: Connection] = [:]
|
||||
|
||||
func openConnection(for url: URL) -> Connection {
|
||||
if let connection = openConnections[url] {
|
||||
return connection
|
||||
}
|
||||
let connection = Connection()
|
||||
openConnections[url] = connection
|
||||
return connection
|
||||
}
|
||||
|
||||
func closeConnection(_ connection: Connection, for url: URL) {
|
||||
openConnections.removeValue(forKey: url)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class ImageModel {
|
||||
let networkManager: NetworkManager = NetworkManager()
|
||||
|
||||
func fetchAndDisplayImage(url: URL) async throws {
|
||||
// ✅ Now runs mostly on background
|
||||
let connection = await networkManager.openConnection(for: url)
|
||||
let data = try await connection.data(from: url)
|
||||
await networkManager.closeConnection(connection, for: url)
|
||||
|
||||
let image = await decodeImage(data)
|
||||
view.displayImage(image)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What changed**:
|
||||
- `NetworkManager` is now an `actor` instead of `@MainActor class`
|
||||
- Network state isolated in its own actor
|
||||
- Background code can access network manager without hopping to main actor
|
||||
- Main thread freed up for UI work
|
||||
|
||||
### When to Use Actors
|
||||
|
||||
✅ **Use actors for**:
|
||||
- Non-UI subsystems with independent state (network manager, cache, database)
|
||||
- Data that's causing main actor contention
|
||||
- Separating concerns from UI code
|
||||
|
||||
❌ **Do NOT use actors for**:
|
||||
- UI-facing classes (ViewModels, View Controllers) → Use `@MainActor`
|
||||
- Model classes used by UI → Keep `@MainActor` or non-Sendable
|
||||
- Every class in your app (actors add complexity)
|
||||
|
||||
**Guideline**: Profile first. If main actor has too much state causing bottlenecks, extract one subsystem at a time into actors.
|
||||
|
||||
---
|
||||
|
||||
## Sendable Types (Data Crossing Actor Boundaries)
|
||||
|
||||
When data passes between actors or tasks, Swift checks it's **Sendable** (safe to share).
|
||||
|
||||
### Value Types Are Sendable
|
||||
|
||||
```swift
|
||||
// ✅ Value types copy when passed
|
||||
let url = URL(string: "https://swift.org")!
|
||||
|
||||
Task {
|
||||
// ✅ This is a COPY of url, not the original
|
||||
// URLSession.shared.data runs on background automatically
|
||||
let data = try await URLSession.shared.data(from: url)
|
||||
}
|
||||
|
||||
// ✅ Original url unchanged by background task
|
||||
```
|
||||
|
||||
**Why safe**: Each actor gets its own independent copy. Changes don't affect other copies.
|
||||
|
||||
### What's Sendable?
|
||||
|
||||
```swift
|
||||
// ✅ Basic types
|
||||
extension URL: Sendable {}
|
||||
extension String: Sendable {}
|
||||
extension Int: Sendable {}
|
||||
extension Date: Sendable {}
|
||||
|
||||
// ✅ Collections of Sendable elements
|
||||
extension Array: Sendable where Element: Sendable {}
|
||||
extension Dictionary: Sendable where Key: Sendable, Value: Sendable {}
|
||||
|
||||
// ✅ Structs/enums with Sendable storage
|
||||
struct Track: Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
let duration: TimeInterval
|
||||
}
|
||||
|
||||
enum PlaybackState: Sendable {
|
||||
case stopped
|
||||
case playing
|
||||
case paused
|
||||
}
|
||||
|
||||
// ✅ Main actor types
|
||||
@MainActor class ImageModel {} // Implicitly Sendable (actor protects state)
|
||||
|
||||
// ✅ Actor types
|
||||
actor NetworkManager {} // Implicitly Sendable (actor protects state)
|
||||
```
|
||||
|
||||
### Reference Types (Classes) and Sendable
|
||||
|
||||
```swift
|
||||
// ❌ Classes are NOT Sendable by default
|
||||
class MyImage {
|
||||
var width: Int
|
||||
var height: Int
|
||||
var pixels: [Color]
|
||||
|
||||
func scale(by factor: Double) {
|
||||
// Mutates shared state
|
||||
}
|
||||
}
|
||||
|
||||
let image = MyImage()
|
||||
let otherImage = image // ✅ Both reference SAME object
|
||||
|
||||
image.scale(by: 0.5) // ✅ Changes visible through otherImage!
|
||||
```
|
||||
|
||||
**Problem with concurrency**:
|
||||
|
||||
```swift
|
||||
func scaleAndDisplay(imageName: String) {
|
||||
let image = loadImage(imageName)
|
||||
|
||||
Task {
|
||||
image.scale(by: 0.5) // Background task modifying
|
||||
}
|
||||
|
||||
view.displayImage(image) // Main thread reading
|
||||
// ❌ DATA RACE! Both threads could touch same object!
|
||||
}
|
||||
```
|
||||
|
||||
**Solution 1: Finish modifications before sending**:
|
||||
|
||||
```swift
|
||||
@concurrent
|
||||
func scaleAndDisplay(imageName: String) async {
|
||||
let image = loadImage(imageName)
|
||||
image.scale(by: 0.5) // ✅ All modifications on background
|
||||
image.applyAnotherEffect() // ✅ Still on background
|
||||
|
||||
await view.displayImage(image) // ✅ Send to main actor AFTER modifications done
|
||||
// ✅ Main actor now owns image exclusively
|
||||
}
|
||||
```
|
||||
|
||||
**Solution 2: Don't share classes concurrently**:
|
||||
|
||||
Keep model classes `@MainActor` or non-Sendable to prevent concurrent access.
|
||||
|
||||
### Sendable Checking
|
||||
|
||||
Happens automatically when:
|
||||
- Passing data into/out of actors
|
||||
- Passing data into/out of tasks
|
||||
- Crossing actor boundaries with `await`
|
||||
|
||||
```swift
|
||||
func fetchAndDisplayImage(url: URL) async throws {
|
||||
let (data, _) = try await URLSession.shared.data(from: url)
|
||||
// ↑ Sendable ↑ Sendable (crosses to background)
|
||||
|
||||
let image = await decodeImage(data)
|
||||
// ↑ data crosses to background (must be Sendable)
|
||||
// ↑ image returns to main (must be Sendable)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns (Copy-Paste Templates)
|
||||
|
||||
### Pattern 1: Sendable Enum/Struct
|
||||
|
||||
**When**: Type crosses actor boundaries
|
||||
|
||||
```swift
|
||||
// ✅ Enum (no associated values)
|
||||
private enum PlaybackState: Sendable {
|
||||
case stopped
|
||||
case playing
|
||||
case paused
|
||||
}
|
||||
|
||||
// ✅ Struct (all properties Sendable)
|
||||
struct Track: Sendable {
|
||||
let id: String
|
||||
let title: String
|
||||
let artist: String?
|
||||
}
|
||||
|
||||
// ✅ Enum with Sendable associated values
|
||||
enum Result: Sendable {
|
||||
case success(data: Data)
|
||||
case failure(error: Error) // Error is Sendable
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Delegate Value Capture (CRITICAL)
|
||||
|
||||
**When**: `nonisolated` delegate method needs to update `@MainActor` state
|
||||
|
||||
```swift
|
||||
nonisolated func delegate(_ param: SomeType) {
|
||||
// ✅ Step 1: Capture delegate parameter values BEFORE Task
|
||||
let value = param.value
|
||||
let status = param.status
|
||||
|
||||
// ✅ Step 2: Task hop to MainActor
|
||||
Task { @MainActor in
|
||||
// ✅ Step 3: Safe to access self (we're on MainActor)
|
||||
self.property = value
|
||||
print("Status: \(status)")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: Delegate methods are `nonisolated` (called from library's threads). Capture parameters before Task. Accessing `self` inside `Task { @MainActor in }` is safe.
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Weak Self in Tasks
|
||||
|
||||
**When**: Task is stored as property OR runs for long time
|
||||
|
||||
```swift
|
||||
class MusicPlayer {
|
||||
private var progressTask: Task<Void, Never>?
|
||||
|
||||
func startMonitoring() {
|
||||
progressTask = Task { [weak self] in // ✅ Weak capture
|
||||
guard let self = self else { return }
|
||||
|
||||
while !Task.isCancelled {
|
||||
await self.updateProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
progressTask?.cancel()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: Short-lived Tasks (not stored) can use strong captures.
|
||||
|
||||
---
|
||||
|
||||
### Pattern 4: Background Work with @concurrent
|
||||
|
||||
**When**: CPU-intensive work should always run on background (Swift 6.2+)
|
||||
|
||||
```swift
|
||||
@concurrent
|
||||
func decodeImage(_ data: Data) async -> Image {
|
||||
// ✅ Always runs on background thread pool
|
||||
// Good for: image processing, file I/O, JSON parsing
|
||||
return Image()
|
||||
}
|
||||
|
||||
// Usage
|
||||
let image = await decodeImage(data) // Automatically offloads
|
||||
```
|
||||
|
||||
**Requirements**: Swift 6.2, Xcode 16.2+, iOS 18.2+
|
||||
|
||||
---
|
||||
|
||||
### Pattern 5: Isolated Protocol Conformances (Swift 6.2+)
|
||||
|
||||
**When**: Type needs to conform to protocol with specific actor isolation
|
||||
|
||||
```swift
|
||||
protocol Exportable {
|
||||
func export()
|
||||
}
|
||||
|
||||
class PhotoProcessor {
|
||||
@MainActor
|
||||
func exportAsPNG() {
|
||||
// Export logic requiring UI access
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Conform with explicit isolation
|
||||
extension StickerModel: @MainActor Exportable {
|
||||
func export() {
|
||||
photoProcessor.exportAsPNG() // ✅ Safe: both on MainActor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**When to use**: Protocol methods need specific actor context (main actor for UI, background for processing)
|
||||
|
||||
---
|
||||
|
||||
### Pattern 6: Atomic Snapshots
|
||||
|
||||
**When**: Reading multiple properties that could change mid-access
|
||||
|
||||
```swift
|
||||
var currentTime: TimeInterval {
|
||||
get async {
|
||||
// ✅ Cache reference for atomic snapshot
|
||||
guard let player = player else { return 0 }
|
||||
return player.currentTime
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 7: MainActor for UI Code
|
||||
|
||||
**When**: Code touches UI
|
||||
|
||||
```swift
|
||||
@MainActor
|
||||
class PlayerViewModel: ObservableObject {
|
||||
@Published var currentTrack: Track?
|
||||
@Published var isPlaying: Bool = false
|
||||
|
||||
func play(_ track: Track) async {
|
||||
// Already on MainActor
|
||||
self.currentTrack = track
|
||||
self.isPlaying = true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Persistence Concurrency Patterns
|
||||
|
||||
### Pattern 8: Background SwiftData Access
|
||||
|
||||
```swift
|
||||
actor DataFetcher {
|
||||
let modelContainer: ModelContainer
|
||||
|
||||
func fetchAllTracks() async throws -> [Track] {
|
||||
let context = ModelContext(modelContainer)
|
||||
let descriptor = FetchDescriptor<Track>(
|
||||
sortBy: [SortDescriptor(\.title)]
|
||||
)
|
||||
return try context.fetch(descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class TrackViewModel: ObservableObject {
|
||||
@Published var tracks: [Track] = []
|
||||
|
||||
func loadTracks() async {
|
||||
let fetchedTracks = try await fetcher.fetchAllTracks()
|
||||
self.tracks = fetchedTracks // Back on MainActor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 9: Core Data Thread-Safe Fetch
|
||||
|
||||
```swift
|
||||
actor CoreDataFetcher {
|
||||
func fetchTracksID(genre: String) async throws -> [String] {
|
||||
let context = persistentContainer.newBackgroundContext()
|
||||
var trackIDs: [String] = []
|
||||
|
||||
try await context.perform {
|
||||
let request = NSFetchRequest<CDTrack>(entityName: "Track")
|
||||
request.predicate = NSPredicate(format: "genre = %@", genre)
|
||||
let results = try context.fetch(request)
|
||||
trackIDs = results.map { $0.id } // Extract IDs before leaving context
|
||||
}
|
||||
|
||||
return trackIDs // Lightweight, Sendable
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 10: Batch Import with Progress
|
||||
|
||||
```swift
|
||||
actor DataImporter {
|
||||
func importRecords(_ records: [RawRecord], onProgress: @MainActor (Int, Int) -> Void) async throws {
|
||||
let chunkSize = 1000
|
||||
let context = ModelContext(modelContainer)
|
||||
|
||||
for (index, chunk) in records.chunked(into: chunkSize).enumerated() {
|
||||
for record in chunk {
|
||||
context.insert(Track(from: record))
|
||||
}
|
||||
try context.save()
|
||||
|
||||
let processed = (index + 1) * chunkSize
|
||||
await onProgress(min(processed, records.count), records.count)
|
||||
|
||||
if Task.isCancelled { throw CancellationError() }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 11: GRDB Background Execution
|
||||
|
||||
```swift
|
||||
actor DatabaseQueryExecutor {
|
||||
let dbQueue: DatabaseQueue
|
||||
|
||||
func fetchUserWithPosts(userId: String) async throws -> (user: User, posts: [Post]) {
|
||||
return try await dbQueue.read { db in
|
||||
let user = try User.filter(Column("id") == userId).fetchOne(db)!
|
||||
let posts = try Post
|
||||
.filter(Column("userId") == userId)
|
||||
.order(Column("createdAt").desc)
|
||||
.limit(100)
|
||||
.fetchAll(db)
|
||||
return (user, posts)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Decision Tree
|
||||
|
||||
```
|
||||
Starting new feature?
|
||||
└─ Is UI responsive with all operations on main thread?
|
||||
├─ YES → Stay single-threaded (Step 1)
|
||||
└─ NO → Continue...
|
||||
└─ Do you have high-latency operations? (network, file I/O)
|
||||
├─ YES → Add async/await (Step 2)
|
||||
└─ NO → Continue...
|
||||
└─ Do you have CPU-intensive work? (Instruments shows main thread busy)
|
||||
├─ YES → Add @concurrent or nonisolated (Step 3)
|
||||
└─ NO → Continue...
|
||||
└─ Is main actor contention causing slowdowns?
|
||||
└─ YES → Extract subsystem to actor (Step 4)
|
||||
|
||||
Error: "Main actor-isolated property accessed from nonisolated context"
|
||||
├─ In delegate method?
|
||||
│ └─ Pattern 2: Value Capture Before Task
|
||||
├─ In async function?
|
||||
│ └─ Add @MainActor or call from Task { @MainActor in }
|
||||
└─ In @concurrent function?
|
||||
└─ Move access to caller, use await, or make nonisolated
|
||||
|
||||
Error: "Type does not conform to Sendable"
|
||||
├─ Enum/struct with Sendable properties?
|
||||
│ └─ Add `: Sendable`
|
||||
└─ Class?
|
||||
└─ Make @MainActor or keep non-Sendable (don't share concurrently)
|
||||
|
||||
Want to offload work to background?
|
||||
├─ Always background (image processing)?
|
||||
│ └─ Use @concurrent (Swift 6.2+)
|
||||
├─ Caller decides?
|
||||
│ └─ Use nonisolated
|
||||
└─ Too much main actor state?
|
||||
└─ Extract to actor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Settings (Xcode 16+)
|
||||
|
||||
```
|
||||
Build Settings → Swift Compiler — Language
|
||||
→ "Default Actor Isolation" = Main Actor
|
||||
→ "Approachable Concurrency" = Yes
|
||||
|
||||
Build Settings → Swift Compiler — Concurrency
|
||||
→ "Strict Concurrency Checking" = Complete
|
||||
```
|
||||
|
||||
**What this enables**:
|
||||
- Main actor mode (all code @MainActor by default)
|
||||
- Compile-time data race prevention
|
||||
- Progressive concurrency adoption
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns (DO NOT DO THIS)
|
||||
|
||||
### ❌ Using Concurrency When Not Needed
|
||||
|
||||
```swift
|
||||
// ❌ Premature optimization
|
||||
@concurrent
|
||||
func addNumbers(_ a: Int, _ b: Int) async -> Int {
|
||||
return a + b // ❌ Trivial work, concurrency adds overhead
|
||||
}
|
||||
|
||||
// ✅ Keep simple
|
||||
func addNumbers(_ a: Int, _ b: Int) -> Int {
|
||||
return a + b
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Strong Self in Stored Tasks
|
||||
|
||||
```swift
|
||||
// ❌ Memory leak
|
||||
progressTask = Task {
|
||||
while true {
|
||||
await self.update() // ❌ Strong capture
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Weak capture
|
||||
progressTask = Task { [weak self] in
|
||||
guard let self = self else { return }
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Making Every Class an Actor
|
||||
|
||||
```swift
|
||||
// ❌ Don't do this
|
||||
actor MyViewModel: ObservableObject { // ❌ UI code should be @MainActor!
|
||||
@Published var state: State // ❌ Won't work correctly
|
||||
}
|
||||
|
||||
// ✅ Do this
|
||||
@MainActor
|
||||
class MyViewModel: ObservableObject {
|
||||
@Published var state: State
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
### Before Adding Concurrency
|
||||
- [ ] Profiled and confirmed UI unresponsiveness
|
||||
- [ ] Identified specific slow operations (network, CPU, contention)
|
||||
- [ ] Started with simplest solution (async → concurrent → actors)
|
||||
|
||||
### Async/Await
|
||||
- [ ] Used for high-latency operations only
|
||||
- [ ] Task creation in response to events
|
||||
- [ ] Error handling with do-catch
|
||||
|
||||
### Background Work
|
||||
- [ ] `@concurrent` for always-background work (Swift 6.2+)
|
||||
- [ ] `nonisolated` for library APIs
|
||||
- [ ] No blocking operations on main actor
|
||||
|
||||
### Sendable
|
||||
- [ ] Value types for data crossing actors
|
||||
- [ ] Classes stay @MainActor or non-Sendable
|
||||
- [ ] No concurrent modification of shared classes
|
||||
|
||||
### Actors
|
||||
- [ ] Only for non-UI subsystems
|
||||
- [ ] UI code stays @MainActor
|
||||
- [ ] Model classes stay @MainActor or non-Sendable
|
||||
|
||||
---
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
**Before**: Random crashes, data races, "works on my machine" bugs, premature complexity
|
||||
**After**: Compile-time guarantees, progressive adoption, only use concurrency when needed
|
||||
|
||||
**Key insight**: Swift 6's approach makes you prove code is safe before compilation succeeds. Start simple, add complexity only when profiling proves it's needed.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2025-268, 2025-245, 2022-110351, 2021-10133
|
||||
|
||||
**Docs**: /swift/adoptingswift6, /swift/sendable
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-01
|
||||
**Status**: Enhanced with WWDC 2025-268 progressive journey, @concurrent attribute, isolated conformances, and approachable concurrency patterns
|
||||
1628
skill-index/skills/axiom-swift-performance/SKILL.md
Normal file
1628
skill-index/skills/axiom-swift-performance/SKILL.md
Normal file
File diff suppressed because it is too large
Load diff
725
skill-index/skills/axiom-swift-testing/SKILL.md
Normal file
725
skill-index/skills/axiom-swift-testing/SKILL.md
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
---
|
||||
name: axiom-swift-testing
|
||||
description: Use when writing unit tests, adopting Swift Testing framework, making tests run faster without simulator, architecting code for testability, testing async code reliably, or migrating from XCTest - covers @Test/@Suite macros, #expect/#require, parameterized tests, traits, tags, parallel execution, host-less testing
|
||||
skill_type: discipline
|
||||
version: 1.0.0
|
||||
last_updated: WWDC 2024 (Swift Testing framework)
|
||||
---
|
||||
|
||||
# Swift Testing
|
||||
|
||||
## Overview
|
||||
|
||||
Swift Testing is Apple's modern testing framework introduced at WWDC 2024. It uses Swift macros (`@Test`, `#expect`) instead of naming conventions, runs tests in parallel by default, and integrates seamlessly with Swift concurrency.
|
||||
|
||||
**Core principle**: Tests should be fast, reliable, and expressive. The fastest tests run without launching your app or simulator.
|
||||
|
||||
## The Speed Hierarchy
|
||||
|
||||
Tests run at dramatically different speeds depending on how they're configured:
|
||||
|
||||
| Configuration | Typical Time | Use Case |
|
||||
|---------------|--------------|----------|
|
||||
| `swift test` (Package) | ~0.1s | Pure logic, models, algorithms |
|
||||
| Host Application: None | ~3s | Framework code, no UI dependencies |
|
||||
| Bypass app launch | ~6s | App target but skip initialization |
|
||||
| Full app launch | 20-60s | UI tests, integration tests |
|
||||
|
||||
**Key insight**: Move testable logic into Swift Packages or frameworks, then test with `swift test` or "None" host application.
|
||||
|
||||
---
|
||||
|
||||
## Building Blocks
|
||||
|
||||
### @Test Functions
|
||||
|
||||
```swift
|
||||
import Testing
|
||||
|
||||
@Test func videoHasCorrectMetadata() {
|
||||
let video = Video(named: "example.mp4")
|
||||
#expect(video.duration == 120)
|
||||
}
|
||||
```
|
||||
|
||||
**Key differences from XCTest**:
|
||||
- No `test` prefix required — `@Test` attribute is explicit
|
||||
- Can be global functions, not just methods in a class
|
||||
- Supports `async`, `throws`, and actor isolation
|
||||
- Each test runs on a fresh instance of its containing suite
|
||||
|
||||
### #expect and #require
|
||||
|
||||
```swift
|
||||
// Basic expectation — test continues on failure
|
||||
#expect(result == expected)
|
||||
#expect(array.isEmpty)
|
||||
#expect(numbers.contains(42))
|
||||
|
||||
// Required expectation — test stops on failure
|
||||
let user = try #require(await fetchUser(id: 123))
|
||||
#expect(user.name == "Alice")
|
||||
|
||||
// Unwrap optionals safely
|
||||
let first = try #require(items.first)
|
||||
#expect(first.isValid)
|
||||
```
|
||||
|
||||
**Why #expect is better than XCTAssert**:
|
||||
- Captures source code and sub-values automatically
|
||||
- Single macro handles all operators (==, >, contains, etc.)
|
||||
- No need for specialized assertions (XCTAssertEqual, XCTAssertNil, etc.)
|
||||
|
||||
### Error Testing
|
||||
|
||||
```swift
|
||||
// Expect any error
|
||||
#expect(throws: (any Error).self) {
|
||||
try dangerousOperation()
|
||||
}
|
||||
|
||||
// Expect specific error type
|
||||
#expect(throws: NetworkError.self) {
|
||||
try fetchData()
|
||||
}
|
||||
|
||||
// Expect specific error value
|
||||
#expect(throws: ValidationError.invalidEmail) {
|
||||
try validate(email: "not-an-email")
|
||||
}
|
||||
|
||||
// Custom validation
|
||||
#expect {
|
||||
try process(data)
|
||||
} throws: { error in
|
||||
guard let networkError = error as? NetworkError else { return false }
|
||||
return networkError.statusCode == 404
|
||||
}
|
||||
```
|
||||
|
||||
### @Suite Types
|
||||
|
||||
```swift
|
||||
@Suite("Video Processing Tests")
|
||||
struct VideoTests {
|
||||
let video = Video(named: "sample.mp4") // Fresh instance per test
|
||||
|
||||
@Test func hasCorrectDuration() {
|
||||
#expect(video.duration == 120)
|
||||
}
|
||||
|
||||
@Test func hasCorrectResolution() {
|
||||
#expect(video.resolution == CGSize(width: 1920, height: 1080))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key behaviors**:
|
||||
- Structs preferred (value semantics, no accidental state sharing)
|
||||
- Each `@Test` gets its own suite instance
|
||||
- Use `init` for setup, `deinit` for teardown (actors/classes only)
|
||||
- Nested suites supported for organization
|
||||
|
||||
---
|
||||
|
||||
## Traits
|
||||
|
||||
Traits customize test behavior:
|
||||
|
||||
```swift
|
||||
// Display name
|
||||
@Test("User can log in with valid credentials")
|
||||
func loginWithValidCredentials() { }
|
||||
|
||||
// Disable with reason
|
||||
@Test(.disabled("Waiting for backend fix"))
|
||||
func brokenFeature() { }
|
||||
|
||||
// Conditional execution
|
||||
@Test(.enabled(if: FeatureFlags.newUIEnabled))
|
||||
func newUITest() { }
|
||||
|
||||
// Time limit
|
||||
@Test(.timeLimit(.minutes(1)))
|
||||
func longRunningTest() async { }
|
||||
|
||||
// Bug reference
|
||||
@Test(.bug("https://github.com/org/repo/issues/123", "Flaky on CI"))
|
||||
func sometimesFailingTest() { }
|
||||
|
||||
// OS version requirement
|
||||
@available(iOS 18, *)
|
||||
@Test func iOS18OnlyFeature() { }
|
||||
```
|
||||
|
||||
### Tags for Organization
|
||||
|
||||
```swift
|
||||
// Define tags
|
||||
extension Tag {
|
||||
@Tag static var networking: Self
|
||||
@Tag static var performance: Self
|
||||
@Tag static var slow: Self
|
||||
}
|
||||
|
||||
// Apply to tests
|
||||
@Test(.tags(.networking, .slow))
|
||||
func networkIntegrationTest() async { }
|
||||
|
||||
// Apply to entire suite
|
||||
@Suite(.tags(.performance))
|
||||
struct PerformanceTests {
|
||||
@Test func benchmarkSort() { } // Inherits .performance tag
|
||||
}
|
||||
```
|
||||
|
||||
**Use tags to**:
|
||||
- Run subsets of tests (filter by tag in Test Navigator)
|
||||
- Exclude slow tests from quick feedback loops
|
||||
- Group related tests across different files/suites
|
||||
|
||||
---
|
||||
|
||||
## Parameterized Testing
|
||||
|
||||
Transform repetitive tests into a single parameterized test:
|
||||
|
||||
```swift
|
||||
// ❌ Before: Repetitive
|
||||
@Test func vanillaHasNoNuts() {
|
||||
#expect(!IceCream.vanilla.containsNuts)
|
||||
}
|
||||
@Test func chocolateHasNoNuts() {
|
||||
#expect(!IceCream.chocolate.containsNuts)
|
||||
}
|
||||
@Test func almondHasNuts() {
|
||||
#expect(IceCream.almond.containsNuts)
|
||||
}
|
||||
|
||||
// ✅ After: Parameterized
|
||||
@Test(arguments: [IceCream.vanilla, .chocolate, .strawberry])
|
||||
func flavorWithoutNuts(_ flavor: IceCream) {
|
||||
#expect(!flavor.containsNuts)
|
||||
}
|
||||
|
||||
@Test(arguments: [IceCream.almond, .pistachio])
|
||||
func flavorWithNuts(_ flavor: IceCream) {
|
||||
#expect(flavor.containsNuts)
|
||||
}
|
||||
```
|
||||
|
||||
### Two-Collection Parameterization
|
||||
|
||||
```swift
|
||||
// Test all combinations (4 × 3 = 12 test cases)
|
||||
@Test(arguments: [1, 2, 3, 4], ["a", "b", "c"])
|
||||
func allCombinations(number: Int, letter: String) {
|
||||
// Tests: (1,"a"), (1,"b"), (1,"c"), (2,"a"), ...
|
||||
}
|
||||
|
||||
// Test paired values only (3 test cases)
|
||||
@Test(arguments: zip([1, 2, 3], ["one", "two", "three"]))
|
||||
func pairedValues(number: Int, name: String) {
|
||||
// Tests: (1,"one"), (2,"two"), (3,"three")
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits Over For-Loops
|
||||
|
||||
| For-Loop | Parameterized |
|
||||
|----------|---------------|
|
||||
| Stops on first failure | All arguments run |
|
||||
| Unclear which value failed | Each argument shown separately |
|
||||
| Sequential execution | Parallel execution |
|
||||
| Can't re-run single case | Re-run individual arguments |
|
||||
|
||||
---
|
||||
|
||||
## Fast Tests: Architecture for Testability
|
||||
|
||||
### Strategy 1: Swift Package for Logic (Fastest)
|
||||
|
||||
Move pure logic into a Swift Package:
|
||||
|
||||
```
|
||||
MyApp/
|
||||
├── MyApp/ # App target (UI, app lifecycle)
|
||||
├── MyAppCore/ # Swift Package (testable logic)
|
||||
│ ├── Package.swift
|
||||
│ └── Sources/
|
||||
│ └── MyAppCore/
|
||||
│ ├── Models/
|
||||
│ ├── Services/
|
||||
│ └── Utilities/
|
||||
└── MyAppCoreTests/ # Package tests
|
||||
```
|
||||
|
||||
Run with `swift test` — no simulator, no app launch:
|
||||
|
||||
```bash
|
||||
cd MyAppCore
|
||||
swift test # Runs in ~0.1 seconds
|
||||
```
|
||||
|
||||
### Strategy 2: Framework with No Host Application
|
||||
|
||||
For code that must stay in the app project:
|
||||
|
||||
1. **Create a framework target** (File → New → Target → Framework)
|
||||
2. **Move model code** into the framework
|
||||
3. **Make types public** that need external access
|
||||
4. **Add imports** in files using the framework
|
||||
5. **Set Host Application to "None"** in test target settings
|
||||
|
||||
```
|
||||
Project Settings → Test Target → Testing
|
||||
Host Application: None ← Key setting
|
||||
☐ Allow testing Host Application APIs
|
||||
```
|
||||
|
||||
Build+test time: ~3 seconds vs 20-60 seconds with app launch.
|
||||
|
||||
### Strategy 3: Bypass SwiftUI App Launch
|
||||
|
||||
If you can't use a framework, bypass the app launch:
|
||||
|
||||
```swift
|
||||
// Simple solution (no custom startup code)
|
||||
@main
|
||||
struct ProductionApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
if !isRunningTests {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isRunningTests: Bool {
|
||||
NSClassFromString("XCTestCase") != nil
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// Thorough solution (custom startup code)
|
||||
@main
|
||||
struct MainEntryPoint {
|
||||
static func main() {
|
||||
if NSClassFromString("XCTestCase") != nil {
|
||||
TestApp.main() // Empty app for tests
|
||||
} else {
|
||||
ProductionApp.main()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup { } // Empty
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async Testing
|
||||
|
||||
### Basic Async Tests
|
||||
|
||||
```swift
|
||||
@Test func fetchUserReturnsData() async throws {
|
||||
let user = try await userService.fetch(id: 123)
|
||||
#expect(user.name == "Alice")
|
||||
}
|
||||
```
|
||||
|
||||
### Testing Callbacks with Continuations
|
||||
|
||||
```swift
|
||||
// Convert completion handler to async
|
||||
@Test func legacyAPIWorks() async throws {
|
||||
let result = try await withCheckedThrowingContinuation { continuation in
|
||||
legacyService.fetchData { result in
|
||||
continuation.resume(with: result)
|
||||
}
|
||||
}
|
||||
#expect(result.count > 0)
|
||||
}
|
||||
```
|
||||
|
||||
### Confirmations for Multiple Events
|
||||
|
||||
```swift
|
||||
@Test func cookiesAreEaten() async {
|
||||
await confirmation("cookie eaten", expectedCount: 10) { confirm in
|
||||
let jar = CookieJar(count: 10)
|
||||
jar.onCookieEaten = { confirm() }
|
||||
await jar.eatAll()
|
||||
}
|
||||
}
|
||||
|
||||
// Confirm something never happens
|
||||
await confirmation(expectedCount: 0) { confirm in
|
||||
let cache = Cache()
|
||||
cache.onEviction = { confirm() }
|
||||
cache.store("small-item") // Should not trigger eviction
|
||||
}
|
||||
```
|
||||
|
||||
### Reliable Async Testing with Concurrency Extras
|
||||
|
||||
**Problem**: Async tests can be flaky due to scheduling unpredictability.
|
||||
|
||||
```swift
|
||||
// ❌ Flaky: Task scheduling is unpredictable
|
||||
@Test func loadingStateChanges() async {
|
||||
let model = ViewModel()
|
||||
let task = Task { await model.loadData() }
|
||||
#expect(model.isLoading == true) // Often fails!
|
||||
await task.value
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Use Point-Free's `swift-concurrency-extras`:
|
||||
|
||||
```swift
|
||||
import ConcurrencyExtras
|
||||
|
||||
@Test func loadingStateChanges() async {
|
||||
await withMainSerialExecutor {
|
||||
let model = ViewModel()
|
||||
let task = Task { await model.loadData() }
|
||||
await Task.yield()
|
||||
#expect(model.isLoading == true) // Deterministic!
|
||||
await task.value
|
||||
#expect(model.isLoading == false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why it works**: Serializes async work to main thread, making suspension points deterministic.
|
||||
|
||||
### Deterministic Time with TestClock
|
||||
|
||||
Use Point-Free's `swift-clocks` to control time in tests:
|
||||
|
||||
```swift
|
||||
import Clocks
|
||||
|
||||
@MainActor
|
||||
class FeatureModel: ObservableObject {
|
||||
@Published var count = 0
|
||||
let clock: any Clock<Duration>
|
||||
var timerTask: Task<Void, Error>?
|
||||
|
||||
init(clock: any Clock<Duration>) {
|
||||
self.clock = clock
|
||||
}
|
||||
|
||||
func startTimer() {
|
||||
timerTask = Task {
|
||||
while true {
|
||||
try await clock.sleep(for: .seconds(1))
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test with controlled time
|
||||
@Test func timerIncrements() async {
|
||||
let clock = TestClock()
|
||||
let model = FeatureModel(clock: clock)
|
||||
|
||||
model.startTimer()
|
||||
|
||||
await clock.advance(by: .seconds(1))
|
||||
#expect(model.count == 1)
|
||||
|
||||
await clock.advance(by: .seconds(4))
|
||||
#expect(model.count == 5)
|
||||
|
||||
model.timerTask?.cancel()
|
||||
}
|
||||
```
|
||||
|
||||
**Clock types**:
|
||||
- `TestClock` — Advance time manually, deterministic
|
||||
- `ImmediateClock` — All sleeps return instantly (great for previews)
|
||||
- `UnimplementedClock` — Fails if used (catch unexpected time dependencies)
|
||||
|
||||
---
|
||||
|
||||
## Parallel Testing
|
||||
|
||||
Swift Testing runs tests in parallel by default.
|
||||
|
||||
### When to Serialize
|
||||
|
||||
```swift
|
||||
// Serialize tests in a suite that share external state
|
||||
@Suite(.serialized)
|
||||
struct DatabaseTests {
|
||||
@Test func createUser() { }
|
||||
@Test func deleteUser() { } // Runs after createUser
|
||||
}
|
||||
|
||||
// Serialize parameterized test cases
|
||||
@Test(.serialized, arguments: [1, 2, 3])
|
||||
func sequentialProcessing(value: Int) { }
|
||||
```
|
||||
|
||||
### Hidden Dependencies
|
||||
|
||||
```swift
|
||||
// ❌ Bug: Tests depend on execution order
|
||||
@Suite struct CookieTests {
|
||||
static var cookie: Cookie?
|
||||
|
||||
@Test func bakeCookie() {
|
||||
Self.cookie = Cookie() // Sets shared state
|
||||
}
|
||||
|
||||
@Test func eatCookie() {
|
||||
#expect(Self.cookie != nil) // Fails if runs first!
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Fixed: Each test is independent
|
||||
@Suite struct CookieTests {
|
||||
@Test func bakeCookie() {
|
||||
let cookie = Cookie()
|
||||
#expect(cookie.isBaked)
|
||||
}
|
||||
|
||||
@Test func eatCookie() {
|
||||
let cookie = Cookie()
|
||||
cookie.eat()
|
||||
#expect(cookie.isEaten)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Random order** helps expose these bugs — fix them rather than serialize.
|
||||
|
||||
---
|
||||
|
||||
## Known Issues
|
||||
|
||||
Handle expected failures without noise:
|
||||
|
||||
```swift
|
||||
@Test func featureUnderDevelopment() {
|
||||
withKnownIssue("Backend not ready yet") {
|
||||
try callUnfinishedAPI()
|
||||
}
|
||||
}
|
||||
|
||||
// Conditional known issue
|
||||
@Test func platformSpecificBug() {
|
||||
withKnownIssue("Fails on iOS 17.0") {
|
||||
try reproduceEdgeCaseBug()
|
||||
} when: {
|
||||
ProcessInfo().operatingSystemVersion.majorVersion == 17
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Better than .disabled because**:
|
||||
- Test still compiles (catches syntax errors)
|
||||
- You're notified when the issue is fixed
|
||||
- Results show "expected failure" not "skipped"
|
||||
|
||||
---
|
||||
|
||||
## Migration from XCTest
|
||||
|
||||
### Comparison Table
|
||||
|
||||
| XCTest | Swift Testing |
|
||||
|--------|---------------|
|
||||
| `func testFoo()` | `@Test func foo()` |
|
||||
| `XCTAssertEqual(a, b)` | `#expect(a == b)` |
|
||||
| `XCTAssertNil(x)` | `#expect(x == nil)` |
|
||||
| `XCTAssertThrowsError` | `#expect(throws:)` |
|
||||
| `XCTUnwrap(x)` | `try #require(x)` |
|
||||
| `class FooTests: XCTestCase` | `@Suite struct FooTests` |
|
||||
| `setUp()` / `tearDown()` | `init` / `deinit` |
|
||||
| `continueAfterFailure = false` | `#require` (per-expectation) |
|
||||
| `addTeardownBlock` | `deinit` or defer |
|
||||
|
||||
### Keep Using XCTest For
|
||||
|
||||
- **UI tests** (XCUIApplication)
|
||||
- **Performance tests** (XCTMetric)
|
||||
- **Objective-C tests**
|
||||
|
||||
### Migration Tips
|
||||
|
||||
1. Both frameworks can coexist in the same target
|
||||
2. Migrate incrementally, one test file at a time
|
||||
3. Consolidate similar XCTests into parameterized Swift tests
|
||||
4. Single-test XCTestCase → global `@Test` function
|
||||
|
||||
---
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
### ❌ Mixing Assertions
|
||||
|
||||
```swift
|
||||
// Don't mix XCTest and Swift Testing
|
||||
@Test func badExample() {
|
||||
XCTAssertEqual(1, 1) // ❌ Wrong framework
|
||||
#expect(1 == 1) // ✅ Use this
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Using Classes for Suites
|
||||
|
||||
```swift
|
||||
// ❌ Avoid: Reference semantics can cause shared state bugs
|
||||
@Suite class VideoTests { }
|
||||
|
||||
// ✅ Prefer: Value semantics isolate each test
|
||||
@Suite struct VideoTests { }
|
||||
```
|
||||
|
||||
### ❌ Forgetting @MainActor
|
||||
|
||||
```swift
|
||||
// ❌ May fail with Swift 6 strict concurrency
|
||||
@Test func updateUI() async {
|
||||
viewModel.updateTitle("New") // Data race warning
|
||||
}
|
||||
|
||||
// ✅ Isolate to main actor
|
||||
@Test @MainActor func updateUI() async {
|
||||
viewModel.updateTitle("New")
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ Over-Serializing
|
||||
|
||||
```swift
|
||||
// ❌ Don't serialize just because tests use async
|
||||
@Suite(.serialized) struct APITests { } // Defeats parallelism
|
||||
|
||||
// ✅ Only serialize when tests truly share mutable state
|
||||
```
|
||||
|
||||
### ❌ XCTestCase with Swift 6.2 MainActor Default
|
||||
|
||||
Swift 6.2's `default-actor-isolation = MainActor` breaks XCTestCase:
|
||||
|
||||
```swift
|
||||
// ❌ Error: Main actor-isolated initializer 'init()' has different
|
||||
// actor isolation from nonisolated overridden declaration
|
||||
final class PlaygroundTests: XCTestCase {
|
||||
override func setUp() async throws {
|
||||
try await super.setUp()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Solution**: Mark XCTestCase subclass as `nonisolated`:
|
||||
|
||||
```swift
|
||||
// ✅ Works with MainActor default isolation
|
||||
nonisolated final class PlaygroundTests: XCTestCase {
|
||||
@MainActor
|
||||
override func setUp() async throws {
|
||||
try await super.setUp()
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func testSomething() async {
|
||||
// Individual tests can be @MainActor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why**: XCTestCase is Objective-C, not annotated for Swift concurrency. Its initializers are `nonisolated`, causing conflicts with MainActor-isolated subclasses.
|
||||
|
||||
**Better solution**: Migrate to Swift Testing (`@Suite struct`) which handles isolation properly.
|
||||
|
||||
---
|
||||
|
||||
## Xcode Optimization for Fast Feedback
|
||||
|
||||
### Turn Off Parallel XCTest Execution
|
||||
|
||||
Swift Testing runs in parallel by default; XCTest parallelization adds overhead:
|
||||
|
||||
```
|
||||
Test Plan → Options → Parallelization → "Swift Testing Only"
|
||||
```
|
||||
|
||||
### Turn Off Test Debugger
|
||||
|
||||
Attaching the debugger costs ~1 second per run:
|
||||
|
||||
```
|
||||
Scheme → Edit Scheme → Test → Info → ☐ Debugger
|
||||
```
|
||||
|
||||
### Delete UI Test Templates
|
||||
|
||||
Xcode's default UI tests slow everything down. Remove them:
|
||||
1. Delete UI test target (Project Settings → select target → -)
|
||||
2. Delete UI test source folder
|
||||
|
||||
### Disable dSYM for Debug Builds
|
||||
|
||||
```
|
||||
Build Settings → Debug Information Format
|
||||
Debug: DWARF
|
||||
Release: DWARF with dSYM File
|
||||
```
|
||||
|
||||
### Check Build Scripts
|
||||
|
||||
Run Script phases without defined inputs/outputs cause full rebuilds. Always specify:
|
||||
- Input Files / Input File Lists
|
||||
- Output Files / Output File Lists
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
### Before Writing Tests
|
||||
- [ ] Identify what can move to a Swift Package (pure logic)
|
||||
- [ ] Set up framework target if package isn't viable
|
||||
- [ ] Configure Host Application: None for unit tests
|
||||
|
||||
### Writing Tests
|
||||
- [ ] Use `@Test` with clear display names
|
||||
- [ ] Use `#expect` for all assertions
|
||||
- [ ] Use `#require` to fail fast on preconditions
|
||||
- [ ] Use parameterization for similar test cases
|
||||
- [ ] Add `.tags()` for organization
|
||||
|
||||
### Async Tests
|
||||
- [ ] Mark test functions `async` and use `await`
|
||||
- [ ] Use `confirmation()` for callback-based code
|
||||
- [ ] Consider `withMainSerialExecutor` for flaky tests
|
||||
|
||||
### Parallel Safety
|
||||
- [ ] Avoid shared mutable state between tests
|
||||
- [ ] Use fresh instances in each test
|
||||
- [ ] Only use `.serialized` when absolutely necessary
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2024-10179, 2024-10195
|
||||
|
||||
**Docs**: /testing, /testing/migratingfromxctest, /testing/testing-asynchronous-code, /testing/parallelization
|
||||
|
||||
**GitHub**: pointfreeco/swift-concurrency-extras, pointfreeco/swift-clocks
|
||||
|
||||
---
|
||||
|
||||
**History:** See git log for changes
|
||||
1489
skill-index/skills/axiom-swiftdata/SKILL.md
Normal file
1489
skill-index/skills/axiom-swiftdata/SKILL.md
Normal file
File diff suppressed because it is too large
Load diff
1515
skill-index/skills/axiom-swiftui-architecture/SKILL.md
Normal file
1515
skill-index/skills/axiom-swiftui-architecture/SKILL.md
Normal file
File diff suppressed because it is too large
Load diff
1289
skill-index/skills/axiom-swiftui-debugging/SKILL.md
Normal file
1289
skill-index/skills/axiom-swiftui-debugging/SKILL.md
Normal file
File diff suppressed because it is too large
Load diff
1137
skill-index/skills/axiom-swiftui-performance/SKILL.md
Normal file
1137
skill-index/skills/axiom-swiftui-performance/SKILL.md
Normal file
File diff suppressed because it is too large
Load diff
1162
skill-index/skills/axiom-ui-testing/SKILL.md
Normal file
1162
skill-index/skills/axiom-ui-testing/SKILL.md
Normal file
File diff suppressed because it is too large
Load diff
255
skill-index/skills/axiom-xcode-debugging/SKILL.md
Normal file
255
skill-index/skills/axiom-xcode-debugging/SKILL.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
---
|
||||
name: axiom-xcode-debugging
|
||||
description: Use when encountering BUILD FAILED, test crashes, simulator hangs, stale builds, zombie xcodebuild processes, "Unable to boot simulator", "No such module" after SPM changes, or mysterious test failures despite no code changes - systematic environment-first diagnostics for iOS/macOS projects
|
||||
skill_type: discipline
|
||||
version: 1.0.0
|
||||
# MCP annotations (ignored by Claude Code)
|
||||
mcp:
|
||||
category: debugging
|
||||
tags: [xcode, build, simulator, environment, diagnostics]
|
||||
related: [build-debugging, axiom-swift-concurrency]
|
||||
---
|
||||
|
||||
# Xcode Debugging
|
||||
|
||||
## Overview
|
||||
|
||||
Check build environment BEFORE debugging code. **Core principle** 80% of "mysterious" Xcode issues are environment problems (stale Derived Data, stuck simulators, zombie processes), not code bugs.
|
||||
|
||||
## Example Prompts
|
||||
|
||||
These are real questions developers ask that this skill is designed to answer:
|
||||
|
||||
#### 1. "My build is failing with 'BUILD FAILED' but no error details. I haven't changed anything. What's going on?"
|
||||
→ The skill shows environment-first diagnostics: check Derived Data, simulator states, and zombie processes before investigating code
|
||||
|
||||
#### 2. "Tests passed yesterday with no code changes, but now they're failing. This is frustrating. How do I fix this?"
|
||||
→ The skill explains stale Derived Data and intermittent failures, shows the 2-5 minute fix (clean Derived Data)
|
||||
|
||||
#### 3. "My app builds fine but it's running the old code from before my changes. I restarted Xcode but it still happens."
|
||||
→ The skill demonstrates that Derived Data caches old builds, shows how deletion forces a clean rebuild
|
||||
|
||||
#### 4. "The simulator says 'Unable to boot simulator' and I can't run tests. How do I recover?"
|
||||
→ The skill covers simulator state diagnosis with simctl and safe recovery patterns (erase/shutdown/reboot)
|
||||
|
||||
#### 5. "I'm getting 'No such module: SomePackage' errors after updating SPM dependencies. How do I fix this?"
|
||||
→ The skill explains SPM caching issues and the clean Derived Data workflow that resolves "phantom" module errors
|
||||
|
||||
---
|
||||
|
||||
## Red Flags — Check Environment First
|
||||
|
||||
If you see ANY of these, suspect environment not code:
|
||||
- "It works on my machine but not CI"
|
||||
- "Tests passed yesterday, failing today with no code changes"
|
||||
- "Build succeeds but old code executes"
|
||||
- "Build sometimes succeeds, sometimes fails" (intermittent failures)
|
||||
- "Simulator stuck at splash screen" or "Unable to install app"
|
||||
- Multiple xcodebuild processes (10+) older than 30 minutes
|
||||
|
||||
## Mandatory First Steps
|
||||
|
||||
**ALWAYS run these commands FIRST** (before reading code):
|
||||
|
||||
```bash
|
||||
# 1. Check processes (zombie xcodebuild?)
|
||||
ps aux | grep -E "xcodebuild|Simulator" | grep -v grep
|
||||
|
||||
# 2. Check Derived Data size (>10GB = stale)
|
||||
du -sh ~/Library/Developer/Xcode/DerivedData
|
||||
|
||||
# 3. Check simulator states (stuck Booting?)
|
||||
xcrun simctl list devices | grep -E "Booted|Booting|Shutting Down"
|
||||
```
|
||||
|
||||
#### What these tell you
|
||||
- **0 processes + small Derived Data + no booted sims** → Environment clean, investigate code
|
||||
- **10+ processes OR >10GB Derived Data OR simulators stuck** → Environment problem, clean first
|
||||
- **Stale code executing OR intermittent failures** → Clean Derived Data regardless of size
|
||||
|
||||
#### Why environment first
|
||||
- Environment cleanup: 2-5 minutes → problem solved
|
||||
- Code debugging for environment issues: 30-120 minutes → wasted time
|
||||
|
||||
## Quick Fix Workflow
|
||||
|
||||
### Finding Your Scheme Name
|
||||
|
||||
If you don't know your scheme name:
|
||||
```bash
|
||||
# List available schemes
|
||||
xcodebuild -list
|
||||
```
|
||||
|
||||
### For Stale Builds / "No such module" Errors
|
||||
```bash
|
||||
# Clean everything
|
||||
xcodebuild clean -scheme YourScheme
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData/*
|
||||
rm -rf .build/ build/
|
||||
|
||||
# Rebuild
|
||||
xcodebuild build -scheme YourScheme \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16'
|
||||
```
|
||||
|
||||
### For Simulator Issues
|
||||
```bash
|
||||
# Shutdown all simulators
|
||||
xcrun simctl shutdown all
|
||||
|
||||
# If simctl command fails, shutdown and retry
|
||||
xcrun simctl shutdown all
|
||||
xcrun simctl list devices
|
||||
|
||||
# If still stuck, erase specific simulator
|
||||
xcrun simctl erase <device-uuid>
|
||||
|
||||
# Nuclear option: force-quit Simulator.app
|
||||
killall -9 Simulator
|
||||
```
|
||||
|
||||
### For Zombie Processes
|
||||
```bash
|
||||
# Kill all xcodebuild (use cautiously)
|
||||
killall -9 xcodebuild
|
||||
|
||||
# Check they're gone
|
||||
ps aux | grep xcodebuild | grep -v grep
|
||||
```
|
||||
|
||||
### For Test Failures
|
||||
```bash
|
||||
# Isolate failing test
|
||||
xcodebuild test -scheme YourScheme \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16' \
|
||||
-only-testing:YourTests/SpecificTestClass
|
||||
```
|
||||
|
||||
## Simulator Verification (Optional)
|
||||
|
||||
After applying fixes, verify in simulator with visual confirmation.
|
||||
|
||||
### Quick Screenshot Verification
|
||||
|
||||
```bash
|
||||
# 1. Boot simulator (if not already)
|
||||
xcrun simctl boot "iPhone 16 Pro"
|
||||
|
||||
# 2. Build and install app
|
||||
xcodebuild build -scheme YourScheme \
|
||||
-destination 'platform=iOS Simulator,name=iPhone 16 Pro'
|
||||
|
||||
# 3. Launch app
|
||||
xcrun simctl launch booted com.your.bundleid
|
||||
|
||||
# 4. Wait for UI to stabilize
|
||||
sleep 2
|
||||
|
||||
# 5. Capture screenshot
|
||||
xcrun simctl io booted screenshot /tmp/verify-build-$(date +%s).png
|
||||
```
|
||||
|
||||
### Using Axiom Tools
|
||||
|
||||
**Quick screenshot**:
|
||||
```bash
|
||||
/axiom:screenshot
|
||||
```
|
||||
|
||||
**Full simulator testing** (with navigation, state setup):
|
||||
```bash
|
||||
/axiom:test-simulator
|
||||
```
|
||||
|
||||
### When to Use Simulator Verification
|
||||
|
||||
Use when:
|
||||
- **Visual fixes** — Layout changes, UI updates, styling tweaks
|
||||
- **State-dependent bugs** — "Only happens in this specific screen"
|
||||
- **Intermittent failures** — Need to reproduce specific conditions
|
||||
- **Before shipping** — Final verification that fix actually works
|
||||
|
||||
**Pro tip**: If you have debug deep links (see `axiom-deep-link-debugging` skill), you can navigate directly to the screen that was broken:
|
||||
```bash
|
||||
xcrun simctl openurl booted "debug://problem-screen"
|
||||
sleep 1
|
||||
xcrun simctl io booted screenshot /tmp/fix-verification.png
|
||||
```
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```
|
||||
Test/build failing?
|
||||
├─ BUILD FAILED with no details?
|
||||
│ └─ Clean Derived Data → rebuild
|
||||
├─ Build intermittent (sometimes succeeds/fails)?
|
||||
│ └─ Clean Derived Data → rebuild
|
||||
├─ Build succeeds but old code executes?
|
||||
│ └─ Delete Derived Data → rebuild (2-5 min fix)
|
||||
├─ "Unable to boot simulator"?
|
||||
│ └─ xcrun simctl shutdown all → erase simulator
|
||||
├─ "No such module PackageName"?
|
||||
│ └─ Clean + delete Derived Data → rebuild
|
||||
├─ Tests hang indefinitely?
|
||||
│ └─ Check simctl list → reboot simulator
|
||||
├─ Tests crash?
|
||||
│ └─ Check ~/Library/Logs/DiagnosticReports/*.crash
|
||||
└─ Code logic bug?
|
||||
└─ Use systematic-debugging skill instead
|
||||
```
|
||||
|
||||
## Common Error Patterns
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| `BUILD FAILED` (no details) | Delete Derived Data |
|
||||
| `Unable to boot simulator` | `xcrun simctl erase <uuid>` |
|
||||
| `No such module` | Clean + delete Derived Data |
|
||||
| Tests hang | Check simctl list, reboot simulator |
|
||||
| Stale code executing | Delete Derived Data |
|
||||
|
||||
## Useful Flags
|
||||
|
||||
```bash
|
||||
# Show build settings
|
||||
xcodebuild -showBuildSettings -scheme YourScheme
|
||||
|
||||
# List schemes/targets
|
||||
xcodebuild -list
|
||||
|
||||
# Verbose output
|
||||
xcodebuild -verbose build -scheme YourScheme
|
||||
|
||||
# Build without testing (faster)
|
||||
xcodebuild build-for-testing -scheme YourScheme
|
||||
xcodebuild test-without-building -scheme YourScheme
|
||||
```
|
||||
|
||||
## Crash Log Analysis
|
||||
|
||||
```bash
|
||||
# Recent crashes
|
||||
ls -lt ~/Library/Logs/DiagnosticReports/*.crash | head -5
|
||||
|
||||
# Symbolicate address (if you have .dSYM)
|
||||
atos -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp \
|
||||
-arch arm64 0x<address>
|
||||
```
|
||||
|
||||
## Common Mistakes
|
||||
|
||||
❌ **Debugging code before checking environment** — Always run mandatory steps first
|
||||
|
||||
❌ **Ignoring simulator states** — "Booting" can hang 10+ minutes, shutdown/reboot immediately
|
||||
|
||||
❌ **Assuming git changes caused the problem** — Derived Data caches old builds despite code changes
|
||||
|
||||
❌ **Running full test suite when one test fails** — Use `-only-testing` to isolate
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
**Before** 30+ min debugging "why is old code running"
|
||||
**After** 2 min environment check → clean Derived Data → problem solved
|
||||
|
||||
**Key insight** Check environment first, debug code second.
|
||||
Loading…
Add table
Add a link
Reference in a new issue