Use for Core Location API reference - CLLocationUpdate, CLMonitor, CLServiceSession, authorization, background location, geofencing
Installation
Details
Usage
After installing, this skill will be available to your AI coding assistant.
Verify installation:
npx agent-skills-cli listSkill Instructions
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 scenariosaxiom-core-location-diag— Symptom-based troubleshootingaxiom-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
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
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:
- Final update delivered with
isStationary = trueand validlocation - Updates pause (saves battery)
- When device moves, updates resume with
isStationary = false
No action required—happens automatically.
AsyncSequence Operations
// 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
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
CLMonitor.CircularGeographicCondition(
center: CLLocationCoordinate2D,
radius: CLLocationDistance // meters, minimum ~100m effective
)
BeaconIdentityCondition
Three granularity levels:
// 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
// If you know initial state
await monitor.add(condition, identifier: "Work", assuming: .unsatisfied)
Core Location will correct if assumption wrong.
Accessing Records
// 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
- One monitor per name — Only one instance with given name at a time
- Always await events — Events only become
lastEventafter handling - 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
// Hold session for duration of feature
let session = CLServiceSession(authorization: .whenInUse)
for try await update in CLLocationUpdate.liveUpdates() {
// Process updates
}
Authorization Requirements
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
// 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:
<!-- Info.plist -->
<key>NSLocationRequireExplicitServiceSession</key>
<true/>
Session Layering
Don't replace sessions—layer them:
// 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
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
<!-- Required for When In Use -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show nearby places</string>
<!-- Required for Always -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We track your location to send arrival reminders</string>
<!-- Optional: default to reduced accuracy -->
<key>NSLocationDefaultAccuracyReduced</key>
<true/>
Legacy Authorization Pattern
@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
- Background mode capability: Signing & Capabilities → Background Modes → Location updates
- Info.plist: Adds
UIBackgroundModeswithlocationvalue - CLBackgroundActivitySession or LiveActivity
CLBackgroundActivitySession
// 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
- Foreground → Background: Session continues
- Background → Suspended: Session preserved, updates pause
- Suspended → Terminated: Core Location tracks session
- Terminated → Background launch: Recreate session immediately
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
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
// 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:
manager.startMonitoringSignificantLocationChanges()
// Updates ~500m movements, works in background
Visit Monitoring
Detect arrivals/departures:
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
// 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
- Run on simulator
- Debug → Simulate Location → Choose location
- Or use custom GPX file
Custom GPX Route
<?xml version="1.0"?>
<gpx version="1.1">
<wpt lat="37.331686" lon="-122.030656">
<time>2024-01-01T00:00:00Z</time>
</wpt>
<wpt lat="37.332686" lon="-122.031656">
<time>2024-01-01T00:00:10Z</time>
</wpt>
</gpx>
Testing Authorization States
Settings → Privacy & Security → Location Services:
- Toggle app authorization
- Toggle system-wide location services
- Test reduced accuracy
Console Filtering
# Filter location logs
log stream --predicate 'subsystem == "com.apple.locationd"'
Part 10: Swift Concurrency Integration
Task Cancellation
let locationTask = Task {
for try await update in CLLocationUpdate.liveUpdates() {
if Task.isCancelled { break }
processUpdate(update)
}
}
// Later
locationTask.cancel()
MainActor Considerations
@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
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)
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)
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
// 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
More by CharlesWiltgen
View allUse when RealityKit entities not visible, anchors not tracking, gestures not responding, performance drops, materials wrong, or multiplayer sync fails
Use when writing unit tests, adopting Swift Testing framework, making tests run faster without simulator, architecting code for testability, testing async code reliably, or migrating from XCTest - covers @Test/@Suite macros,
Use when working with SceneKit 3D scenes, migrating SceneKit to RealityKit, or maintaining legacy SceneKit code. Covers scene graph, materials, physics, animation, SwiftUI bridge, migration decision tree.
Use when encountering BUILD FAILED, test crashes, simulator hangs, stale builds, zombie xcodebuild processes, "Unable to boot simulator", "No such module" after SPM changes, or mysterious test failures despite no code changes - systematic environment-first diagnostics for iOS/macOS projects
