| name | axiom-core-location |
| description | Use for Core Location implementation patterns - authorization strategy, monitoring strategy, accuracy selection, background location |
| skill_type | discipline |
| version | 1.0.0 |
| apple_platforms | iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+ |
| last_updated | Sat Jan 03 2026 00:00:00 GMT+0000 (Coordinated Universal Time) |
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 examplesaxiom-core-location-diag— Symptom-based troubleshootingaxiom-energy— Location as battery subsystem
Part 1: Anti-Patterns (with Time Costs)
Anti-Pattern 1: Premature Always Authorization
Wrong (30-60% denial rate):
// First launch: "Can we have Always access?"
manager.requestAlwaysAuthorization()
Right (5-10% denial rate):
// 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):
for try await update in CLLocationUpdate.liveUpdates() {
if isNearTarget(update.location) {
triggerGeofence()
}
}
Right (system-managed, low power):
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):
for try await update in CLLocationUpdate.liveUpdates() {
processLocation(update.location)
// Never stops, even when device stationary
}
Right (automatic pause/resume):
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):
for try await update in CLLocationUpdate.liveUpdates() {
guard let location = update.location else { continue }
// User denied - silent failure, no feedback
}
Right (graceful degradation):
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):
// Weather app using navigation accuracy
CLLocationUpdate.liveUpdates(.automotiveNavigation)
Right (match accuracy to need):
// 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):
func viewDidLoad() {
Task {
for try await update in CLLocationUpdate.liveUpdates() {
updateMap(update.location)
}
}
}
// User navigates away, updates continue forever
Right (cancel when done):
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):
func requestAuth() {
switch manager.authorizationStatus {
case .notDetermined:
manager.requestWhenInUseAuthorization()
case .authorizedWhenInUse:
if needsFullAccuracy {
manager.requestTemporaryFullAccuracyAuthorization(...)
}
// Complex state machine...
}
}
Right (declarative goals):
// 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 = truewithout understanding - Request Always authorization
Right diagnosis:
- Check background mode capability exists
- Check CLBackgroundActivitySession is held (not deallocated)
- Check session started from foreground
- 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:
// 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:
- Too many conditions: Maximum 20 per app
- Radius too small: Minimum ~100m for reliable triggering
- Overlapping regions: Can cause confusion
- Not awaiting events: Events only become lastEvent after handled
- 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:
// 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:
-
NSLocationWhenInUseUsageDescriptionwith clear explanation -
NSLocationAlwaysAndWhenInUseUsageDescriptionif using Always (clear why background needed) -
NSLocationDefaultAccuracyReducedif reduced accuracy acceptable -
NSLocationTemporaryUsageDescriptionDictionaryif requesting temporary full accuracy -
UIBackgroundModesincludeslocationif 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