Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
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.
@Observable
final class LocationService: @unchecked Sendable {
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
func startTracking() {
guard !isTracking else { return }
isTracking = true
trackingTask = Task { [weak self] in
guard let self else { return }
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 < self.accuracyThreshold else {
continue
}
// Skip if the user has not moved enough
if let last = self.lastReportedLocation,
location.distance(from: last) < self.distanceFilter {
continue
}
self.lastReportedLocation = location
await MainActor.run {
self.currentLocation = location
}
}
await MainActor.run {
self.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)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.
CLServiceSession declares your authorization requirements for a feature.
Hold a strong reference for the session's entire duration.
@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 {
for try await update in CLLocationUpdate.liveUpdates() {
guard let location = update.location else { continue }
// process location
}
}
}
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)CLMonitor replaces CLLocationManager region monitoring. It is an actor,
so all its 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 {
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)
}
}
}
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
}// DEPRECATED
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).
On iOS 17 (not iOS 18+), liveUpdates() may return no updates when the user
has denied full accuracy. This is resolved in iOS 18. Target iOS 18+ or
fall back to CLLocationManager if you need to handle approximate-only
location on iOS 17.
// 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.
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
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