Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-core-location-ref/.openskills.json
Normal file
7
.claude/skills/axiom-core-location-ref/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-core-location-ref",
|
||||
"installedAt": "2026-04-12T08:06:08.634Z"
|
||||
}
|
||||
746
.claude/skills/axiom-core-location-ref/SKILL.md
Normal file
746
.claude/skills/axiom-core-location-ref/SKILL.md
Normal file
@@ -0,0 +1,746 @@
|
||||
---
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Core Location Reference"
|
||||
short_description: "Core Location API reference"
|
||||
Reference in New Issue
Block a user