@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md
CHANGED
|
@@ -1,411 +1,322 @@
|
|
|
1
|
-
# CoreLocation Patterns
|
|
1
|
+
# CoreLocation Patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Import `CoreLocation` in
|
|
5
|
-
|
|
6
|
-
```swift
|
|
7
|
-
import CoreLocation
|
|
8
|
-
```
|
|
9
|
-
|
|
10
|
-
---
|
|
3
|
+
Modern CoreLocation with Swift concurrency, iOS 17 and later unless a heading
|
|
4
|
+
says otherwise. Import `CoreLocation` in each file that uses these types.
|
|
11
5
|
|
|
12
6
|
## Contents
|
|
13
7
|
|
|
14
|
-
- [CLLocationUpdate.liveUpdates()
|
|
15
|
-
- [
|
|
16
|
-
- [
|
|
8
|
+
- [CLLocationUpdate.liveUpdates()](#cllocationupdateliveupdates)
|
|
9
|
+
- [Service Sessions (iOS 18+)](#service-sessions-ios-18)
|
|
10
|
+
- [Geofences with CLMonitor (iOS 17+)](#geofences-with-clmonitor-ios-17)
|
|
17
11
|
- [CLBackgroundActivitySession](#clbackgroundactivitysession)
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
12
|
+
- [Coarse Updates from Significant Changes](#coarse-updates-from-significant-changes)
|
|
13
|
+
- [Visits](#visits)
|
|
14
|
+
- [Moving Off CLCircularRegion](#moving-off-clcircularregion)
|
|
15
|
+
- [Choosing Accuracy](#choosing-accuracy)
|
|
16
|
+
- [Simulating Position](#simulating-position)
|
|
17
|
+
- [Info.plist Privacy Keys](#infoplist-privacy-keys)
|
|
18
|
+
- [Pitfalls](#pitfalls)
|
|
25
19
|
- [References](#references)
|
|
26
20
|
|
|
27
|
-
## CLLocationUpdate.liveUpdates()
|
|
21
|
+
## CLLocationUpdate.liveUpdates()
|
|
28
22
|
|
|
29
23
|
### Basic Usage
|
|
30
24
|
|
|
31
25
|
```swift
|
|
32
|
-
func
|
|
33
|
-
for try await
|
|
34
|
-
guard let
|
|
35
|
-
|
|
36
|
-
|
|
26
|
+
func logPositions() async throws {
|
|
27
|
+
for try await reading in CLLocationUpdate.liveUpdates() {
|
|
28
|
+
guard let here = reading.location else { continue }
|
|
29
|
+
let lat = here.coordinate.latitude
|
|
30
|
+
let lon = here.coordinate.longitude
|
|
31
|
+
let errorMetres = here.horizontalAccuracy
|
|
32
|
+
_ = (lat, lon, errorMetres)
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
```
|
|
40
36
|
|
|
41
37
|
### Full Implementation with Filtering
|
|
42
38
|
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
Filtering keeps the UI from redrawing on noise and saves battery. Each rule
|
|
40
|
+
below drops a sample that should never reach the screen.
|
|
45
41
|
|
|
46
42
|
```swift
|
|
43
|
+
import CoreLocation
|
|
44
|
+
import Observation
|
|
45
|
+
|
|
47
46
|
@MainActor
|
|
48
47
|
@Observable
|
|
49
|
-
final class
|
|
50
|
-
var
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
private var
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
private let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
guard !isTracking else { return }
|
|
65
|
-
isTracking = true
|
|
66
|
-
|
|
67
|
-
trackingTask = Task {
|
|
48
|
+
final class HikeLocator {
|
|
49
|
+
private(set) var position: CLLocation?
|
|
50
|
+
private(set) var isRunning = false
|
|
51
|
+
private var feed: Task<Void, Never>?
|
|
52
|
+
private var lastPublished: CLLocation?
|
|
53
|
+
|
|
54
|
+
private let minimumMove: CLLocationDistance = 10
|
|
55
|
+
private let worstAccuracy: CLLocationAccuracy = 100
|
|
56
|
+
private let oldestAge: TimeInterval = 15
|
|
57
|
+
private let fastestSpeed: CLLocationSpeed = 80
|
|
58
|
+
|
|
59
|
+
func start() {
|
|
60
|
+
guard !isRunning else { return }
|
|
61
|
+
isRunning = true
|
|
62
|
+
feed = Task {
|
|
68
63
|
do {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for try await update in updates {
|
|
64
|
+
for try await reading in CLLocationUpdate.liveUpdates(.default) {
|
|
72
65
|
if Task.isCancelled { break }
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// Skip inaccurate readings
|
|
78
|
-
guard location.horizontalAccuracy >= 0,
|
|
79
|
-
location.horizontalAccuracy < accuracyThreshold else {
|
|
80
|
-
continue
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Skip stale readings
|
|
84
|
-
guard abs(location.timestamp.timeIntervalSinceNow) < maximumLocationAge else {
|
|
85
|
-
continue
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Skip impossible movement when speed/course are available
|
|
89
|
-
if location.speed >= 0, location.speed > 80 {
|
|
90
|
-
continue
|
|
91
|
-
}
|
|
92
|
-
if location.course >= 0, location.courseAccuracy < 0 {
|
|
93
|
-
continue
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Skip if the user has not moved enough
|
|
97
|
-
if let last = lastReportedLocation,
|
|
98
|
-
location.distance(from: last) < distanceFilter {
|
|
99
|
-
continue
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
lastReportedLocation = location
|
|
103
|
-
currentLocation = location
|
|
66
|
+
guard let fix = reading.location, accept(fix) else { continue }
|
|
67
|
+
lastPublished = fix
|
|
68
|
+
position = fix
|
|
104
69
|
}
|
|
105
70
|
} catch is CancellationError {
|
|
106
|
-
// Expected when tracking stops.
|
|
107
71
|
} catch {
|
|
108
|
-
|
|
72
|
+
position = nil
|
|
109
73
|
}
|
|
110
|
-
|
|
111
|
-
isTracking = false
|
|
74
|
+
isRunning = false
|
|
112
75
|
}
|
|
113
76
|
}
|
|
114
77
|
|
|
115
|
-
func
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
78
|
+
func stop() {
|
|
79
|
+
feed?.cancel()
|
|
80
|
+
feed = nil
|
|
81
|
+
isRunning = false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private func accept(_ fix: CLLocation) -> Bool {
|
|
85
|
+
if fix.horizontalAccuracy < 0 || fix.horizontalAccuracy >= worstAccuracy { return false }
|
|
86
|
+
if abs(fix.timestamp.timeIntervalSinceNow) >= oldestAge { return false }
|
|
87
|
+
if fix.speed >= 0 && fix.speed > fastestSpeed { return false }
|
|
88
|
+
if fix.course >= 0 && fix.courseAccuracy < 0 { return false }
|
|
89
|
+
if let lastPublished, fix.distance(from: lastPublished) < minimumMove { return false }
|
|
90
|
+
return true
|
|
119
91
|
}
|
|
120
92
|
}
|
|
121
93
|
```
|
|
122
94
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
```swift
|
|
128
|
-
// Default: balanced power and accuracy
|
|
129
|
-
CLLocationUpdate.liveUpdates(.default)
|
|
95
|
+
The rules: accuracy must be valid (not negative) and under 100 m; the sample
|
|
96
|
+
must be younger than 15 s; a speed above 80 m/s is a glitch; a course without
|
|
97
|
+
a valid course accuracy is ignored; moves under 10 m from the last published
|
|
98
|
+
fix are not worth a redraw. `start()` refuses to run twice.
|
|
130
99
|
|
|
131
|
-
|
|
132
|
-
CLLocationUpdate.liveUpdates(.automotiveNavigation)
|
|
133
|
-
|
|
134
|
-
// Fitness tracking
|
|
135
|
-
CLLocationUpdate.liveUpdates(.fitness)
|
|
100
|
+
### LiveConfiguration Options
|
|
136
101
|
|
|
137
|
-
|
|
138
|
-
CLLocationUpdate.liveUpdates(.otherNavigation)
|
|
102
|
+
`CLLocationUpdate.liveUpdates(_:)` takes a `LiveConfiguration`:
|
|
139
103
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
104
|
+
| Value | Tuned for |
|
|
105
|
+
|-------|-----------|
|
|
106
|
+
| `.default` | balanced power and accuracy |
|
|
107
|
+
| `.automotiveNavigation` | turn-by-turn driving, where precision and update rate matter most |
|
|
108
|
+
| `.otherNavigation` | navigation that is not by car |
|
|
109
|
+
| `.fitness` | workouts |
|
|
110
|
+
| `.airborne` | drones and aviation |
|
|
143
111
|
|
|
144
|
-
###
|
|
112
|
+
### Diagnostics and Fallbacks (iOS 18+)
|
|
145
113
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
114
|
+
Own every `liveUpdates()` stream in a stored `Task` and cancel it when the
|
|
115
|
+
map, route or monitoring flow ends. Treat a diagnostic or a bad sample as a
|
|
116
|
+
change of state: update the UI, fall back to cached or typed input, or pause
|
|
117
|
+
background work. A loop that silently waits forever is the failure mode.
|
|
150
118
|
|
|
151
119
|
```swift
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if update.insufficientlyInUse {
|
|
165
|
-
// App does not meet in-use requirements
|
|
166
|
-
continue
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if update.locationUnavailable {
|
|
170
|
-
// Temporarily unable to determine location; keep iterating
|
|
171
|
-
continue
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if update.stationary {
|
|
175
|
-
// Device stopped moving; updates will pause
|
|
176
|
-
continue
|
|
120
|
+
@available(iOS 18, *)
|
|
121
|
+
func watch(onDenied: @escaping @MainActor () -> Void) async throws {
|
|
122
|
+
for try await reading in CLLocationUpdate.liveUpdates() {
|
|
123
|
+
if reading.authorizationDenied {
|
|
124
|
+
await onDenied()
|
|
125
|
+
break
|
|
126
|
+
}
|
|
127
|
+
if reading.authorizationDeniedGlobally { break }
|
|
128
|
+
if reading.insufficientlyInUse { continue }
|
|
129
|
+
if reading.locationUnavailable { continue }
|
|
130
|
+
if reading.stationary { continue }
|
|
131
|
+
if let fix = reading.location { _ = fix }
|
|
177
132
|
}
|
|
178
|
-
|
|
179
|
-
guard let location = update.location else { continue }
|
|
180
|
-
// Use location
|
|
181
133
|
}
|
|
182
134
|
```
|
|
183
135
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
136
|
+
| Diagnostic (iOS 18+) | Meaning | Response |
|
|
137
|
+
|----------------------|---------|----------|
|
|
138
|
+
| `authorizationDenied` | the user said no | stop location work, show a Settings path |
|
|
139
|
+
| `authorizationDeniedGlobally` | Location Services are off for the device | explain the system switch |
|
|
140
|
+
| `authorizationRestricted` | parental or MDM restriction | explain; no Settings fix for the user |
|
|
141
|
+
| `insufficientlyInUse` | the app is not in a state that allows updates | pause; resume when the user returns to the feature |
|
|
142
|
+
| `locationUnavailable` | temporarily cannot locate | keep iterating; let the user type a place or use the last known or visible area |
|
|
143
|
+
| `accuracyLimited` | only approximate location | search a wider radius and size geofences generously; ask for precise location only when the user starts something that needs it |
|
|
144
|
+
| `stationary` | device stopped moving; updates pause | nothing to do; they resume on movement |
|
|
188
145
|
|
|
189
|
-
|
|
146
|
+
Availability: all of these are iOS 18 properties. On iOS 17 the only related
|
|
147
|
+
property is `isStationary`, which iOS 18 deprecates in favour of
|
|
148
|
+
`stationary`; detect unavailability on iOS 17 by `location == nil` on the update.
|
|
190
149
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
- `locationUnavailable`: keep the feature usable with cached, typed, or map-region
|
|
195
|
-
input while waiting for recovery.
|
|
196
|
-
- Reduced or approximate accuracy: widen search/geofence assumptions or request
|
|
197
|
-
full accuracy only from a user-triggered feature that needs it.
|
|
150
|
+
Before publishing any fix, check that horizontal accuracy is valid, the
|
|
151
|
+
timestamp is recent and the movement is plausible. `speed` and `course` are
|
|
152
|
+
negative when unknown; read them only when non-negative.
|
|
198
153
|
|
|
199
|
-
|
|
200
|
-
timestamp age, and plausible movement. Use `speed` and `course` only when their
|
|
201
|
-
values are valid; negative values mean the measurement is unavailable.
|
|
154
|
+
## Service Sessions (iOS 18+)
|
|
202
155
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
## CLServiceSession (iOS 18+)
|
|
156
|
+
`CLServiceSession` is iOS 18 and later, and unavailable on macOS.
|
|
206
157
|
|
|
207
158
|
### Setup and Lifecycle
|
|
208
159
|
|
|
209
|
-
|
|
210
|
-
|
|
160
|
+
The session announces what a feature needs. Keep a strong reference for as
|
|
161
|
+
long as the feature runs; dropping it tells the system you are done.
|
|
211
162
|
|
|
212
163
|
```swift
|
|
164
|
+
@available(iOS 18, *)
|
|
213
165
|
@MainActor
|
|
214
166
|
@Observable
|
|
215
|
-
final class
|
|
216
|
-
private var
|
|
217
|
-
private var
|
|
167
|
+
final class NearbyFeature {
|
|
168
|
+
private var session: CLServiceSession?
|
|
169
|
+
private var feed: Task<Void, Never>?
|
|
170
|
+
private(set) var here: CLLocation?
|
|
218
171
|
|
|
219
172
|
func activate() {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
locationTask = Task {
|
|
173
|
+
session = CLServiceSession(authorization: .whenInUse)
|
|
174
|
+
feed = Task {
|
|
224
175
|
do {
|
|
225
|
-
for try await
|
|
226
|
-
|
|
227
|
-
// process location
|
|
176
|
+
for try await reading in CLLocationUpdate.liveUpdates() {
|
|
177
|
+
if let fix = reading.location { here = fix }
|
|
228
178
|
}
|
|
229
179
|
} catch is CancellationError {
|
|
230
|
-
// Expected when the feature stops.
|
|
231
180
|
} catch {
|
|
232
|
-
|
|
181
|
+
here = nil
|
|
233
182
|
}
|
|
234
183
|
}
|
|
235
184
|
}
|
|
236
185
|
|
|
237
186
|
func deactivate() {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
serviceSession = nil
|
|
187
|
+
feed?.cancel()
|
|
188
|
+
feed = nil
|
|
189
|
+
session = nil
|
|
242
190
|
}
|
|
243
191
|
}
|
|
244
192
|
```
|
|
245
193
|
|
|
194
|
+
On an unexpected error, move to a degraded state and offer retry rather than
|
|
195
|
+
leaving the screen spinning.
|
|
196
|
+
|
|
246
197
|
### Full Accuracy Request
|
|
247
198
|
|
|
248
|
-
|
|
199
|
+
When the user granted only approximate location, ask for precision for one
|
|
200
|
+
feature:
|
|
249
201
|
|
|
250
202
|
```swift
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
let session = CLServiceSession(
|
|
256
|
-
authorization: .whenInUse,
|
|
257
|
-
fullAccuracyPurposeKey: "NearbySearchPurpose"
|
|
258
|
-
)
|
|
203
|
+
@available(iOS 18, *)
|
|
204
|
+
func preciseSession() -> CLServiceSession {
|
|
205
|
+
CLServiceSession(authorization: .whenInUse, fullAccuracyPurposeKey: "TrailNavigation")
|
|
206
|
+
}
|
|
259
207
|
```
|
|
260
208
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
background for significant location changes after termination.
|
|
209
|
+
The key must exist in Info.plist under
|
|
210
|
+
`NSLocationTemporaryUsageDescriptionDictionary`, mapped to a sentence the user
|
|
211
|
+
will read.
|
|
265
212
|
|
|
266
|
-
|
|
267
|
-
let session = CLServiceSession(authorization: .always)
|
|
268
|
-
```
|
|
213
|
+
### Always Authorization
|
|
269
214
|
|
|
270
|
-
|
|
215
|
+
`CLServiceSession(authorization: .always)` is for one case: the system must
|
|
216
|
+
relaunch the app in the background after termination, for significant
|
|
217
|
+
location changes. It requires `NSLocationAlwaysAndWhenInUseUsageDescription`.
|
|
271
218
|
|
|
272
219
|
### Implicit vs. Explicit Sessions
|
|
273
220
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
need an explicit session when:
|
|
221
|
+
If the app has no session, iOS 18 makes one on its behalf as soon as
|
|
222
|
+
`liveUpdates()` or `CLMonitor` starts. Create one explicitly when:
|
|
277
223
|
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
-
|
|
281
|
-
|
|
224
|
+
- the feature needs `.always`;
|
|
225
|
+
- the feature needs full accuracy through `fullAccuracyPurposeKey`;
|
|
226
|
+
- Info.plist sets `NSLocationRequireExplicitServiceSession`, which turns
|
|
227
|
+
implicit sessions off.
|
|
282
228
|
|
|
283
|
-
|
|
229
|
+
## Geofences with CLMonitor (iOS 17+)
|
|
284
230
|
|
|
285
|
-
|
|
231
|
+
`CLMonitor` is iOS 17 and macOS 14 and later; it is not available on watchOS,
|
|
232
|
+
tvOS or visionOS.
|
|
286
233
|
|
|
287
234
|
### Basic Setup
|
|
288
235
|
|
|
289
|
-
|
|
290
|
-
|
|
236
|
+
`CLMonitor` is an `actor`, so every call is `await`ed. Create it with a name,
|
|
237
|
+
add conditions, and read one `events` sequence.
|
|
291
238
|
|
|
292
239
|
```swift
|
|
240
|
+
import CoreLocation
|
|
241
|
+
|
|
242
|
+
struct Geofence: Sendable {
|
|
243
|
+
let id: String
|
|
244
|
+
let center: CLLocationCoordinate2D
|
|
245
|
+
let radius: CLLocationDistance
|
|
246
|
+
}
|
|
247
|
+
|
|
293
248
|
@available(iOS 17, *)
|
|
294
|
-
actor
|
|
249
|
+
actor ParkGates {
|
|
295
250
|
private var monitor: CLMonitor?
|
|
296
|
-
private var
|
|
251
|
+
private var listener: Task<Void, any Error>?
|
|
297
252
|
|
|
298
|
-
func
|
|
299
|
-
let monitor = await CLMonitor("
|
|
253
|
+
func start(_ fences: [Geofence], onChange: @escaping @Sendable (String, Bool) -> Void) async {
|
|
254
|
+
let monitor = await CLMonitor("park-gates")
|
|
300
255
|
self.monitor = monitor
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
let condition = CLMonitor.CircularGeographicCondition(
|
|
305
|
-
center: region.center,
|
|
306
|
-
radius: region.radius
|
|
307
|
-
)
|
|
308
|
-
await monitor.add(condition, identifier: region.id)
|
|
256
|
+
for fence in fences {
|
|
257
|
+
let area = CLMonitor.CircularGeographicCondition(center: fence.center, radius: fence.radius)
|
|
258
|
+
await monitor.add(area, identifier: fence.id)
|
|
309
259
|
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Device entered the region
|
|
317
|
-
handleEntry(identifier: event.identifier)
|
|
318
|
-
case .unsatisfied:
|
|
319
|
-
// Device exited the region
|
|
320
|
-
handleExit(identifier: event.identifier)
|
|
321
|
-
case .unknown:
|
|
322
|
-
break
|
|
323
|
-
default:
|
|
324
|
-
break
|
|
260
|
+
listener = Task {
|
|
261
|
+
for try await change in await monitor.events {
|
|
262
|
+
switch change.state {
|
|
263
|
+
case .satisfied: onChange(change.identifier, true)
|
|
264
|
+
case .unsatisfied: onChange(change.identifier, false)
|
|
265
|
+
default: break
|
|
325
266
|
}
|
|
326
267
|
}
|
|
327
268
|
}
|
|
328
269
|
}
|
|
329
270
|
|
|
330
|
-
func
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
271
|
+
func stop() async {
|
|
272
|
+
listener?.cancel()
|
|
273
|
+
listener = nil
|
|
334
274
|
if let monitor {
|
|
335
|
-
for
|
|
336
|
-
await monitor.remove(
|
|
275
|
+
for id in await monitor.identifiers {
|
|
276
|
+
await monitor.remove(id)
|
|
337
277
|
}
|
|
338
278
|
}
|
|
339
279
|
monitor = nil
|
|
340
280
|
}
|
|
341
|
-
|
|
342
|
-
private func handleEntry(identifier: String) {
|
|
343
|
-
print("Entered region: \(identifier)")
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private func handleExit(identifier: String) {
|
|
347
|
-
print("Exited region: \(identifier)")
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
struct GeofenceRegion: Identifiable {
|
|
352
|
-
let id: String
|
|
353
|
-
let center: CLLocationCoordinate2D
|
|
354
|
-
let radius: CLLocationDistance
|
|
355
281
|
}
|
|
356
282
|
```
|
|
357
283
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
to report `unmonitored` state. This limit is per-app, not per-monitor.
|
|
284
|
+
`.satisfied` means the device is inside (entry), `.unsatisfied` outside
|
|
285
|
+
(exit); `.unknown` and anything else are ignored. `event.identifier` names the
|
|
286
|
+
condition.
|
|
362
287
|
|
|
363
|
-
|
|
364
|
-
the same name while one is still alive crashes the app. Reuse the instance
|
|
365
|
-
and call `add`/`remove` to change conditions.
|
|
366
|
-
|
|
367
|
-
3. **Subscribe to `events` exactly once per CLMonitor.** Cancelling and
|
|
368
|
-
re-subscribing causes the new subscription to immediately cancel. Keep a
|
|
369
|
-
single long-lived subscription.
|
|
370
|
-
|
|
371
|
-
4. **Use diffing for condition updates.** Instead of removing all conditions
|
|
372
|
-
and re-adding them, calculate which to add and which to remove.
|
|
288
|
+
### Critical CLMonitor Rules
|
|
373
289
|
|
|
374
|
-
|
|
375
|
-
|
|
290
|
+
- At most 20 conditions per app, counted across all monitors. Extra ones
|
|
291
|
+
report the `unmonitored` state.
|
|
292
|
+
- Never recreate a monitor quickly. Creating one with the name of a live
|
|
293
|
+
monitor crashes the app. Keep one and call `add` and `remove`.
|
|
294
|
+
- Subscribe to `events` once per monitor. Cancelling and subscribing again
|
|
295
|
+
gives a subscription that ends immediately; keep one long-lived listener.
|
|
296
|
+
- Change conditions by diffing (add the new, remove the gone), not by
|
|
297
|
+
removing everything and adding it back.
|
|
298
|
+
- Where possible, target iOS 18 and pair `CLMonitor` with a `CLServiceSession`
|
|
299
|
+
so authorization is handled reliably.
|
|
376
300
|
|
|
377
301
|
### Adding Conditions with Initial State
|
|
378
302
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
await monitor.add(condition, identifier: "office", assuming: .unsatisfied)
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
Use `.unsatisfied` when you believe the device is outside the region. Use
|
|
386
|
-
`.satisfied` when you believe the device is inside.
|
|
303
|
+
`add(_:identifier:assuming:)` supplies the state you believe is true, which
|
|
304
|
+
avoids a spurious first event. Pass `.unsatisfied` when the device is
|
|
305
|
+
believed outside, `.satisfied` when inside.
|
|
387
306
|
|
|
388
307
|
### Updating Conditions Dynamically
|
|
389
308
|
|
|
390
309
|
```swift
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
let
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
// Remove stale conditions
|
|
398
|
-
for id in existingIDs.subtracting(newIDs) {
|
|
399
|
-
await monitor.remove(id)
|
|
310
|
+
@available(iOS 17, *)
|
|
311
|
+
func sync(_ monitor: CLMonitor, to fences: [Geofence]) async {
|
|
312
|
+
let current = Set(await monitor.identifiers)
|
|
313
|
+
let wanted = Set(fences.map(\.id))
|
|
314
|
+
for gone in current.subtracting(wanted) {
|
|
315
|
+
await monitor.remove(gone)
|
|
400
316
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
let condition = CLMonitor.CircularGeographicCondition(
|
|
405
|
-
center: region.center,
|
|
406
|
-
radius: region.radius
|
|
407
|
-
)
|
|
408
|
-
await monitor.add(condition, identifier: region.id, assuming: .unsatisfied)
|
|
317
|
+
for fence in fences where !current.contains(fence.id) {
|
|
318
|
+
let area = CLMonitor.CircularGeographicCondition(center: fence.center, radius: fence.radius)
|
|
319
|
+
await monitor.add(area, identifier: fence.id, assuming: .unsatisfied)
|
|
409
320
|
}
|
|
410
321
|
}
|
|
411
322
|
```
|
|
@@ -413,365 +324,300 @@ func updateRegions(_ newRegions: [GeofenceRegion]) async {
|
|
|
413
324
|
### Checking Last Known State
|
|
414
325
|
|
|
415
326
|
```swift
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
let
|
|
419
|
-
|
|
327
|
+
@available(iOS 17, *)
|
|
328
|
+
func lastSeen(_ monitor: CLMonitor, fence id: String) async -> (CLMonitor.Event.State, Date)? {
|
|
329
|
+
let saved = await monitor.record(for: id)
|
|
330
|
+
guard let last = saved?.lastEvent else { return nil }
|
|
331
|
+
return (last.state, last.date)
|
|
420
332
|
}
|
|
421
333
|
```
|
|
422
334
|
|
|
423
|
-
---
|
|
424
|
-
|
|
425
335
|
## CLBackgroundActivitySession
|
|
426
336
|
|
|
427
|
-
|
|
428
|
-
|
|
337
|
+
Available from iOS 17; not on macOS or tvOS. It lets an app with when-in-use
|
|
338
|
+
authorization keep receiving location in the background. It needs the
|
|
339
|
+
Location updates background mode, and while it is held the system shows the
|
|
340
|
+
blue location indicator.
|
|
429
341
|
|
|
430
342
|
```swift
|
|
431
343
|
@available(iOS 17, *)
|
|
432
|
-
actor
|
|
433
|
-
private var
|
|
434
|
-
private var
|
|
435
|
-
private var
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
trackingTask = Task {
|
|
344
|
+
actor WorkoutRecorder {
|
|
345
|
+
private var background: CLBackgroundActivitySession?
|
|
346
|
+
private var service: AnyObject?
|
|
347
|
+
private var feed: Task<Void, Never>?
|
|
348
|
+
private var trail: [CLLocation] = []
|
|
349
|
+
|
|
350
|
+
func begin() {
|
|
351
|
+
if #available(iOS 18, *) {
|
|
352
|
+
service = CLServiceSession(authorization: .whenInUse)
|
|
353
|
+
}
|
|
354
|
+
background = CLBackgroundActivitySession()
|
|
355
|
+
feed = Task {
|
|
445
356
|
do {
|
|
446
|
-
for try await
|
|
447
|
-
|
|
448
|
-
// Record location for fitness tracking, navigation, etc.
|
|
449
|
-
await recordLocation(location)
|
|
357
|
+
for try await reading in CLLocationUpdate.liveUpdates(.fitness) {
|
|
358
|
+
if let fix = reading.location { record(fix) }
|
|
450
359
|
}
|
|
451
|
-
} catch
|
|
452
|
-
// Expected when background tracking stops.
|
|
453
|
-
} catch {
|
|
454
|
-
// Persist an error state or retry from user action.
|
|
455
|
-
}
|
|
360
|
+
} catch {}
|
|
456
361
|
}
|
|
457
362
|
}
|
|
458
363
|
|
|
459
|
-
func
|
|
460
|
-
|
|
461
|
-
trackingTask = nil
|
|
462
|
-
backgroundSession?.invalidate()
|
|
463
|
-
backgroundSession = nil
|
|
464
|
-
serviceSession = nil
|
|
364
|
+
private func record(_ fix: CLLocation) {
|
|
365
|
+
trail.append(fix)
|
|
465
366
|
}
|
|
466
367
|
|
|
467
|
-
|
|
468
|
-
|
|
368
|
+
func finish() {
|
|
369
|
+
feed?.cancel()
|
|
370
|
+
feed = nil
|
|
371
|
+
background?.invalidate()
|
|
372
|
+
background = nil
|
|
373
|
+
service = nil
|
|
469
374
|
}
|
|
470
375
|
}
|
|
471
376
|
```
|
|
472
377
|
|
|
473
378
|
### Background Requirements Summary
|
|
474
379
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
1. `Background Modes > Location updates` capability enabled.
|
|
478
|
-
2. `NSLocationWhenInUseUsageDescription` in Info.plist.
|
|
479
|
-
3. `.whenInUse` or `.always` authorization granted.
|
|
480
|
-
4. Either a `CLBackgroundActivitySession` held or a Live Activity running.
|
|
481
|
-
5. An active `CLLocationUpdate` or `CLMonitor` subscription.
|
|
380
|
+
Background location works only when all of these hold:
|
|
482
381
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
app
|
|
382
|
+
1. Background Modes > Location updates is enabled.
|
|
383
|
+
2. Info.plist has `NSLocationWhenInUseUsageDescription`.
|
|
384
|
+
3. The user granted `.whenInUse` or `.always`.
|
|
385
|
+
4. The app holds a `CLBackgroundActivitySession`, or runs a Live Activity
|
|
386
|
+
(see `live-activities`).
|
|
387
|
+
5. A `CLLocationUpdate` or `CLMonitor` subscription is active.
|
|
487
388
|
|
|
488
|
-
|
|
389
|
+
`.always` is not needed for this. What `.always` adds is relaunch: after the
|
|
390
|
+
app has been terminated, a significant move can bring it back. With
|
|
391
|
+
`.whenInUse` plus a background session, the app must still be running,
|
|
392
|
+
in the foreground or suspended.
|
|
489
393
|
|
|
490
|
-
##
|
|
394
|
+
## Coarse Updates from Significant Changes
|
|
491
395
|
|
|
492
|
-
|
|
493
|
-
|
|
396
|
+
Coarse updates, roughly every 500 m, driven by cell tower changes and cheap on
|
|
397
|
+
battery. There is no `CLLocationUpdate` equivalent, so this stays on
|
|
398
|
+
`CLLocationManager`, which is legacy but valid:
|
|
494
399
|
|
|
495
400
|
```swift
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
manager.startMonitoringSignificantLocationChanges()
|
|
499
|
-
|
|
500
|
-
// The delegate receives updates when the device moves ~500m+ from the
|
|
501
|
-
// last reported location. Updates arrive 1-5 minutes apart.
|
|
401
|
+
let coarseManager = CLLocationManager()
|
|
402
|
+
coarseManager.startMonitoringSignificantLocationChanges()
|
|
502
403
|
```
|
|
503
404
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
---
|
|
405
|
+
The delegate hears about moves of 500 m or more, typically 1 to 5 minutes
|
|
406
|
+
apart.
|
|
508
407
|
|
|
509
|
-
##
|
|
408
|
+
## Visits
|
|
510
409
|
|
|
511
|
-
|
|
512
|
-
|
|
410
|
+
Arrivals and departures at places, for journaling, check-ins or context-aware
|
|
411
|
+
features.
|
|
513
412
|
|
|
514
413
|
```swift
|
|
515
|
-
|
|
516
|
-
manager
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
414
|
+
final class VisitLog: NSObject, CLLocationManagerDelegate {
|
|
415
|
+
private let manager = CLLocationManager()
|
|
416
|
+
|
|
417
|
+
func start() {
|
|
418
|
+
manager.delegate = self
|
|
419
|
+
manager.startMonitoringVisits()
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
func locationManager(_ manager: CLLocationManager, didVisit stop: CLVisit) {
|
|
423
|
+
let stillThere = stop.departureDate == .distantFuture
|
|
424
|
+
_ = (stop.coordinate, stop.arrivalDate, stillThere)
|
|
425
|
+
}
|
|
524
426
|
}
|
|
525
427
|
```
|
|
526
428
|
|
|
527
|
-
|
|
429
|
+
A `departureDate` of `.distantFuture` means the user has not left yet.
|
|
528
430
|
|
|
529
|
-
##
|
|
431
|
+
## Moving Off CLCircularRegion
|
|
530
432
|
|
|
531
433
|
### Older Delegate Approach
|
|
532
434
|
|
|
533
435
|
```swift
|
|
534
|
-
let
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
436
|
+
let gate = CLCircularRegion(center: gateCenter, radius: 200, identifier: "north-gate")
|
|
437
|
+
gate.notifyOnEntry = true
|
|
438
|
+
gate.notifyOnExit = true
|
|
439
|
+
legacyManager.startMonitoring(for: gate)
|
|
538
440
|
```
|
|
539
441
|
|
|
540
442
|
### Modern Approach (iOS 17+)
|
|
541
443
|
|
|
542
444
|
```swift
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
let
|
|
546
|
-
await monitor.add(
|
|
547
|
-
|
|
548
|
-
for try await
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
case .
|
|
552
|
-
case .unsatisfied: handleExit()
|
|
445
|
+
@available(iOS 17, *)
|
|
446
|
+
func watchGate(_ center: CLLocationCoordinate2D) async throws {
|
|
447
|
+
let monitor = await CLMonitor("gates")
|
|
448
|
+
await monitor.add(CLMonitor.CircularGeographicCondition(center: center, radius: 200),
|
|
449
|
+
identifier: "north-gate")
|
|
450
|
+
for try await change in await monitor.events where change.identifier == "north-gate" {
|
|
451
|
+
switch change.state {
|
|
452
|
+
case .satisfied: break
|
|
453
|
+
case .unsatisfied: break
|
|
553
454
|
default: break
|
|
554
455
|
}
|
|
555
456
|
}
|
|
556
457
|
}
|
|
557
458
|
```
|
|
558
459
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
|
562
|
-
|
|
563
|
-
|
|
|
564
|
-
|
|
|
565
|
-
|
|
|
566
|
-
| Concurrency | @objc delegate | Actor-based |
|
|
567
|
-
| Min iOS | iOS 7 | iOS 17 |
|
|
460
|
+
| | `CLCircularRegion` | `CLMonitor` |
|
|
461
|
+
|-|--------------------|-------------|
|
|
462
|
+
| Shape of the API | callbacks on a delegate | an async sequence of events |
|
|
463
|
+
| Region limit | 20 for the whole app | same 20, shared |
|
|
464
|
+
| Entry and exit | two booleans | one state enum (satisfied, unsatisfied) |
|
|
465
|
+
| Concurrency | `@objc` delegate | actor |
|
|
466
|
+
| Minimum iOS | 7 | 17 |
|
|
568
467
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
## Location Accuracy Management
|
|
468
|
+
## Choosing Accuracy
|
|
572
469
|
|
|
573
470
|
### Accuracy Levels for CLLocationManager
|
|
574
471
|
|
|
575
|
-
|
|
576
|
-
manager.desiredAccuracy = kCLLocationAccuracyBest // GPS, ~5m, highest power
|
|
577
|
-
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters // ~10m
|
|
578
|
-
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters // WiFi, ~100m
|
|
579
|
-
manager.desiredAccuracy = kCLLocationAccuracyKilometer // Cell tower, ~1km
|
|
580
|
-
manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers // ~3km, lowest power
|
|
581
|
-
manager.desiredAccuracy = kCLLocationAccuracyReduced // ~5km, privacy-safe
|
|
582
|
-
```
|
|
472
|
+
Set with `manager.desiredAccuracy`:
|
|
583
473
|
|
|
584
|
-
|
|
474
|
+
| Constant | Roughly | Source and cost |
|
|
475
|
+
|----------|---------|-----------------|
|
|
476
|
+
| `kCLLocationAccuracyBest` | 5 m | GPS, most power |
|
|
477
|
+
| `kCLLocationAccuracyNearestTenMeters` | 10 m | |
|
|
478
|
+
| `kCLLocationAccuracyHundredMeters` | 100 m | Wi-Fi |
|
|
479
|
+
| `kCLLocationAccuracyKilometer` | 1 km | cell towers |
|
|
480
|
+
| `kCLLocationAccuracyThreeKilometers` | 3 km | least power |
|
|
481
|
+
| `kCLLocationAccuracyReduced` | 5 km | approximate, privacy-preserving |
|
|
585
482
|
|
|
586
|
-
|
|
587
|
-
manager.activityType = .other // Default
|
|
588
|
-
manager.activityType = .automotiveNavigation // Highway speeds, high accuracy
|
|
589
|
-
manager.activityType = .fitness // Walking/running
|
|
590
|
-
manager.activityType = .otherNavigation // Boats, trains
|
|
591
|
-
manager.activityType = .airborne // Drones, aircraft (iOS 12+)
|
|
592
|
-
```
|
|
483
|
+
### Activity Type
|
|
593
484
|
|
|
594
|
-
|
|
485
|
+
`manager.activityType` guides power management:
|
|
595
486
|
|
|
596
|
-
|
|
597
|
-
|
|
487
|
+
| Value | Use |
|
|
488
|
+
|-------|-----|
|
|
489
|
+
| `.other` | default |
|
|
490
|
+
| `.automotiveNavigation` | highway speeds, high accuracy |
|
|
491
|
+
| `.fitness` | walking and running |
|
|
492
|
+
| `.otherNavigation` | boats, trains |
|
|
493
|
+
| `.airborne` | drones and aircraft (iOS 12+) |
|
|
598
494
|
|
|
599
|
-
|
|
600
|
-
for try await update in CLLocationUpdate.liveUpdates() {
|
|
601
|
-
guard let location = update.location,
|
|
602
|
-
location.horizontalAccuracy < 50,
|
|
603
|
-
location.horizontalAccuracy >= 0 else { continue }
|
|
604
|
-
// Use filtered location
|
|
605
|
-
}
|
|
606
|
-
```
|
|
607
|
-
|
|
608
|
-
---
|
|
609
|
-
|
|
610
|
-
## Testing Location in Simulator
|
|
611
|
-
|
|
612
|
-
### Set a fixed simulated location
|
|
495
|
+
### CLLocationUpdate has no filtering
|
|
613
496
|
|
|
614
|
-
|
|
497
|
+
`liveUpdates()` has neither `desiredAccuracy` nor `distanceFilter`. Pick a
|
|
498
|
+
`LiveConfiguration` and filter the stream yourself, for example keep only
|
|
499
|
+
fixes with `horizontalAccuracy >= 0 && horizontalAccuracy < 50`.
|
|
615
500
|
|
|
616
|
-
|
|
501
|
+
## Simulating Position
|
|
617
502
|
|
|
618
|
-
|
|
503
|
+
- One fixed spot: Xcode Debug > Simulate Location, then a city or a custom
|
|
504
|
+
coordinate.
|
|
505
|
+
- A moving path: add a `.gpx` file to the project and pick it under Edit
|
|
506
|
+
Scheme > Run > Options > Default Location.
|
|
619
507
|
|
|
620
508
|
```xml
|
|
621
509
|
<?xml version="1.0"?>
|
|
622
|
-
<gpx version="1.1"
|
|
623
|
-
<wpt lat="
|
|
624
|
-
<
|
|
625
|
-
<
|
|
510
|
+
<gpx creator="Xcode" version="1.1">
|
|
511
|
+
<wpt lat="47.6062" lon="-122.3321">
|
|
512
|
+
<name>Start</name>
|
|
513
|
+
<time>2026-01-01T09:00:00Z</time>
|
|
626
514
|
</wpt>
|
|
627
|
-
<wpt lat="
|
|
628
|
-
<
|
|
629
|
-
<
|
|
630
|
-
</wpt>
|
|
631
|
-
<wpt lat="37.3230" lon="-122.0322">
|
|
632
|
-
<time>2025-01-01T00:02:00Z</time>
|
|
633
|
-
<name>De Anza College</name>
|
|
515
|
+
<wpt lat="47.6097" lon="-122.3331">
|
|
516
|
+
<name>Market</name>
|
|
517
|
+
<time>2026-01-01T09:05:00Z</time>
|
|
634
518
|
</wpt>
|
|
635
519
|
</gpx>
|
|
636
520
|
```
|
|
637
521
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
The simulator interpolates between waypoints using timestamps. Playback loops
|
|
641
|
-
automatically when it reaches the last waypoint.
|
|
522
|
+
The simulator moves between waypoints at the pace their timestamps imply and
|
|
523
|
+
starts over after the last one.
|
|
642
524
|
|
|
643
525
|
### Programmatic Simulation in Tests
|
|
644
526
|
|
|
645
|
-
|
|
527
|
+
Unit tests need a position source they control. Hide the stream behind a
|
|
528
|
+
protocol and inject a fake:
|
|
646
529
|
|
|
647
530
|
```swift
|
|
648
|
-
protocol
|
|
649
|
-
func
|
|
531
|
+
protocol PositionSource: Sendable {
|
|
532
|
+
func positions() -> AsyncStream<CLLocation>
|
|
650
533
|
}
|
|
651
534
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
func updates() -> AsyncStream<CLLocation> {
|
|
535
|
+
struct DevicePositionSource: PositionSource {
|
|
536
|
+
func positions() -> AsyncStream<CLLocation> {
|
|
655
537
|
AsyncStream { continuation in
|
|
656
|
-
Task {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
continuation.yield(
|
|
538
|
+
let pump = Task {
|
|
539
|
+
do {
|
|
540
|
+
for try await reading in CLLocationUpdate.liveUpdates() {
|
|
541
|
+
if let fix = reading.location { continuation.yield(fix) }
|
|
660
542
|
}
|
|
661
|
-
}
|
|
543
|
+
} catch {}
|
|
662
544
|
continuation.finish()
|
|
663
545
|
}
|
|
546
|
+
continuation.onTermination = { _ in pump.cancel() }
|
|
664
547
|
}
|
|
665
548
|
}
|
|
666
549
|
}
|
|
667
550
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
let locations: [CLLocation]
|
|
551
|
+
struct ScriptedPositionSource: PositionSource {
|
|
552
|
+
let script: [CLLocation]
|
|
671
553
|
|
|
672
|
-
func
|
|
554
|
+
func positions() -> AsyncStream<CLLocation> {
|
|
673
555
|
AsyncStream { continuation in
|
|
674
|
-
for
|
|
675
|
-
continuation.yield(location)
|
|
676
|
-
}
|
|
556
|
+
for fix in script { continuation.yield(fix) }
|
|
677
557
|
continuation.finish()
|
|
678
558
|
}
|
|
679
559
|
}
|
|
680
560
|
}
|
|
681
561
|
```
|
|
682
562
|
|
|
683
|
-
|
|
563
|
+
The same seam works for code still on `CLLocationManager`: wrap the delegate
|
|
564
|
+
behind the protocol and feed the scripted source in XCTest or Swift Testing.
|
|
684
565
|
|
|
685
|
-
##
|
|
566
|
+
## Info.plist Privacy Keys
|
|
686
567
|
|
|
687
568
|
### Required Keys
|
|
688
569
|
|
|
689
|
-
| Key | When
|
|
690
|
-
|
|
691
|
-
| `NSLocationWhenInUseUsageDescription` |
|
|
692
|
-
| `NSLocationAlwaysAndWhenInUseUsageDescription` |
|
|
570
|
+
| Key | When |
|
|
571
|
+
|-----|------|
|
|
572
|
+
| `NSLocationWhenInUseUsageDescription` | every app that reads location at all |
|
|
573
|
+
| `NSLocationAlwaysAndWhenInUseUsageDescription` | only when requesting `.always` |
|
|
693
574
|
|
|
694
575
|
### Optional Keys
|
|
695
576
|
|
|
696
|
-
| Key |
|
|
697
|
-
|
|
698
|
-
| `NSLocationTemporaryUsageDescriptionDictionary` |
|
|
699
|
-
| `NSLocationRequireExplicitServiceSession` |
|
|
700
|
-
| `NSLocationDefaultAccuracyReduced` |
|
|
701
|
-
| `UIBackgroundModes`
|
|
577
|
+
| Key | Effect |
|
|
578
|
+
|-----|--------|
|
|
579
|
+
| `NSLocationTemporaryUsageDescriptionDictionary` | purpose strings for per-feature full accuracy |
|
|
580
|
+
| `NSLocationRequireExplicitServiceSession` | disables implicit `CLServiceSession` (iOS 18+) |
|
|
581
|
+
| `NSLocationDefaultAccuracyReduced` | the app starts with approximate location |
|
|
582
|
+
| `UIBackgroundModes` containing `location` | background location updates |
|
|
702
583
|
|
|
703
584
|
### Usage Description Best Practices
|
|
704
585
|
|
|
705
|
-
|
|
586
|
+
Say what the user gets. "Shows trailheads within a short drive of you" is a
|
|
587
|
+
reason; "This app uses your location." is not, and App Review rejects vague
|
|
588
|
+
purpose strings.
|
|
706
589
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
App Review rejects vague usage descriptions. Be specific about what the user
|
|
710
|
-
gains from sharing their location.
|
|
711
|
-
|
|
712
|
-
---
|
|
713
|
-
|
|
714
|
-
## Common Pitfalls
|
|
590
|
+
## Pitfalls
|
|
715
591
|
|
|
716
592
|
### CLMonitor crash on rapid recreation
|
|
717
593
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
let monitorB = await CLMonitor("myMonitor") // NSInternalInconsistencyException
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
Fix: reuse the existing monitor instance. Only create a new one after the
|
|
726
|
-
old one has been fully torn down (conditions removed, reference released,
|
|
727
|
-
NOT in the same run loop).
|
|
594
|
+
A second `CLMonitor` created with a name that is still in use raises
|
|
595
|
+
`NSInternalInconsistencyException`. Reuse the monitor. Make a new one only
|
|
596
|
+
after full teardown (conditions removed, reference released) and never in the
|
|
597
|
+
same run loop turn.
|
|
728
598
|
|
|
729
|
-
###
|
|
599
|
+
### Approximate location arrives rarely
|
|
730
600
|
|
|
731
|
-
Approximate location
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
601
|
+
Approximate location can arrive rarely. On iOS 18 check `accuracyLimited` and
|
|
602
|
+
`locationUnavailable`; on iOS 17 treat `location == nil` on the update as a state
|
|
603
|
+
change. Either way, give a degraded experience instead of a spinner or a
|
|
604
|
+
timeout.
|
|
735
605
|
|
|
736
606
|
### Forgetting to hold CLBackgroundActivitySession
|
|
737
607
|
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
func startBackground() {
|
|
741
|
-
let _ = CLBackgroundActivitySession()
|
|
742
|
-
// ^ No strong reference; session ends immediately
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// CORRECT -- hold as a stored property
|
|
746
|
-
private var bgSession: CLBackgroundActivitySession?
|
|
747
|
-
|
|
748
|
-
func startBackground() {
|
|
749
|
-
bgSession = CLBackgroundActivitySession()
|
|
750
|
-
}
|
|
751
|
-
```
|
|
608
|
+
`_ = CLBackgroundActivitySession()` deallocates at once and the session ends
|
|
609
|
+
with it. Store it in a property.
|
|
752
610
|
|
|
753
611
|
### Not checking horizontalAccuracy
|
|
754
612
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
guard let location = update.location else { continue }
|
|
758
|
-
updateMap(location) // May have accuracy of -1 (invalid)
|
|
759
|
-
|
|
760
|
-
// CORRECT
|
|
761
|
-
guard let location = update.location,
|
|
762
|
-
location.horizontalAccuracy >= 0 else { continue }
|
|
763
|
-
updateMap(location)
|
|
764
|
-
```
|
|
765
|
-
|
|
766
|
-
A `horizontalAccuracy` of -1 means the coordinate is invalid.
|
|
767
|
-
|
|
768
|
-
---
|
|
613
|
+
A `horizontalAccuracy` of -1 marks the coordinate invalid. Check
|
|
614
|
+
`horizontalAccuracy >= 0` before using any fix.
|
|
769
615
|
|
|
770
616
|
## References
|
|
771
617
|
|
|
772
|
-
-
|
|
773
|
-
-
|
|
774
|
-
-
|
|
775
|
-
-
|
|
776
|
-
-
|
|
777
|
-
-
|
|
618
|
+
- [CLLocationUpdate](https://developer.apple.com/documentation/corelocation/cllocationupdate)
|
|
619
|
+
- [CLServiceSession](https://developer.apple.com/documentation/corelocation/clservicesession)
|
|
620
|
+
- [CLMonitor](https://developer.apple.com/documentation/corelocation/clmonitor)
|
|
621
|
+
- [CLBackgroundActivitySession](https://developer.apple.com/documentation/corelocation/clbackgroundactivitysession)
|
|
622
|
+
- [Location authorization](https://developer.apple.com/documentation/corelocation/requesting-authorization-to-use-location-services)
|
|
623
|
+
- [Background location updates](https://developer.apple.com/documentation/corelocation/handling-location-updates-in-the-background)
|