Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-core-location/.openskills.json
Normal file
7
.claude/skills/axiom-core-location/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-core-location",
|
||||
"installedAt": "2026-04-12T08:06:07.581Z"
|
||||
}
|
||||
484
.claude/skills/axiom-core-location/SKILL.md
Normal file
484
.claude/skills/axiom-core-location/SKILL.md
Normal file
@@ -0,0 +1,484 @@
|
||||
---
|
||||
name: axiom-core-location
|
||||
description: Use for Core Location implementation patterns - authorization strategy, monitoring strategy, accuracy selection, background location
|
||||
license: MIT
|
||||
compatibility: iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
last-updated: "2026-01-03"
|
||||
---
|
||||
|
||||
# Core Location Patterns
|
||||
|
||||
Discipline skill for Core Location implementation decisions. Prevents common authorization mistakes, battery drain, and background location failures.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Choosing authorization strategy (When In Use vs Always)
|
||||
- Deciding monitoring approach (continuous vs significant-change vs CLMonitor)
|
||||
- Implementing geofencing or background location
|
||||
- Debugging "location not working" issues
|
||||
- Reviewing location code for anti-patterns
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `axiom-core-location-ref` — API reference, code examples
|
||||
- `axiom-core-location-diag` — Symptom-based troubleshooting
|
||||
- `axiom-energy` — Location as battery subsystem
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Anti-Patterns (with Time Costs)
|
||||
|
||||
### Anti-Pattern 1: Premature Always Authorization
|
||||
|
||||
**Wrong** (30-60% denial rate):
|
||||
```swift
|
||||
// First launch: "Can we have Always access?"
|
||||
manager.requestAlwaysAuthorization()
|
||||
```
|
||||
|
||||
**Right** (5-10% denial rate):
|
||||
```swift
|
||||
// Start with When In Use
|
||||
CLServiceSession(authorization: .whenInUse)
|
||||
|
||||
// Later, when user triggers background feature:
|
||||
CLServiceSession(authorization: .always)
|
||||
```
|
||||
|
||||
**Time cost**: 15 min to fix code, but 30-60% of users permanently denied = feature adoption destroyed.
|
||||
|
||||
**Why**: Users deny aggressive requests. Start minimal, upgrade when user understands value.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 2: Continuous Updates for Geofencing
|
||||
|
||||
**Wrong** (10x battery drain):
|
||||
```swift
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
if isNearTarget(update.location) {
|
||||
triggerGeofence()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Right** (system-managed, low power):
|
||||
```swift
|
||||
let monitor = await CLMonitor("Geofences")
|
||||
let condition = CLMonitor.CircularGeographicCondition(
|
||||
center: target, radius: 100
|
||||
)
|
||||
await monitor.add(condition, identifier: "Target")
|
||||
|
||||
for try await event in monitor.events {
|
||||
if event.state == .satisfied { triggerGeofence() }
|
||||
}
|
||||
```
|
||||
|
||||
**Time cost**: 5 min to refactor, saves 10x battery.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 3: Ignoring Stationary Detection
|
||||
|
||||
**Wrong** (wasted battery):
|
||||
```swift
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
processLocation(update.location)
|
||||
// Never stops, even when device stationary
|
||||
}
|
||||
```
|
||||
|
||||
**Right** (automatic pause/resume):
|
||||
```swift
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
if let location = update.location {
|
||||
processLocation(location)
|
||||
}
|
||||
if update.isStationary, let location = update.location {
|
||||
// Device stopped moving - updates pause automatically
|
||||
// Will resume when device moves again
|
||||
saveLastKnownLocation(location)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Time cost**: 2 min to add check, saves significant battery.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 4: No Graceful Denial Handling
|
||||
|
||||
**Wrong** (broken UX):
|
||||
```swift
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
guard let location = update.location else { continue }
|
||||
// User denied - silent failure, no feedback
|
||||
}
|
||||
```
|
||||
|
||||
**Right** (graceful degradation):
|
||||
```swift
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
if update.authorizationDenied {
|
||||
showManualLocationPicker()
|
||||
break
|
||||
}
|
||||
if update.authorizationDeniedGlobally {
|
||||
showSystemLocationDisabledMessage()
|
||||
break
|
||||
}
|
||||
if let location = update.location {
|
||||
processLocation(location)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Time cost**: 10 min to add handling, prevents confused users.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 5: Wrong Accuracy for Use Case
|
||||
|
||||
**Wrong** (battery drain for weather app):
|
||||
```swift
|
||||
// Weather app using navigation accuracy
|
||||
CLLocationUpdate.liveUpdates(.automotiveNavigation)
|
||||
```
|
||||
|
||||
**Right** (match accuracy to need):
|
||||
```swift
|
||||
// Weather: city-level is fine
|
||||
CLLocationUpdate.liveUpdates(.default) // or .fitness for runners
|
||||
|
||||
// Navigation: needs high accuracy
|
||||
CLLocationUpdate.liveUpdates(.automotiveNavigation)
|
||||
```
|
||||
|
||||
| Use Case | Configuration | Accuracy | Battery |
|
||||
|----------|---------------|----------|---------|
|
||||
| Navigation | `.automotiveNavigation` | ~5m | Highest |
|
||||
| Fitness tracking | `.fitness` | ~10m | High |
|
||||
| Store finder | `.default` | ~10-100m | Medium |
|
||||
| Weather | `.default` | ~100m+ | Low |
|
||||
|
||||
**Time cost**: 1 min to change, significant battery savings.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 6: Not Stopping Updates
|
||||
|
||||
**Wrong** (battery drain, location icon persists):
|
||||
```swift
|
||||
func viewDidLoad() {
|
||||
Task {
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
updateMap(update.location)
|
||||
}
|
||||
}
|
||||
}
|
||||
// User navigates away, updates continue forever
|
||||
```
|
||||
|
||||
**Right** (cancel when done):
|
||||
```swift
|
||||
private var locationTask: Task<Void, Error>?
|
||||
|
||||
func startTracking() {
|
||||
locationTask = Task {
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
if Task.isCancelled { break }
|
||||
updateMap(update.location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopTracking() {
|
||||
locationTask?.cancel()
|
||||
locationTask = nil
|
||||
}
|
||||
```
|
||||
|
||||
**Time cost**: 5 min to add cancellation, stops battery drain.
|
||||
|
||||
---
|
||||
|
||||
### Anti-Pattern 7: Ignoring CLServiceSession (iOS 18+)
|
||||
|
||||
**Wrong** (procedural authorization juggling):
|
||||
```swift
|
||||
func requestAuth() {
|
||||
switch manager.authorizationStatus {
|
||||
case .notDetermined:
|
||||
manager.requestWhenInUseAuthorization()
|
||||
case .authorizedWhenInUse:
|
||||
if needsFullAccuracy {
|
||||
manager.requestTemporaryFullAccuracyAuthorization(...)
|
||||
}
|
||||
// Complex state machine...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Right** (declarative goals):
|
||||
```swift
|
||||
// Just declare what you need - Core Location handles the rest
|
||||
let session = CLServiceSession(authorization: .whenInUse)
|
||||
|
||||
// For feature needing full accuracy
|
||||
let navSession = CLServiceSession(
|
||||
authorization: .whenInUse,
|
||||
fullAccuracyPurposeKey: "Navigation"
|
||||
)
|
||||
|
||||
// Monitor diagnostics if needed
|
||||
for try await diag in session.diagnostics {
|
||||
if diag.authorizationDenied { handleDenial() }
|
||||
}
|
||||
```
|
||||
|
||||
**Time cost**: 30 min to migrate, simpler code, fewer bugs.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Decision Trees
|
||||
|
||||
### Authorization Strategy
|
||||
|
||||
```
|
||||
Q1: Does your feature REQUIRE background location?
|
||||
├─ NO → Use .whenInUse
|
||||
│ └─ Q2: Does any feature need precise location?
|
||||
│ ├─ ALWAYS → Add fullAccuracyPurposeKey to session
|
||||
│ └─ SOMETIMES → Layer full-accuracy session when feature active
|
||||
│
|
||||
└─ YES → Start with .whenInUse, upgrade to .always when user triggers feature
|
||||
└─ Q3: When does user first need background location?
|
||||
├─ IMMEDIATELY (e.g., fitness tracker) → Request .always on first relevant action
|
||||
└─ LATER (e.g., geofence reminders) → Add .always session when user creates first geofence
|
||||
```
|
||||
|
||||
### Monitoring Strategy
|
||||
|
||||
```
|
||||
Q1: What are you monitoring for?
|
||||
├─ USER POSITION (continuous tracking)
|
||||
│ └─ Use CLLocationUpdate.liveUpdates()
|
||||
│ └─ Q2: What activity?
|
||||
│ ├─ Driving navigation → .automotiveNavigation
|
||||
│ ├─ Walking/cycling nav → .otherNavigation
|
||||
│ ├─ Fitness tracking → .fitness
|
||||
│ ├─ Airplane apps → .airborne
|
||||
│ └─ General → .default or omit
|
||||
│
|
||||
├─ ENTRY/EXIT REGIONS (geofencing)
|
||||
│ └─ Use CLMonitor with CircularGeographicCondition
|
||||
│ └─ Note: Maximum 20 conditions per app
|
||||
│
|
||||
├─ BEACON PROXIMITY
|
||||
│ └─ Use CLMonitor with BeaconIdentityCondition
|
||||
│ └─ Choose granularity: UUID only, UUID+major, UUID+major+minor
|
||||
│
|
||||
└─ SIGNIFICANT CHANGES ONLY (lowest power)
|
||||
└─ Use startMonitoringSignificantLocationChanges() (legacy)
|
||||
└─ Updates ~500m movements, works in background
|
||||
```
|
||||
|
||||
### Accuracy Selection
|
||||
|
||||
```
|
||||
Q1: What's the minimum accuracy that makes your feature work?
|
||||
├─ TURN-BY-TURN NAV needs 5-10m → .automotiveNavigation / .otherNavigation
|
||||
├─ FITNESS TRACKING needs 10-20m → .fitness
|
||||
├─ STORE FINDER needs 100m → .default
|
||||
├─ WEATHER/CITY needs 1km+ → .default (reduced accuracy acceptable)
|
||||
└─ GEOFENCING uses system determination → CLMonitor handles it
|
||||
|
||||
Q2: Will user be moving fast?
|
||||
├─ DRIVING (high speed) → .automotiveNavigation (extra processing for speed)
|
||||
├─ CYCLING/WALKING → .otherNavigation
|
||||
└─ STATIONARY/SLOW → .default
|
||||
|
||||
Always start with lowest acceptable accuracy. Higher accuracy = higher battery drain.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Pressure Scenarios
|
||||
|
||||
### Scenario 1: "Just Use Always Authorization"
|
||||
|
||||
**Context**: PM says "Users want location reminders. Just request Always access on first launch so it works."
|
||||
|
||||
**Pressure**: Ship fast, seems simpler.
|
||||
|
||||
**Reality**:
|
||||
- 30-60% of users will deny Always authorization when asked upfront
|
||||
- Users who deny can only re-enable in Settings (most won't)
|
||||
- Feature adoption destroyed before users understand value
|
||||
|
||||
**Response**:
|
||||
> "Always authorization has 30-60% denial rates when requested upfront. We should start with When In Use, then request Always upgrade when the user creates their first location reminder. This gives us a 5-10% denial rate because users understand why they need it."
|
||||
|
||||
**Evidence**: Apple's own guidance in WWDC 2024-10212: "CLServiceSessions should be taken proactively... hold one requiring full-accuracy when people engage a feature that would warrant a special ask for it."
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: "Location Isn't Working in Background"
|
||||
|
||||
**Context**: QA reports "App stops getting location when backgrounded."
|
||||
|
||||
**Pressure**: Quick fix before release.
|
||||
|
||||
**Wrong fixes**:
|
||||
- Add all background modes
|
||||
- Use `allowsBackgroundLocationUpdates = true` without understanding
|
||||
- Request Always authorization
|
||||
|
||||
**Right diagnosis**:
|
||||
1. Check background mode capability exists
|
||||
2. Check CLBackgroundActivitySession is held (not deallocated)
|
||||
3. Check session started from foreground
|
||||
4. Check authorization level (.whenInUse works with CLBackgroundActivitySession)
|
||||
|
||||
**Response**:
|
||||
> "Background location requires specific setup. Let me check: (1) Background mode capability, (2) CLBackgroundActivitySession held during tracking, (3) session started from foreground. Missing any of these causes silent failure."
|
||||
|
||||
**Checklist**:
|
||||
```swift
|
||||
// 1. Signing & Capabilities → Background Modes → Location updates
|
||||
// 2. Hold session reference (property, not local variable)
|
||||
var backgroundSession: CLBackgroundActivitySession?
|
||||
|
||||
func startBackgroundTracking() {
|
||||
// 3. Must start from foreground
|
||||
backgroundSession = CLBackgroundActivitySession()
|
||||
startLocationUpdates()
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: "Geofence Events Aren't Firing"
|
||||
|
||||
**Context**: Geofences work in testing but not in production for some users.
|
||||
|
||||
**Pressure**: "It works on my device" dismissal.
|
||||
|
||||
**Common causes**:
|
||||
1. **Too many conditions**: Maximum 20 per app
|
||||
2. **Radius too small**: Minimum ~100m for reliable triggering
|
||||
3. **Overlapping regions**: Can cause confusion
|
||||
4. **Not awaiting events**: Events only become lastEvent after handled
|
||||
5. **Not reinitializing on launch**: Monitor must be recreated
|
||||
|
||||
**Response**:
|
||||
> "Geofencing has several system constraints. Check: (1) Are we within the 20-condition limit? (2) Are all radii at least 100m? (3) Is the app reinitializing CLMonitor on launch? (4) Is the app always awaiting on monitor.events?"
|
||||
|
||||
**Diagnostic code**:
|
||||
```swift
|
||||
// Check condition count
|
||||
let count = await monitor.identifiers.count
|
||||
if count >= 20 {
|
||||
print("At 20-condition limit!")
|
||||
}
|
||||
|
||||
// Check all conditions
|
||||
for id in await monitor.identifiers {
|
||||
if let record = await monitor.record(for: id) {
|
||||
let condition = record.condition
|
||||
if let geo = condition as? CLMonitor.CircularGeographicCondition {
|
||||
if geo.radius < 100 {
|
||||
print("Radius too small: \(id)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Part 4: Checklists
|
||||
|
||||
### Pre-Release Location Checklist
|
||||
|
||||
**Info.plist**:
|
||||
- [ ] `NSLocationWhenInUseUsageDescription` with clear explanation
|
||||
- [ ] `NSLocationAlwaysAndWhenInUseUsageDescription` if using Always (clear why background needed)
|
||||
- [ ] `NSLocationDefaultAccuracyReduced` if reduced accuracy acceptable
|
||||
- [ ] `NSLocationTemporaryUsageDescriptionDictionary` if requesting temporary full accuracy
|
||||
- [ ] `UIBackgroundModes` includes `location` if background tracking
|
||||
|
||||
**Authorization**:
|
||||
- [ ] Start with minimal authorization (.whenInUse)
|
||||
- [ ] Upgrade to .always only when user triggers background feature
|
||||
- [ ] Handle authorization denial gracefully (offer alternatives)
|
||||
- [ ] Handle global location services disabled
|
||||
- [ ] Test with reduced accuracy authorization
|
||||
|
||||
**Updates**:
|
||||
- [ ] Using appropriate LiveConfiguration for use case
|
||||
- [ ] Handling isStationary for pause/resume
|
||||
- [ ] Cancelling location tasks when feature inactive
|
||||
- [ ] Not using continuous updates for geofencing
|
||||
|
||||
**Testing**:
|
||||
- [ ] Tested authorization denial flow
|
||||
- [ ] Tested reduced accuracy mode
|
||||
- [ ] Tested background-to-foreground transitions
|
||||
- [ ] Tested app termination and relaunch recovery
|
||||
|
||||
### Background Location Checklist
|
||||
|
||||
**Setup**:
|
||||
- [ ] Background mode capability added (Location updates)
|
||||
- [ ] CLBackgroundActivitySession created and HELD (not local variable)
|
||||
- [ ] Session started from foreground
|
||||
- [ ] Updates restarted on background launch in didFinishLaunchingWithOptions
|
||||
|
||||
**Authorization**:
|
||||
- [ ] Using .whenInUse with CLBackgroundActivitySession, OR
|
||||
- [ ] Using .always (but only if needed beyond background indicator)
|
||||
|
||||
**Lifecycle**:
|
||||
- [ ] Persisting "was tracking" state for relaunch recovery
|
||||
- [ ] Recreating CLBackgroundActivitySession on background launch
|
||||
- [ ] Restarting CLLocationUpdate iteration on launch
|
||||
- [ ] CLMonitor reinitialized with same name on launch
|
||||
|
||||
**Testing**:
|
||||
- [ ] Blue background location indicator appears when backgrounded
|
||||
- [ ] Updates continue when app backgrounded
|
||||
- [ ] Updates resume after app suspended and resumed
|
||||
- [ ] Updates resume after app terminated and relaunched
|
||||
|
||||
---
|
||||
|
||||
## Part 5: iOS Version Considerations
|
||||
|
||||
| Feature | iOS Version | Notes |
|
||||
|---------|-------------|-------|
|
||||
| CLLocationUpdate | iOS 17+ | AsyncSequence API |
|
||||
| CLMonitor | iOS 17+ | Replaces CLCircularRegion |
|
||||
| CLBackgroundActivitySession | iOS 17+ | Background with blue indicator |
|
||||
| CLServiceSession | iOS 18+ | Declarative authorization |
|
||||
| Implicit service sessions | iOS 18+ | From iterating liveUpdates |
|
||||
| CLLocationManager | iOS 2+ | Legacy but still works |
|
||||
|
||||
**For iOS 14-16 support**: Use CLLocationManager delegate pattern (see core-location-ref Part 7).
|
||||
|
||||
**For iOS 17+**: Prefer CLLocationUpdate and CLMonitor.
|
||||
|
||||
**For iOS 18+**: Add CLServiceSession for declarative authorization.
|
||||
|
||||
---
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2023-10180, 2023-10147, 2024-10212
|
||||
|
||||
**Docs**: /corelocation, /corelocation/clmonitor, /corelocation/cllocationupdate, /corelocation/clservicesession
|
||||
|
||||
**Skills**: axiom-core-location-ref, axiom-core-location-diag, axiom-energy
|
||||
3
.claude/skills/axiom-core-location/agents/openai.yaml
Normal file
3
.claude/skills/axiom-core-location/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Core Location"
|
||||
short_description: "Core Location implementation patterns"
|
||||
Reference in New Issue
Block a user