Use for Core Location API reference - CLLocationUpdate, CLMonitor, CLServiceSession, authorization, background location, geofencing
/plugin marketplace add CharlesWiltgen/Axiom/plugin install axiom@axiom-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive API reference for modern Core Location (iOS 17+).
axiom-core-location — Anti-patterns, decision trees, pressure scenariosaxiom-core-location-diag — Symptom-based troubleshootingaxiom-energy-ref — Location as battery subsystem (accuracy vs power)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:
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
}
}
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.
| 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) |
When device becomes stationary:
isStationary = true and valid locationisStationary = falseNo action required—happens automatically.
// 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)
Swift actor for monitoring geographic conditions and beacons.
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
}
}
CLMonitor.CircularGeographicCondition(
center: CLLocationCoordinate2D,
radius: CLLocationDistance // meters, minimum ~100m effective
)
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)
Maximum 20 conditions per app. Prioritize what to monitor. Swap regions dynamically based on user location if needed.
// If you know initial state
await monitor.add(condition, identifier: "Work", assuming: .unsatisfied)
Core Location will correct if assumption wrong.
// 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
| 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 |
lastEvent after handlingdidFinishLaunchingWithOptionsDeclarative authorization—tell Core Location what you need, not what to do.
// Hold session for duration of feature
let session = CLServiceSession(authorization: .whenInUse)
for try await update in CLLocationUpdate.liveUpdates() {
// Process updates
}
CLServiceSession(authorization: .none) // No auth request
CLServiceSession(authorization: .whenInUse) // Request When In Use
CLServiceSession(authorization: .always) // Request Always (must start in foreground)
// For features requiring precise location (e.g., navigation)
CLServiceSession(
authorization: .whenInUse,
fullAccuracyPurposeKey: "NavigationPurpose" // Key in Info.plist
)
Requires NSLocationTemporaryUsageDescriptionDictionary in Info.plist.
Iterating CLLocationUpdate.liveUpdates() or CLMonitor.events creates implicit session with .whenInUse goal.
To disable implicit sessions:
<!-- Info.plist -->
<key>NSLocationRequireExplicitServiceSession</key>
<true/>
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
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
}
}
Sessions persist through:
On relaunch, recreate sessions immediately in didFinishLaunchingWithOptions.
| Status | Description |
|---|---|
.notDetermined | User hasn't decided |
.restricted | Parental controls prevent access |
.denied | User explicitly refused |
.authorizedWhenInUse | Access while app active |
.authorizedAlways | Background access |
| Value | Description |
|---|---|
.fullAccuracy | Precise location |
.reducedAccuracy | Approximate (~5km), updates every 15-20 min |
<!-- 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/>
@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
}
}
}
UIBackgroundModes with location value// 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
}
Blue status bar/pill appears when:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Recreate background session if was tracking
if wasTrackingLocation {
backgroundSession = CLBackgroundActivitySession()
startLocationUpdates()
}
return true
}
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
}
}
| 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 |
// 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)
Low-power alternative for coarse tracking:
manager.startMonitoringSignificantLocationChanges()
// Updates ~500m movements, works in background
Detect arrivals/departures:
manager.startMonitoringVisits()
func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
let arrival = visit.arrivalDate
let departure = visit.departureDate
let coordinate = visit.coordinate
}
// 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)
}
}
<?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>
Settings → Privacy & Security → Location Services:
# Filter location logs
log stream --predicate 'subsystem == "com.apple.locationd"'
let locationTask = Task {
for try await update in CLLocationUpdate.liveUpdates() {
if Task.isCancelled { break }
processUpdate(update)
}
}
// Later
locationTask.cancel()
@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
}
}
}
}
Task {
do {
for try await update in CLLocationUpdate.liveUpdates() {
if update.authorizationDenied {
throw LocationError.authorizationDenied
}
processUpdate(update)
}
} catch {
handleError(error)
}
}
| 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 |
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
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.