Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
90
90%
Does it follow best practices?
Impact
—
Average score across 248 eval scenarios
Advisory
Suggest reviewing before use
Extended patterns for CoreLocation on iOS 17+ with modern Swift concurrency.
Import CoreLocation in every file that uses these APIs.
import CoreLocationfunc startReceivingLocation() async {
for try await update in CLLocationUpdate.liveUpdates() {
guard let location = update.location else { continue }
print("Lat: \(location.coordinate.latitude), Lon: \(location.coordinate.longitude)")
print("Accuracy: \(location.horizontalAccuracy)m")
}
}Filter out stale, inaccurate, or duplicate updates to avoid unnecessary UI refreshes and reduce battery impact.
@MainActor
@Observable
final class LocationService {
var currentLocation: CLLocation?
var isTracking = false
private var trackingTask: Task<Void, Never>?
private var lastReportedLocation: CLLocation?
/// Minimum distance in meters between reported locations.
private let distanceFilter: CLLocationDistance = 10
/// Maximum acceptable horizontal accuracy in meters.
private let accuracyThreshold: CLLocationAccuracy = 100
/// Maximum acceptable age for a delivered location.
private let maximumLocationAge: TimeInterval = 15
func startTracking() {
guard !isTracking else { return }
isTracking = true
trackingTask = Task {
do {
let updates = CLLocationUpdate.liveUpdates(.default)
for try await update in updates {
if Task.isCancelled { break }
// Skip updates without a location
guard let location = update.location else { continue }
// Skip inaccurate readings
guard location.horizontalAccuracy >= 0,
location.horizontalAccuracy < accuracyThreshold else {
continue
}
// Skip stale readings
guard abs(location.timestamp.timeIntervalSinceNow) < maximumLocationAge else {
continue
}
// Skip impossible movement when speed/course are available
if location.speed >= 0, location.speed > 80 {
continue
}
if location.course >= 0, location.courseAccuracy < 0 {
continue
}
// Skip if the user has not moved enough
if let last = lastReportedLocation,
location.distance(from: last) < distanceFilter {
continue
}
lastReportedLocation = location
currentLocation = location
}
} catch is CancellationError {
// Expected when tracking stops.
} catch {
currentLocation = nil
}
isTracking = false
}
}
func stopTracking() {
trackingTask?.cancel()
trackingTask = nil
isTracking = false
}
}CLLocationUpdate.liveUpdates(_:) accepts a LiveConfiguration parameter:
// Default: balanced power and accuracy
CLLocationUpdate.liveUpdates(.default)
// Best for navigation: highest accuracy, most frequent updates
CLLocationUpdate.liveUpdates(.automotiveNavigation)
// Fitness tracking
CLLocationUpdate.liveUpdates(.fitness)
// Other-navigation (non-automotive)
CLLocationUpdate.liveUpdates(.otherNavigation)
// Airborne: for drone or aviation apps
CLLocationUpdate.liveUpdates(.airborne)Keep every liveUpdates() stream in an owned Task so the feature can cancel
it when the map, route, or monitoring workflow stops. Treat diagnostics and bad
location samples as state changes: update UI, fall back to cached/manual input,
or suspend background work instead of continuing an invisible loop.
for try await update in CLLocationUpdate.liveUpdates() {
// Check authorization status (iOS 18+)
if update.authorizationDenied {
// User denied location; prompt to open Settings
break
}
if update.authorizationDeniedGlobally {
// Location Services disabled system-wide
break
}
if update.insufficientlyInUse {
// App does not meet in-use requirements
continue
}
if update.locationUnavailable {
// Temporarily unable to determine location; keep iterating
continue
}
if update.stationary {
// Device stopped moving; updates will pause
continue
}
guard let location = update.location else { continue }
// Use location
}Note: authorizationDenied, authorizationDeniedGlobally,
insufficientlyInUse, locationUnavailable, and stationary are only
available on iOS 18+. On iOS 17, check update.location == nil to detect
unavailable location.
Map diagnostics to user-visible behavior instead of silently waiting:
authorizationDenied: stop location work and show a Settings recovery path.authorizationDeniedGlobally: explain that system Location Services are off.insufficientlyInUse: suspend live updates until the feature is active again.locationUnavailable: keep the feature usable with cached, typed, or map-region
input while waiting for recovery.Before publishing a location, filter for valid horizontal accuracy, acceptable
timestamp age, and plausible movement. Use speed and course only when their
values are valid; negative values mean the measurement is unavailable.
CLServiceSession declares your authorization requirements for a feature.
Hold a strong reference for the session's entire duration.
@MainActor
@Observable
final class LocationFeature {
private var serviceSession: CLServiceSession?
private var locationTask: Task<Void, Never>?
func activate() {
// Declare that this feature needs when-in-use authorization
serviceSession = CLServiceSession(authorization: .whenInUse)
locationTask = Task {
do {
for try await update in CLLocationUpdate.liveUpdates() {
guard let location = update.location else { continue }
// process location
}
} catch is CancellationError {
// Expected when the feature stops.
} catch {
// Show a degraded state or retry from user action.
}
}
}
func deactivate() {
locationTask?.cancel()
locationTask = nil
// Release the session to signal you no longer need location
serviceSession = nil
}
}Request full accuracy when the user has granted approximate-only permission:
// Requires Info.plist:
// NSLocationTemporaryUsageDescriptionDictionary
// NearbySearchPurpose: "Show nearby stores within walking distance."
let session = CLServiceSession(
authorization: .whenInUse,
fullAccuracyPurposeKey: "NearbySearchPurpose"
)Only use .always when you need the system to relaunch your app in the
background for significant location changes after termination.
let session = CLServiceSession(authorization: .always)Requires NSLocationAlwaysAndWhenInUseUsageDescription in Info.plist.
On iOS 18+, CLLocationUpdate.liveUpdates() and CLMonitor create an
implicit CLServiceSession behind the scenes if you do not create one. You
need an explicit session when:
.always authorizationfullAccuracyPurposeKeyNSLocationRequireExplicitServiceSession to Info.plist)Use CLMonitor for modern condition monitoring. It is an actor, so its
async APIs require await.
@available(iOS 17, *)
actor GeofenceMonitor {
private var monitor: CLMonitor?
private var monitoringTask: Task<Void, any Error>?
func startMonitoring(regions: [GeofenceRegion]) async {
let monitor = await CLMonitor("myAppGeofences")
self.monitor = monitor
// Add circular geographic conditions
for region in regions {
let condition = CLMonitor.CircularGeographicCondition(
center: region.center,
radius: region.radius
)
await monitor.add(condition, identifier: region.id)
}
// Listen for events
monitoringTask = Task {
for try await event in await monitor.events {
switch event.state {
case .satisfied:
// Device entered the region
handleEntry(identifier: event.identifier)
case .unsatisfied:
// Device exited the region
handleExit(identifier: event.identifier)
case .unknown:
break
default:
break
}
}
}
}
func stopMonitoring() async {
monitoringTask?.cancel()
monitoringTask = nil
if let monitor {
for identifier in await monitor.identifiers {
await monitor.remove(identifier)
}
}
monitor = nil
}
private func handleEntry(identifier: String) {
print("Entered region: \(identifier)")
}
private func handleExit(identifier: String) {
print("Exited region: \(identifier)")
}
}
struct GeofenceRegion: Identifiable {
let id: String
let center: CLLocationCoordinate2D
let radius: CLLocationDistance
}Maximum 20 conditions per app. Adding more causes excess conditions
to report unmonitored state. This limit is per-app, not per-monitor.
Do not recreate CLMonitor instances rapidly. Creating a monitor with
the same name while one is still alive crashes the app. Reuse the instance
and call add/remove to change conditions.
Subscribe to events exactly once per CLMonitor. Cancelling and
re-subscribing causes the new subscription to immediately cancel. Keep a
single long-lived subscription.
Use diffing for condition updates. Instead of removing all conditions and re-adding them, calculate which to add and which to remove.
Target iOS 18+ for best results. Pair CLMonitor with
CLServiceSession for reliable authorization management.
Specify an assumed initial state to avoid spurious events on first add:
await monitor.add(condition, identifier: "office", assuming: .unsatisfied)Use .unsatisfied when you believe the device is outside the region. Use
.satisfied when you believe the device is inside.
func updateRegions(_ newRegions: [GeofenceRegion]) async {
guard let monitor else { return }
let existingIDs = Set(await monitor.identifiers)
let newIDs = Set(newRegions.map(\.id))
// Remove stale conditions
for id in existingIDs.subtracting(newIDs) {
await monitor.remove(id)
}
// Add new conditions
for region in newRegions where !existingIDs.contains(region.id) {
let condition = CLMonitor.CircularGeographicCondition(
center: region.center,
radius: region.radius
)
await monitor.add(condition, identifier: region.id, assuming: .unsatisfied)
}
}if let record = await monitor.record(for: "office") {
let lastState = record.lastEvent.state
let lastDate = record.lastEvent.date
print("Region 'office' was \(lastState) at \(lastDate)")
}Allow a when-in-use authorized app to receive location updates in the
background. Requires the Location updates background mode capability.
@available(iOS 17, *)
actor BackgroundLocationTracker {
private var backgroundSession: CLBackgroundActivitySession?
private var serviceSession: CLServiceSession?
private var trackingTask: Task<Void, Never>?
func startBackgroundTracking() {
// Declare authorization intent (iOS 18+)
serviceSession = CLServiceSession(authorization: .whenInUse)
// Start background activity session -- shows blue location indicator
backgroundSession = CLBackgroundActivitySession()
trackingTask = Task {
do {
for try await update in CLLocationUpdate.liveUpdates(.fitness) {
guard let location = update.location else { continue }
// Record location for fitness tracking, navigation, etc.
await recordLocation(location)
}
} catch is CancellationError {
// Expected when background tracking stops.
} catch {
// Persist an error state or retry from user action.
}
}
}
func stopBackgroundTracking() {
trackingTask?.cancel()
trackingTask = nil
backgroundSession?.invalidate()
backgroundSession = nil
serviceSession = nil
}
private func recordLocation(_ location: CLLocation) async {
// Persist to database, update Live Activity, etc.
}
}To receive location in the background you need ALL of these:
Background Modes > Location updates capability enabled.NSLocationWhenInUseUsageDescription in Info.plist..whenInUse or .always authorization granted.CLBackgroundActivitySession held or a Live Activity running.CLLocationUpdate or CLMonitor subscription..always authorization is NOT required for background location. The
difference: with .always, the system can relaunch your terminated app for
significant location changes. With .whenInUse + background session, the
app must be running (foreground or suspended).
Use when you only need coarse location updates at ~500-meter intervals. Extremely battery efficient because it piggybacks on cellular tower changes.
// Legacy CLLocationManager approach (still valid, no modern replacement)
let manager = CLLocationManager()
manager.startMonitoringSignificantLocationChanges()
// The delegate receives updates when the device moves ~500m+ from the
// last reported location. Updates arrive 1-5 minutes apart.There is no CLLocationUpdate equivalent for significant location changes.
Use CLLocationManager for this specific use case.
Detect when the user arrives at or departs from a place. Useful for journaling, check-in, and context-aware features.
let manager = CLLocationManager()
manager.startMonitoringVisits()
// Delegate callback:
func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
let coordinate = visit.coordinate
let arrivalDate = visit.arrivalDate
let departureDate = visit.departureDate
// departureDate == .distantFuture means the user is still at the location
}let region = CLCircularRegion(center: coordinate, radius: 200, identifier: "office")
region.notifyOnEntry = true
region.notifyOnExit = true
manager.startMonitoring(for: region)// MODERN
let monitor = await CLMonitor("appMonitor")
let condition = CLMonitor.CircularGeographicCondition(center: coordinate, radius: 200)
await monitor.add(condition, identifier: "office")
for try await event in await monitor.events {
if event.identifier == "office" {
switch event.state {
case .satisfied: handleEntry()
case .unsatisfied: handleExit()
default: break
}
}
}Key differences:
| Aspect | CLCircularRegion | CLMonitor |
|---|---|---|
| API style | Delegate callbacks | Async sequence |
| Max regions | 20 per app | 20 per app |
| Entry/exit | Separate booleans | State enum (satisfied/unsatisfied) |
| Concurrency | @objc delegate | Actor-based |
| Min iOS | iOS 7 | iOS 17 |
manager.desiredAccuracy = kCLLocationAccuracyBest // GPS, ~5m, highest power
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters // ~10m
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters // WiFi, ~100m
manager.desiredAccuracy = kCLLocationAccuracyKilometer // Cell tower, ~1km
manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers // ~3km, lowest power
manager.desiredAccuracy = kCLLocationAccuracyReduced // ~5km, privacy-safemanager.activityType = .other // Default
manager.activityType = .automotiveNavigation // Highway speeds, high accuracy
manager.activityType = .fitness // Walking/running
manager.activityType = .otherNavigation // Boats, trains
manager.activityType = .airborne // Drones, aircraft (iOS 12+)CLLocationUpdate.liveUpdates() does not support desiredAccuracy or
distanceFilter. Filter the stream yourself:
for try await update in CLLocationUpdate.liveUpdates() {
guard let location = update.location,
location.horizontalAccuracy < 50,
location.horizontalAccuracy >= 0 else { continue }
// Use filtered location
}In Xcode: Debug > Simulate Location > choose a city or custom coordinate.
Create a .gpx file and add it to your Xcode project:
<?xml version="1.0"?>
<gpx version="1.1" creator="Xcode">
<wpt lat="37.3349" lon="-122.0090">
<time>2025-01-01T00:00:00Z</time>
<name>Apple Park</name>
</wpt>
<wpt lat="37.3318" lon="-122.0312">
<time>2025-01-01T00:01:00Z</time>
<name>Infinite Loop</name>
</wpt>
<wpt lat="37.3230" lon="-122.0322">
<time>2025-01-01T00:02:00Z</time>
<name>De Anza College</name>
</wpt>
</gpx>Set this file in the scheme: Edit Scheme > Run > Options > Default Location.
The simulator interpolates between waypoints using timestamps. Playback loops automatically when it reaches the last waypoint.
Use CLLocationManager with XCTest by injecting a location protocol:
protocol LocationProviding: Sendable {
func updates() -> AsyncStream<CLLocation>
}
// Production
struct LiveLocationProvider: LocationProviding {
func updates() -> AsyncStream<CLLocation> {
AsyncStream { continuation in
Task {
for try await update in CLLocationUpdate.liveUpdates() {
if let location = update.location {
continuation.yield(location)
}
}
continuation.finish()
}
}
}
}
// Test mock
struct MockLocationProvider: LocationProviding {
let locations: [CLLocation]
func updates() -> AsyncStream<CLLocation> {
AsyncStream { continuation in
for location in locations {
continuation.yield(location)
}
continuation.finish()
}
}
}| Key | When to add |
|---|---|
NSLocationWhenInUseUsageDescription | Always, for any location use |
NSLocationAlwaysAndWhenInUseUsageDescription | Only if requesting .always |
| Key | Purpose |
|---|---|
NSLocationTemporaryUsageDescriptionDictionary | Per-feature full-accuracy descriptions |
NSLocationRequireExplicitServiceSession | Force explicit CLServiceSession usage (iOS 18+) |
NSLocationDefaultAccuracyReduced | Default to approximate location |
UIBackgroundModes (includes location) | Background location updates |
Good: "Shows nearby coffee shops within walking distance so you can find your next stop quickly."
Bad: "This app uses your location."
App Review rejects vague usage descriptions. Be specific about what the user gains from sharing their location.
// CRASH -- creating a monitor with a name already in use
let monitorA = await CLMonitor("myMonitor")
// ... immediately discard monitorA ...
let monitorB = await CLMonitor("myMonitor") // NSInternalInconsistencyExceptionFix: reuse the existing monitor instance. Only create a new one after the old one has been fully torn down (conditions removed, reference released, NOT in the same run loop).
Approximate location may update infrequently. On iOS 18+, check diagnostic
properties such as accuracyLimited and locationUnavailable; otherwise,
treat update.location == nil as a state change and provide a degraded
experience instead of spinning or timing out.
// WRONG -- session is immediately deallocated
func startBackground() {
let _ = CLBackgroundActivitySession()
// ^ No strong reference; session ends immediately
}
// CORRECT -- hold as a stored property
private var bgSession: CLBackgroundActivitySession?
func startBackground() {
bgSession = CLBackgroundActivitySession()
}// WRONG -- using location with negative accuracy (invalid)
guard let location = update.location else { continue }
updateMap(location) // May have accuracy of -1 (invalid)
// CORRECT
guard let location = update.location,
location.horizontalAccuracy >= 0 else { continue }
updateMap(location)A horizontalAccuracy of -1 means the coordinate is invalid.
.tessl-plugin
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
references
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references