home / skills / charleswiltgen / axiom / axiom-core-location

This skill helps you implement robust Core Location patterns, preventing battery drain and background failures while improving accuracy and user consent

npx playbooks add skill charleswiltgen/axiom --skill axiom-core-location

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
14.6 KB
---
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

Overview

This skill captures battle-tested Core Location implementation patterns for modern xOS development. It focuses on authorization strategy, monitoring choices, accuracy selection, background location, and common anti-patterns that cause battery drain or feature breakage. Use it to design, review, or debug location features with minimal user friction and power impact.

How this skill works

The skill codifies decision trees and concrete patterns: start with minimal authorization and upgrade only when users opt into background features; choose system-managed monitors (CLMonitor) for geofencing; match LiveConfiguration accuracy to the use case; and hold background sessions correctly. It inspects common failure modes, suggests fixes, and provides pre-release and background checklists to validate behavior across lifecycle events and iOS versions.

When to use it

  • Designing authorization flow (When In Use → Always upgrade)
  • Choosing monitoring approach: continuous updates vs CLMonitor vs significant changes
  • Implementing geofencing, beacons, or background tracking
  • Debugging background location failures or unexpected battery drain
  • Code review to eliminate anti-patterns and improve reliability

Best practices

  • Request minimal authorization first; upgrade to Always only when user triggers background features
  • Prefer CLMonitor for geofencing and system-managed conditions instead of continuous polling
  • Match LiveConfiguration accuracy to the feature (start lowest acceptable accuracy)
  • Respect stationary detection and cancel location tasks when inactive
  • Hold CLBackgroundActivitySession and reinitialize monitors on launch for reliable background work

Example use cases

  • Weather app: use .default accuracy and avoid high-precision navigation settings
  • Geofence reminders: start with When In Use and request .always when user creates first geofence
  • Fitness tracker: use .fitness or .otherNavigation accuracy for continuous tracking
  • Store finder: use CLMonitor with circular geographic conditions and reasonable radius (~100m+)
  • Background delivery tracking: ensure background session is held and started from foreground

FAQ

Why not request Always authorization on first launch?

Asking for Always upfront yields high denial rates (30–60%). Start with When In Use and request Always when the user clearly needs background features to improve adoption.

Geofence events work in testing but not in production—what should I check?

Verify you’re under the 20-condition limit, radii are large enough (~100m+), monitors are recreated on launch, and you are awaiting monitor.events so events are delivered.