--- name: axiom-core-location-ref description: Use for Core Location API reference - CLLocationUpdate, CLMonitor, CLServiceSession, authorization, background location, geofencing license: MIT compatibility: iOS 17+, iPadOS 17+, macOS 14+, watchOS 10+ metadata: version: "1.0.0" last-updated: "2026-01-03" --- # Core Location Reference Comprehensive API reference for modern Core Location (iOS 17+). ## When to Use - Need API signatures for CLLocationUpdate, CLMonitor, CLServiceSession - Implementing geofencing or region monitoring - Configuring background location updates - Understanding authorization patterns - Debugging location service issues ## Related Skills - `axiom-core-location` — Anti-patterns, decision trees, pressure scenarios - `axiom-core-location-diag` — Symptom-based troubleshooting - `axiom-energy-ref` — Location as battery subsystem (accuracy vs power) --- ## Part 1: Modern API Overview (iOS 17+) Four key classes replace legacy CLLocationManager patterns: | Class | Purpose | iOS | |-------|---------|-----| | `CLLocationUpdate` | AsyncSequence for location updates | 17+ | | `CLMonitor` | Condition-based geofencing/beacons | 17+ | | `CLServiceSession` | Declarative authorization goals | 18+ | | `CLBackgroundActivitySession` | Background location support | 17+ | **Migration path**: Legacy CLLocationManager still works, but new APIs provide: - Swift concurrency (async/await) - Automatic pause/resume - Simplified authorization - Better battery efficiency --- ## Part 2: CLLocationUpdate API ### Basic Usage ```swift import CoreLocation Task { do { for try await update in CLLocationUpdate.liveUpdates() { if let location = update.location { // Process location } if update.isStationary { break // Stop when user stops moving } } } catch { // Handle location errors } } ``` ### LiveConfiguration Options ```swift CLLocationUpdate.liveUpdates(.default) CLLocationUpdate.liveUpdates(.automotiveNavigation) CLLocationUpdate.liveUpdates(.otherNavigation) CLLocationUpdate.liveUpdates(.fitness) CLLocationUpdate.liveUpdates(.airborne) ``` Choose based on use case. If unsure, use `.default` or omit parameter. ### Key Properties | Property | Type | Description | |----------|------|-------------| | `location` | `CLLocation?` | Current location (nil if unavailable) | | `isStationary` | `Bool` | True when device stopped moving | | `authorizationDenied` | `Bool` | User denied location access | | `authorizationDeniedGlobally` | `Bool` | Location services disabled system-wide | | `authorizationRequestInProgress` | `Bool` | Awaiting user authorization decision | | `accuracyLimited` | `Bool` | Reduced accuracy (updates every 15-20 min) | | `locationUnavailable` | `Bool` | Cannot determine location | | `insufficientlyInUse` | `Bool` | Can't request auth (not in foreground) | ### Automatic Pause/Resume When device becomes stationary: 1. Final update delivered with `isStationary = true` and valid `location` 2. Updates pause (saves battery) 3. When device moves, updates resume with `isStationary = false` No action required—happens automatically. ### AsyncSequence Operations ```swift // Get first location with speed > 10 m/s let fastUpdate = try await CLLocationUpdate.liveUpdates() .first { $0.location?.speed ?? 0 > 10 } // WARNING: Avoid filters that may never match (e.g., horizontalAccuracy < 1) ``` --- ## Part 3: CLMonitor API Swift actor for monitoring geographic conditions and beacons. ### Basic Geofencing ```swift let monitor = await CLMonitor("MyMonitor") // Add circular region let condition = CLMonitor.CircularGeographicCondition( center: CLLocationCoordinate2D(latitude: 37.33, longitude: -122.01), radius: 100 ) await monitor.add(condition, identifier: "ApplePark") // Await events for try await event in monitor.events { switch event.state { case .satisfied: // User entered region handleEntry(event.identifier) case .unsatisfied: // User exited region handleExit(event.identifier) case .unknown: break @unknown default: break } } ``` ### CircularGeographicCondition ```swift CLMonitor.CircularGeographicCondition( center: CLLocationCoordinate2D, radius: CLLocationDistance // meters, minimum ~100m effective ) ``` ### BeaconIdentityCondition Three granularity levels: ```swift // All beacons with UUID (any site) CLMonitor.BeaconIdentityCondition(uuid: myUUID) // Specific site (UUID + major) CLMonitor.BeaconIdentityCondition(uuid: myUUID, major: 100) // Specific beacon (UUID + major + minor) CLMonitor.BeaconIdentityCondition(uuid: myUUID, major: 100, minor: 5) ``` ### Condition Limit **Maximum 20 conditions per app.** Prioritize what to monitor. Swap regions dynamically based on user location if needed. ### Adding with Assumed State ```swift // If you know initial state await monitor.add(condition, identifier: "Work", assuming: .unsatisfied) ``` Core Location will correct if assumption wrong. ### Accessing Records ```swift // Get single record if let record = await monitor.record(for: "ApplePark") { let condition = record.condition let lastEvent = record.lastEvent let state = lastEvent.state let date = lastEvent.date } // Get all identifiers let allIds = await monitor.identifiers ``` ### Event Properties | Property | Description | |----------|-------------| | `identifier` | String identifier of condition | | `state` | `.satisfied`, `.unsatisfied`, `.unknown` | | `date` | When state changed | | `refinement` | For wildcard beacons, actual UUID/major/minor detected | | `conditionLimitExceeded` | Too many conditions (max 20) | | `conditionUnsupported` | Condition type not available | | `accuracyLimited` | Reduced accuracy prevents monitoring | ### Critical Requirements 1. **One monitor per name** — Only one instance with given name at a time 2. **Always await events** — Events only become `lastEvent` after handling 3. **Reinitialize on launch** — Recreate monitor in `didFinishLaunchingWithOptions` --- ## Part 4: CLServiceSession API (iOS 18+) Declarative authorization—tell Core Location what you need, not what to do. ### Basic Usage ```swift // Hold session for duration of feature let session = CLServiceSession(authorization: .whenInUse) for try await update in CLLocationUpdate.liveUpdates() { // Process updates } ``` ### Authorization Requirements ```swift CLServiceSession(authorization: .none) // No auth request CLServiceSession(authorization: .whenInUse) // Request When In Use CLServiceSession(authorization: .always) // Request Always (must start in foreground) ``` ### Full Accuracy Request ```swift // For features requiring precise location (e.g., navigation) CLServiceSession( authorization: .whenInUse, fullAccuracyPurposeKey: "NavigationPurpose" // Key in Info.plist ) ``` Requires `NSLocationTemporaryUsageDescriptionDictionary` in Info.plist. ### Implicit Sessions Iterating `CLLocationUpdate.liveUpdates()` or `CLMonitor.events` creates implicit session with `.whenInUse` goal. To disable implicit sessions: ```xml NSLocationRequireExplicitServiceSession ``` ### Session Layering Don't replace sessions—layer them: ```swift // Base session for app let baseSession = CLServiceSession(authorization: .whenInUse) // Additional session when navigation feature active let navSession = CLServiceSession( authorization: .whenInUse, fullAccuracyPurposeKey: "Nav" ) // Both sessions active simultaneously ``` ### Diagnostic Properties ```swift for try await diagnostic in session.diagnostics { if diagnostic.authorizationDenied { // User denied—offer alternative } if diagnostic.authorizationDeniedGlobally { // Location services off system-wide } if diagnostic.insufficientlyInUse { // Can't request auth (not foreground) } if diagnostic.alwaysAuthorizationDenied { // Always auth specifically denied } if !diagnostic.authorizationRequestInProgress { // Decision made (granted or denied) break } } ``` ### Session Lifecycle Sessions persist through: - App backgrounding - App suspension - App termination (Core Location tracks) On relaunch, recreate sessions immediately in `didFinishLaunchingWithOptions`. --- ## Part 5: Authorization State Machine ### Authorization Levels | Status | Description | |--------|-------------| | `.notDetermined` | User hasn't decided | | `.restricted` | Parental controls prevent access | | `.denied` | User explicitly refused | | `.authorizedWhenInUse` | Access while app active | | `.authorizedAlways` | Background access | ### Accuracy Authorization | Value | Description | |-------|-------------| | `.fullAccuracy` | Precise location | | `.reducedAccuracy` | Approximate (~5km), updates every 15-20 min | ### Required Info.plist Keys ```xml NSLocationWhenInUseUsageDescription We need your location to show nearby places NSLocationAlwaysAndWhenInUseUsageDescription We track your location to send arrival reminders NSLocationDefaultAccuracyReduced ``` ### Legacy Authorization Pattern ```swift @MainActor class LocationManager: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.authorizationStatus { case .notDetermined: manager.requestWhenInUseAuthorization() case .authorizedWhenInUse, .authorizedAlways: enableLocationFeatures() case .denied, .restricted: disableLocationFeatures() @unknown default: break } } } ``` --- ## Part 6: Background Location ### Requirements 1. **Background mode capability**: Signing & Capabilities → Background Modes → Location updates 2. **Info.plist**: Adds `UIBackgroundModes` with `location` value 3. **CLBackgroundActivitySession** or **LiveActivity** ### CLBackgroundActivitySession ```swift // Create and HOLD reference (deallocation invalidates session) var backgroundSession: CLBackgroundActivitySession? func startBackgroundTracking() { // Must start from foreground backgroundSession = CLBackgroundActivitySession() Task { for try await update in CLLocationUpdate.liveUpdates() { processUpdate(update) } } } func stopBackgroundTracking() { backgroundSession?.invalidate() backgroundSession = nil } ``` ### Background Indicator Blue status bar/pill appears when: - App authorized as "When In Use" - App receiving location in background - CLBackgroundActivitySession active ### App Lifecycle 1. **Foreground → Background**: Session continues 2. **Background → Suspended**: Session preserved, updates pause 3. **Suspended → Terminated**: Core Location tracks session 4. **Terminated → Background launch**: Recreate session immediately ```swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Recreate background session if was tracking if wasTrackingLocation { backgroundSession = CLBackgroundActivitySession() startLocationUpdates() } return true } ``` --- ## Part 7: Legacy APIs (iOS 12-16) ### CLLocationManager Delegate Pattern ```swift class LocationManager: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest manager.distanceFilter = 10 // meters } func startUpdates() { manager.startUpdatingLocation() } func stopUpdates() { manager.stopUpdatingLocation() } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let location = locations.last else { return } // Process location } } ``` ### Accuracy Constants | Constant | Accuracy | Battery Impact | |----------|----------|----------------| | `kCLLocationAccuracyBestForNavigation` | ~5m | Highest | | `kCLLocationAccuracyBest` | ~10m | Very High | | `kCLLocationAccuracyNearestTenMeters` | ~10m | High | | `kCLLocationAccuracyHundredMeters` | ~100m | Medium | | `kCLLocationAccuracyKilometer` | ~1km | Low | | `kCLLocationAccuracyThreeKilometers` | ~3km | Very Low | | `kCLLocationAccuracyReduced` | ~5km | Lowest | ### Legacy Region Monitoring ```swift // Deprecated in iOS 17, use CLMonitor instead let region = CLCircularRegion( center: coordinate, radius: 100, identifier: "MyRegion" ) region.notifyOnEntry = true region.notifyOnExit = true manager.startMonitoring(for: region) ``` ### Significant Location Changes Low-power alternative for coarse tracking: ```swift manager.startMonitoringSignificantLocationChanges() // Updates ~500m movements, works in background ``` ### Visit Monitoring Detect arrivals/departures: ```swift manager.startMonitoringVisits() func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) { let arrival = visit.arrivalDate let departure = visit.departureDate let coordinate = visit.coordinate } ``` --- ## Part 8: Geofencing Best Practices ### Region Size - **Minimum effective radius**: ~100 meters - **Smaller regions**: May not trigger reliably - **Larger regions**: More reliable but less precise ### 20-Region Limit Strategy ```swift // Dynamic region management func updateMonitoredRegions(userLocation: CLLocation) async { let nearbyPOIs = fetchNearbyPOIs(around: userLocation, limit: 20) // Remove old regions for id in await monitor.identifiers { if !nearbyPOIs.contains(where: { $0.id == id }) { await monitor.remove(id) } } // Add new regions for poi in nearbyPOIs { let condition = CLMonitor.CircularGeographicCondition( center: poi.coordinate, radius: 100 ) await monitor.add(condition, identifier: poi.id) } } ``` ### Entry/Exit Timing - **Entry**: Usually within seconds to minutes - **Exit**: May take 3-5 minutes after leaving - **Accuracy depends on**: Cell towers, WiFi, GPS availability ### Persistence - Conditions persist across app launches - Must reinitialize monitor with same name on launch - Core Location wakes app for events --- ## Part 9: Testing and Simulation ### Xcode Location Simulation 1. Run on simulator 2. Debug → Simulate Location → Choose location 3. Or use custom GPX file ### Custom GPX Route ```xml ``` ### Testing Authorization States Settings → Privacy & Security → Location Services: - Toggle app authorization - Toggle system-wide location services - Test reduced accuracy ### Console Filtering ```bash # Filter location logs log stream --predicate 'subsystem == "com.apple.locationd"' ``` --- ## Part 10: Swift Concurrency Integration ### Task Cancellation ```swift let locationTask = Task { for try await update in CLLocationUpdate.liveUpdates() { if Task.isCancelled { break } processUpdate(update) } } // Later locationTask.cancel() ``` ### MainActor Considerations ```swift @MainActor class LocationViewModel: ObservableObject { @Published var currentLocation: CLLocation? func startTracking() { Task { for try await update in CLLocationUpdate.liveUpdates() { // Already on MainActor, safe to update @Published self.currentLocation = update.location } } } } ``` ### Error Handling ```swift Task { do { for try await update in CLLocationUpdate.liveUpdates() { if update.authorizationDenied { throw LocationError.authorizationDenied } processUpdate(update) } } catch { handleError(error) } } ``` --- ## Part 11: Geocoding ### CLGeocoder — Forward Geocoding (Address → Coordinate) ```swift let geocoder = CLGeocoder() func geocodeAddress(_ address: String) async throws -> CLLocation? { let placemarks = try await geocoder.geocodeAddressString(address) return placemarks.first?.location } // With locale for localized results let placemarks = try await geocoder.geocodeAddressString( "1 Apple Park Way", in: nil, // CLRegion hint (optional) preferredLocale: Locale(identifier: "en_US") ) ``` ### CLGeocoder — Reverse Geocoding (Coordinate → Address) ```swift func reverseGeocode(_ location: CLLocation) async throws -> CLPlacemark? { let placemarks = try await geocoder.reverseGeocodeLocation(location) return placemarks.first } // Usage if let placemark = try await reverseGeocode(location) { let street = placemark.thoroughfare // "Apple Park Way" let city = placemark.locality // "Cupertino" let state = placemark.administrativeArea // "CA" let zip = placemark.postalCode // "95014" let country = placemark.country // "United States" let isoCountry = placemark.isoCountryCode // "US" } ``` ### CLPlacemark Key Properties | Property | Example | Notes | |----------|---------|-------| | `name` | "Apple Park" | Location name | | `thoroughfare` | "Apple Park Way" | Street name | | `subThoroughfare` | "1" | Street number | | `locality` | "Cupertino" | City | | `subLocality` | "Silicon Valley" | Neighborhood | | `administrativeArea` | "CA" | State/province | | `postalCode` | "95014" | ZIP/postal code | | `country` | "United States" | Country name | | `isoCountryCode` | "US" | ISO country code | | `timeZone` | America/Los_Angeles | Time zone | | `location` | CLLocation | Coordinate | ### Geocoding Rate Limits - **One request at a time** — CLGeocoder throws if a request is in progress - **Apple rate-limits** — Throttle to avoid `kCLErrorGeocodeCanceled` - **Cache results** — Don't re-geocode the same address/coordinate - **Batch carefully** — Add delays between sequential geocode requests ```swift // Check if geocoder is busy if geocoder.isGeocoding { geocoder.cancelGeocode() // Cancel previous before starting new } ``` --- ## Troubleshooting Quick Reference | Symptom | Check | |---------|-------| | No location updates | Authorization status, Info.plist keys | | Background not working | Background mode capability, CLBackgroundActivitySession | | Always auth not effective | CLServiceSession with `.always`, started in foreground | | Geofence not triggering | Region count (max 20), radius (min ~100m) | | Reduced accuracy only | Check `accuracyAuthorization`, request temporary full accuracy | | Location icon stays on | Ensure `stopUpdatingLocation()` or break from async loop | --- ## Resources **WWDC**: 2023-10180, 2023-10147, 2024-10212 **Docs**: /corelocation, /corelocation/clmonitor, /corelocation/cllocationupdate, /corelocation/clservicesession **Skills**: axiom-core-location, axiom-core-location-diag, axiom-energy-ref