Agent Skills: Core Location Patterns

Use for Core Location implementation patterns - authorization strategy, monitoring strategy, accuracy selection, background location

UncategorizedID: charleswiltgen/axiom/axiom-core-location

Install this agent skill to your local

pnpm dlx add-skill https://github.com/CharlesWiltgen/Axiom/tree/HEAD/.claude-plugin/plugins/axiom/skills/axiom-core-location

Skill Files

Browse the full folder contents for axiom-core-location.

Download Skill

Loading file tree…

.claude-plugin/plugins/axiom/skills/axiom-core-location/SKILL.md

Skill Metadata

Name
axiom-core-location
Description
Use for Core Location implementation patterns - authorization strategy, monitoring strategy, accuracy selection, background location

Core Location Patterns

Discipline skill for Core Location implementation decisions. Prevents common authorization mistakes, battery drain, and background location failures.

When to Use

  • Choosing authorization strategy (When In Use vs Always)
  • Deciding monitoring approach (continuous vs significant-change vs CLMonitor)
  • Implementing geofencing or background location
  • Debugging "location not working" issues
  • Reviewing location code for anti-patterns

Related Skills

  • axiom-core-location-ref — API reference, code examples
  • axiom-core-location-diag — Symptom-based troubleshooting
  • axiom-energy — Location as battery subsystem

Part 1: Anti-Patterns (with Time Costs)

Anti-Pattern 1: Premature Always Authorization

Wrong (30-60% denial rate):

// First launch: "Can we have Always access?"
manager.requestAlwaysAuthorization()

Right (5-10% denial rate):

// Start with When In Use
CLServiceSession(authorization: .whenInUse)

// Later, when user triggers background feature:
CLServiceSession(authorization: .always)

Time cost: 15 min to fix code, but 30-60% of users permanently denied = feature adoption destroyed.

Why: Users deny aggressive requests. Start minimal, upgrade when user understands value.


Anti-Pattern 2: Continuous Updates for Geofencing

Wrong (10x battery drain):

for try await update in CLLocationUpdate.liveUpdates() {
    if isNearTarget(update.location) {
        triggerGeofence()
    }
}

Right (system-managed, low power):

let monitor = await CLMonitor("Geofences")
let condition = CLMonitor.CircularGeographicCondition(
    center: target, radius: 100
)
await monitor.add(condition, identifier: "Target")

for try await event in monitor.events {
    if event.state == .satisfied { triggerGeofence() }
}

Time cost: 5 min to refactor, saves 10x battery.


Anti-Pattern 3: Ignoring Stationary Detection

Wrong (wasted battery):

for try await update in CLLocationUpdate.liveUpdates() {
    processLocation(update.location)
    // Never stops, even when device stationary
}

Right (automatic pause/resume):

for try await update in CLLocationUpdate.liveUpdates() {
    if let location = update.location {
        processLocation(location)
    }
    if update.isStationary, let location = update.location {
        // Device stopped moving - updates pause automatically
        // Will resume when device moves again
        saveLastKnownLocation(location)
    }
}

Time cost: 2 min to add check, saves significant battery.


Anti-Pattern 4: No Graceful Denial Handling

Wrong (broken UX):

for try await update in CLLocationUpdate.liveUpdates() {
    guard let location = update.location else { continue }
    // User denied - silent failure, no feedback
}

Right (graceful degradation):

for try await update in CLLocationUpdate.liveUpdates() {
    if update.authorizationDenied {
        showManualLocationPicker()
        break
    }
    if update.authorizationDeniedGlobally {
        showSystemLocationDisabledMessage()
        break
    }
    if let location = update.location {
        processLocation(location)
    }
}

Time cost: 10 min to add handling, prevents confused users.


Anti-Pattern 5: Wrong Accuracy for Use Case

Wrong (battery drain for weather app):

// Weather app using navigation accuracy
CLLocationUpdate.liveUpdates(.automotiveNavigation)

Right (match accuracy to need):

// Weather: city-level is fine
CLLocationUpdate.liveUpdates(.default)  // or .fitness for runners

// Navigation: needs high accuracy
CLLocationUpdate.liveUpdates(.automotiveNavigation)

| Use Case | Configuration | Accuracy | Battery | |----------|---------------|----------|---------| | Navigation | .automotiveNavigation | ~5m | Highest | | Fitness tracking | .fitness | ~10m | High | | Store finder | .default | ~10-100m | Medium | | Weather | .default | ~100m+ | Low |

Time cost: 1 min to change, significant battery savings.


Anti-Pattern 6: Not Stopping Updates

Wrong (battery drain, location icon persists):

func viewDidLoad() {
    Task {
        for try await update in CLLocationUpdate.liveUpdates() {
            updateMap(update.location)
        }
    }
}
// User navigates away, updates continue forever

Right (cancel when done):

private var locationTask: Task<Void, Error>?

func startTracking() {
    locationTask = Task {
        for try await update in CLLocationUpdate.liveUpdates() {
            if Task.isCancelled { break }
            updateMap(update.location)
        }
    }
}

func stopTracking() {
    locationTask?.cancel()
    locationTask = nil
}

Time cost: 5 min to add cancellation, stops battery drain.


Anti-Pattern 7: Ignoring CLServiceSession (iOS 18+)

Wrong (procedural authorization juggling):

func requestAuth() {
    switch manager.authorizationStatus {
    case .notDetermined:
        manager.requestWhenInUseAuthorization()
    case .authorizedWhenInUse:
        if needsFullAccuracy {
            manager.requestTemporaryFullAccuracyAuthorization(...)
        }
    // Complex state machine...
    }
}

Right (declarative goals):

// Just declare what you need - Core Location handles the rest
let session = CLServiceSession(authorization: .whenInUse)

// For feature needing full accuracy
let navSession = CLServiceSession(
    authorization: .whenInUse,
    fullAccuracyPurposeKey: "Navigation"
)

// Monitor diagnostics if needed
for try await diag in session.diagnostics {
    if diag.authorizationDenied { handleDenial() }
}

Time cost: 30 min to migrate, simpler code, fewer bugs.


Part 2: Decision Trees

Authorization Strategy

Q1: Does your feature REQUIRE background location?
├─ NO → Use .whenInUse
│   └─ Q2: Does any feature need precise location?
│       ├─ ALWAYS → Add fullAccuracyPurposeKey to session
│       └─ SOMETIMES → Layer full-accuracy session when feature active
│
└─ YES → Start with .whenInUse, upgrade to .always when user triggers feature
    └─ Q3: When does user first need background location?
        ├─ IMMEDIATELY (e.g., fitness tracker) → Request .always on first relevant action
        └─ LATER (e.g., geofence reminders) → Add .always session when user creates first geofence

Monitoring Strategy

Q1: What are you monitoring for?
├─ USER POSITION (continuous tracking)
│   └─ Use CLLocationUpdate.liveUpdates()
│       └─ Q2: What activity?
│           ├─ Driving navigation → .automotiveNavigation
│           ├─ Walking/cycling nav → .otherNavigation
│           ├─ Fitness tracking → .fitness
│           ├─ Airplane apps → .airborne
│           └─ General → .default or omit
│
├─ ENTRY/EXIT REGIONS (geofencing)
│   └─ Use CLMonitor with CircularGeographicCondition
│       └─ Note: Maximum 20 conditions per app
│
├─ BEACON PROXIMITY
│   └─ Use CLMonitor with BeaconIdentityCondition
│       └─ Choose granularity: UUID only, UUID+major, UUID+major+minor
│
└─ SIGNIFICANT CHANGES ONLY (lowest power)
    └─ Use startMonitoringSignificantLocationChanges() (legacy)
        └─ Updates ~500m movements, works in background

Accuracy Selection

Q1: What's the minimum accuracy that makes your feature work?
├─ TURN-BY-TURN NAV needs 5-10m → .automotiveNavigation / .otherNavigation
├─ FITNESS TRACKING needs 10-20m → .fitness
├─ STORE FINDER needs 100m → .default
├─ WEATHER/CITY needs 1km+ → .default (reduced accuracy acceptable)
└─ GEOFENCING uses system determination → CLMonitor handles it

Q2: Will user be moving fast?
├─ DRIVING (high speed) → .automotiveNavigation (extra processing for speed)
├─ CYCLING/WALKING → .otherNavigation
└─ STATIONARY/SLOW → .default

Always start with lowest acceptable accuracy. Higher accuracy = higher battery drain.

Part 3: Pressure Scenarios

Scenario 1: "Just Use Always Authorization"

Context: PM says "Users want location reminders. Just request Always access on first launch so it works."

Pressure: Ship fast, seems simpler.

Reality:

  • 30-60% of users will deny Always authorization when asked upfront
  • Users who deny can only re-enable in Settings (most won't)
  • Feature adoption destroyed before users understand value

Response:

"Always authorization has 30-60% denial rates when requested upfront. We should start with When In Use, then request Always upgrade when the user creates their first location reminder. This gives us a 5-10% denial rate because users understand why they need it."

Evidence: Apple's own guidance in WWDC 2024-10212: "CLServiceSessions should be taken proactively... hold one requiring full-accuracy when people engage a feature that would warrant a special ask for it."


Scenario 2: "Location Isn't Working in Background"

Context: QA reports "App stops getting location when backgrounded."

Pressure: Quick fix before release.

Wrong fixes:

  • Add all background modes
  • Use allowsBackgroundLocationUpdates = true without understanding
  • Request Always authorization

Right diagnosis:

  1. Check background mode capability exists
  2. Check CLBackgroundActivitySession is held (not deallocated)
  3. Check session started from foreground
  4. Check authorization level (.whenInUse works with CLBackgroundActivitySession)

Response:

"Background location requires specific setup. Let me check: (1) Background mode capability, (2) CLBackgroundActivitySession held during tracking, (3) session started from foreground. Missing any of these causes silent failure."

Checklist:

// 1. Signing & Capabilities → Background Modes → Location updates
// 2. Hold session reference (property, not local variable)
var backgroundSession: CLBackgroundActivitySession?

func startBackgroundTracking() {
    // 3. Must start from foreground
    backgroundSession = CLBackgroundActivitySession()
    startLocationUpdates()
}

Scenario 3: "Geofence Events Aren't Firing"

Context: Geofences work in testing but not in production for some users.

Pressure: "It works on my device" dismissal.

Common causes:

  1. Too many conditions: Maximum 20 per app
  2. Radius too small: Minimum ~100m for reliable triggering
  3. Overlapping regions: Can cause confusion
  4. Not awaiting events: Events only become lastEvent after handled
  5. Not reinitializing on launch: Monitor must be recreated

Response:

"Geofencing has several system constraints. Check: (1) Are we within the 20-condition limit? (2) Are all radii at least 100m? (3) Is the app reinitializing CLMonitor on launch? (4) Is the app always awaiting on monitor.events?"

Diagnostic code:

// Check condition count
let count = await monitor.identifiers.count
if count >= 20 {
    print("At 20-condition limit!")
}

// Check all conditions
for id in await monitor.identifiers {
    if let record = await monitor.record(for: id) {
        let condition = record.condition
        if let geo = condition as? CLMonitor.CircularGeographicCondition {
            if geo.radius < 100 {
                print("Radius too small: \(id)")
            }
        }
    }
}

Part 4: Checklists

Pre-Release Location Checklist

Info.plist:

  • [ ] NSLocationWhenInUseUsageDescription with clear explanation
  • [ ] NSLocationAlwaysAndWhenInUseUsageDescription if using Always (clear why background needed)
  • [ ] NSLocationDefaultAccuracyReduced if reduced accuracy acceptable
  • [ ] NSLocationTemporaryUsageDescriptionDictionary if requesting temporary full accuracy
  • [ ] UIBackgroundModes includes location if background tracking

Authorization:

  • [ ] Start with minimal authorization (.whenInUse)
  • [ ] Upgrade to .always only when user triggers background feature
  • [ ] Handle authorization denial gracefully (offer alternatives)
  • [ ] Handle global location services disabled
  • [ ] Test with reduced accuracy authorization

Updates:

  • [ ] Using appropriate LiveConfiguration for use case
  • [ ] Handling isStationary for pause/resume
  • [ ] Cancelling location tasks when feature inactive
  • [ ] Not using continuous updates for geofencing

Testing:

  • [ ] Tested authorization denial flow
  • [ ] Tested reduced accuracy mode
  • [ ] Tested background-to-foreground transitions
  • [ ] Tested app termination and relaunch recovery

Background Location Checklist

Setup:

  • [ ] Background mode capability added (Location updates)
  • [ ] CLBackgroundActivitySession created and HELD (not local variable)
  • [ ] Session started from foreground
  • [ ] Updates restarted on background launch in didFinishLaunchingWithOptions

Authorization:

  • [ ] Using .whenInUse with CLBackgroundActivitySession, OR
  • [ ] Using .always (but only if needed beyond background indicator)

Lifecycle:

  • [ ] Persisting "was tracking" state for relaunch recovery
  • [ ] Recreating CLBackgroundActivitySession on background launch
  • [ ] Restarting CLLocationUpdate iteration on launch
  • [ ] CLMonitor reinitialized with same name on launch

Testing:

  • [ ] Blue background location indicator appears when backgrounded
  • [ ] Updates continue when app backgrounded
  • [ ] Updates resume after app suspended and resumed
  • [ ] Updates resume after app terminated and relaunched

Part 5: iOS Version Considerations

| Feature | iOS Version | Notes | |---------|-------------|-------| | CLLocationUpdate | iOS 17+ | AsyncSequence API | | CLMonitor | iOS 17+ | Replaces CLCircularRegion | | CLBackgroundActivitySession | iOS 17+ | Background with blue indicator | | CLServiceSession | iOS 18+ | Declarative authorization | | Implicit service sessions | iOS 18+ | From iterating liveUpdates | | CLLocationManager | iOS 2+ | Legacy but still works |

For iOS 14-16 support: Use CLLocationManager delegate pattern (see core-location-ref Part 7).

For iOS 17+: Prefer CLLocationUpdate and CLMonitor.

For iOS 18+: Add CLServiceSession for declarative authorization.


Resources

WWDC: 2023-10180, 2023-10147, 2024-10212

Docs: /corelocation, /corelocation/clmonitor, /corelocation/cllocationupdate, /corelocation/clservicesession

Skills: axiom-core-location-ref, axiom-core-location-diag, axiom-energy