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):
// 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
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 = true without 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:
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