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
- energy — Location as a 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 = 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 the legacy CLLocationManager delegate pattern (see the Legacy section in this skill).
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: core-location, energy