---
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
<!-- Info.plist -->
<key>NSLocationRequireExplicitServiceSession</key>
<true/>
```

### 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
<!-- 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

```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
<?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

```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


---
Source attribution: CharlesWiltgen
Original project: https://github.com/CharlesWiltgen/Axiom/blob/a66a3c0b44011011fed256c49954ba3c77075a1e/.claude-plugin/plugins/axiom/skills/axiom-core-location-ref/SKILL.md
License: MIT
License information: https://opensource.org/license/mit
Original upstream instructions; downloaded from Dave AI.

## Upstream license notice

MIT License

Copyright (c) 2025 Charles Wiltgen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



# Dave AI resource metadata

Creator / publisher: CharlesWiltgen
Original project: https://github.com/CharlesWiltgen/Axiom/blob/a66a3c0b44011011fed256c49954ba3c77075a1e/.claude-plugin/plugins/axiom/skills/axiom-core-location-ref/SKILL.md
Attribution basis: Recovered original package and original project license at a pinned revision

Fields of work: Development & Engineering, Product Management
Roles: Backend Developer, Mobile Developer, Product Manager
Specialties: API, Mobile
Tools: Tool not specified
Country applicability: Country not specified
Classification basis: Source catalog labels
Instruction SHA-256: 2048580f7e0e60832c71b22883f571b8088d68ec79cb00ff3934c1d9002b078a
Metadata reviewed: 2026-10-27
This metadata supersedes earlier Dave AI catalog attribution. Original instruction and supporting files are preserved.
