@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
|
@@ -1,793 +1,691 @@
|
|
|
1
|
-
# MapKit Patterns
|
|
1
|
+
# MapKit Patterns for SwiftUI
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
```swift
|
|
7
|
-
import MapKit
|
|
8
|
-
import SwiftUI
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
---
|
|
3
|
+
Everything here targets iOS 17 and SwiftUI unless a heading names a later
|
|
4
|
+
release. Each file that uses these types imports both `MapKit` and `SwiftUI`.
|
|
12
5
|
|
|
13
6
|
## Contents
|
|
14
7
|
|
|
15
|
-
- [
|
|
16
|
-
- [Custom
|
|
17
|
-
- [Camera Control
|
|
18
|
-
- [
|
|
19
|
-
- [Search
|
|
8
|
+
- [A Full Map Screen](#a-full-map-screen)
|
|
9
|
+
- [Custom Annotations](#custom-annotations)
|
|
10
|
+
- [Camera Control](#camera-control)
|
|
11
|
+
- [Selection](#selection)
|
|
12
|
+
- [Autocomplete Search](#autocomplete-search)
|
|
20
13
|
- [Route Display](#route-display)
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
14
|
+
- [Cycling Directions (iOS 14+)](#cycling-directions-ios-14)
|
|
15
|
+
- [Street-Level Look Around](#street-level-look-around)
|
|
16
|
+
- [Still Images with MKMapSnapshotter](#still-images-with-mkmapsnapshotter)
|
|
17
|
+
- [Dense Annotations and Clustering](#dense-annotations-and-clustering)
|
|
18
|
+
- [MKMapItem Helpers](#mkmapitem-helpers)
|
|
19
|
+
- [Geocoding and Places on iOS 26](#geocoding-and-places-on-ios-26)
|
|
20
|
+
- [Showing the User](#showing-the-user)
|
|
21
|
+
- [Maps Inside Scrolling Content](#maps-inside-scrolling-content)
|
|
22
|
+
- [Coordinate Math](#coordinate-math)
|
|
29
23
|
- [Accessibility](#accessibility)
|
|
30
|
-
- [
|
|
24
|
+
- [Apple Documentation](#apple-documentation)
|
|
31
25
|
|
|
32
|
-
##
|
|
26
|
+
## A Full Map Screen
|
|
33
27
|
|
|
34
|
-
A
|
|
28
|
+
A bike-dock finder: user location, one marker per dock coloured by
|
|
29
|
+
availability, built-in points of interest hidden, a detail card for the
|
|
30
|
+
selected dock.
|
|
35
31
|
|
|
36
32
|
```swift
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
import SwiftUI
|
|
34
|
+
import MapKit
|
|
35
|
+
|
|
36
|
+
struct BikeDock: Identifiable, Hashable {
|
|
37
|
+
let id: UUID
|
|
38
|
+
let name: String
|
|
39
|
+
let latitude: Double
|
|
40
|
+
let longitude: Double
|
|
41
|
+
let freeBikes: Int
|
|
42
|
+
|
|
43
|
+
var coordinate: CLLocationCoordinate2D {
|
|
44
|
+
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
struct DockFinderView: View {
|
|
49
|
+
let docks: [BikeDock]
|
|
50
|
+
@State private var camera: MapCameraPosition = .userLocation(fallback: .automatic)
|
|
51
|
+
@State private var picked: BikeDock?
|
|
41
52
|
|
|
42
53
|
var body: some View {
|
|
43
|
-
Map(position: $
|
|
54
|
+
Map(position: $camera, selection: $picked) {
|
|
44
55
|
UserAnnotation()
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
.tint(store.isOpen ? .green : .gray)
|
|
50
|
-
.tag(store)
|
|
56
|
+
ForEach(docks) { dock in
|
|
57
|
+
Marker(dock.name, systemImage: "bicycle", coordinate: dock.coordinate)
|
|
58
|
+
.tint(dock.freeBikes > 0 ? .green : .gray)
|
|
59
|
+
.tag(dock)
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
|
-
.mapStyle(.standard(pointsOfInterest: .excludingAll))
|
|
54
62
|
.mapControls {
|
|
55
|
-
MapUserLocationButton()
|
|
56
63
|
MapCompass()
|
|
57
|
-
MapScaleView()
|
|
58
64
|
MapPitchToggle()
|
|
65
|
+
MapScaleView()
|
|
66
|
+
MapUserLocationButton()
|
|
59
67
|
}
|
|
68
|
+
.mapStyle(.standard(pointsOfInterest: .excludingAll))
|
|
60
69
|
.safeAreaInset(edge: .bottom) {
|
|
61
|
-
if let
|
|
62
|
-
|
|
70
|
+
if let picked {
|
|
71
|
+
Text("\(picked.name): \(picked.freeBikes) bikes")
|
|
63
72
|
.padding()
|
|
73
|
+
.frame(maxWidth: .infinity)
|
|
74
|
+
.background(.regularMaterial)
|
|
64
75
|
}
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
}
|
|
68
79
|
```
|
|
69
80
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
let name: String
|
|
76
|
-
let coordinate: CLLocationCoordinate2D
|
|
77
|
-
let isOpen: Bool
|
|
78
|
-
|
|
79
|
-
static func == (lhs: Store, rhs: Store) -> Bool { lhs.id == rhs.id }
|
|
80
|
-
func hash(into hasher: inout Hasher) { hasher.combine(id) }
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
---
|
|
81
|
+
Whatever you tag and select must be `Hashable`. `CLLocationCoordinate2D` is
|
|
82
|
+
neither `Hashable` nor `Equatable`, so a model that stores one cannot get the
|
|
83
|
+
synthesised conformance. Either keep plain `Double` latitude and longitude, as
|
|
84
|
+
above, and build the coordinate on demand, or write `==` and `hash(into:)`
|
|
85
|
+
yourself using only `id`.
|
|
85
86
|
|
|
86
|
-
## Custom
|
|
87
|
+
## Custom Annotations
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
`Marker
|
|
90
|
-
|
|
89
|
+
`Annotation` takes arbitrary SwiftUI content. For an ordinary pin, prefer
|
|
90
|
+
`Marker`: it looks like the platform marker and VoiceOver reads its title
|
|
91
|
+
without extra work.
|
|
91
92
|
|
|
92
93
|
```swift
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
struct FriendPin: MapContent {
|
|
95
|
+
let name: String
|
|
96
|
+
let photo: URL
|
|
97
|
+
let spot: CLLocationCoordinate2D
|
|
98
|
+
|
|
99
|
+
var body: some MapContent {
|
|
100
|
+
Annotation(name, coordinate: spot, anchor: .bottom) {
|
|
96
101
|
VStack(spacing: 0) {
|
|
97
|
-
AsyncImage(url:
|
|
98
|
-
|
|
102
|
+
AsyncImage(url: photo) { loaded in
|
|
103
|
+
loaded.resizable().aspectRatio(contentMode: .fill)
|
|
99
104
|
} placeholder: {
|
|
100
|
-
|
|
105
|
+
ProgressView()
|
|
101
106
|
}
|
|
102
107
|
.frame(width: 40, height: 40)
|
|
103
|
-
.clipShape(
|
|
104
|
-
.overlay
|
|
108
|
+
.clipShape(Circle())
|
|
109
|
+
.overlay { Circle().strokeBorder(.white, lineWidth: 2) }
|
|
105
110
|
|
|
106
|
-
Image(systemName: "
|
|
107
|
-
.
|
|
111
|
+
Image(systemName: "arrowtriangle.down.fill")
|
|
112
|
+
.imageScale(.small)
|
|
108
113
|
.foregroundStyle(.white)
|
|
109
|
-
.rotationEffect(.degrees(180))
|
|
110
|
-
.offset(y: -3)
|
|
111
114
|
}
|
|
112
115
|
}
|
|
113
116
|
}
|
|
114
117
|
}
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
###
|
|
120
|
+
### Callout-style annotation anchored at the bottom
|
|
121
|
+
|
|
122
|
+
SwiftUI's `Annotation` positions content with `anchor`, a `UnitPoint`; it has
|
|
123
|
+
no point-offset parameter. For a label floating above a pin, stack the label
|
|
124
|
+
over the symbol and anchor at `.bottom`, so the tip of the symbol sits on the
|
|
125
|
+
coordinate.
|
|
118
126
|
|
|
119
127
|
```swift
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
struct LabeledPin: MapContent {
|
|
129
|
+
let title: String
|
|
130
|
+
let spot: CLLocationCoordinate2D
|
|
131
|
+
|
|
132
|
+
var body: some MapContent {
|
|
133
|
+
Annotation(title, coordinate: spot, anchor: .bottom) {
|
|
134
|
+
VStack(spacing: 3) {
|
|
135
|
+
Text(title)
|
|
136
|
+
.font(.caption.weight(.semibold))
|
|
137
|
+
.padding(.horizontal, 8)
|
|
138
|
+
.padding(.vertical, 4)
|
|
139
|
+
.background(.ultraThinMaterial, in: Capsule())
|
|
140
|
+
Image(systemName: "mappin.circle.fill")
|
|
141
|
+
.font(.title2)
|
|
142
|
+
.foregroundStyle(.white, .pink)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
132
145
|
}
|
|
133
146
|
}
|
|
134
147
|
```
|
|
135
148
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
## Camera Control (MapCameraPosition)
|
|
149
|
+
## Camera Control
|
|
139
150
|
|
|
140
151
|
### Setting an initial position
|
|
141
152
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
center: CLLocationCoordinate2D(latitude: 37.334, longitude: -122.009),
|
|
150
|
-
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
|
|
151
|
-
)
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
// Follow user location
|
|
155
|
-
@State private var position: MapCameraPosition = .userLocation(fallback: .automatic)
|
|
156
|
-
|
|
157
|
-
// Specific camera angle (3D perspective)
|
|
158
|
-
@State private var position: MapCameraPosition = .camera(
|
|
159
|
-
MapCamera(centerCoordinate: applePark, distance: 1000, heading: 90, pitch: 60)
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
// Frame specific items
|
|
163
|
-
position = .item(MKMapItem.forCurrentLocation())
|
|
164
|
-
position = .rect(MKMapRect(...))
|
|
165
|
-
```
|
|
153
|
+
| Position | Shows |
|
|
154
|
+
|----------|-------|
|
|
155
|
+
| `.region(MKCoordinateRegion(center: c, span: nearbySpan))` | a region around a centre (`nearbySpan` here is about 0.05 degrees each way) |
|
|
156
|
+
| `.userLocation(fallback: .automatic)` | follows the user |
|
|
157
|
+
| `.camera(MapCamera(centerCoordinate: c, distance: 1200, heading: 120, pitch: 55))` | a tilted 3D viewpoint, 1.2 km out, facing south-east |
|
|
158
|
+
| `.item(MKMapItem.forCurrentLocation())` | frames a map item |
|
|
159
|
+
| `.rect(someMapRect)` | frames an `MKMapRect` |
|
|
166
160
|
|
|
167
161
|
### Animate camera changes
|
|
168
162
|
|
|
169
|
-
|
|
163
|
+
Assign inside `withAnimation` (any curve, for example `.easeInOut`) for a fly-to:
|
|
170
164
|
|
|
171
165
|
```swift
|
|
172
|
-
func
|
|
173
|
-
withAnimation(.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
heading: 0, pitch: 45)
|
|
177
|
-
)
|
|
166
|
+
func fly(to target: CLLocationCoordinate2D) {
|
|
167
|
+
withAnimation(.smooth(duration: 1)) {
|
|
168
|
+
let overhead = MapCamera(centerCoordinate: target, distance: 1800, heading: 30, pitch: 50)
|
|
169
|
+
camera = .camera(overhead)
|
|
178
170
|
}
|
|
179
171
|
}
|
|
180
172
|
```
|
|
181
173
|
|
|
182
174
|
### Frame multiple annotations
|
|
183
175
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
position = .automatic // Frames all map content
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Or frame a specific rect
|
|
192
|
-
func frameRegion(_ region: MKCoordinateRegion) {
|
|
193
|
-
withAnimation {
|
|
194
|
-
position = .region(region)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
```
|
|
176
|
+
`camera = .automatic` (inside `withAnimation`) frames all map content.
|
|
177
|
+
`camera = .region(someRegion)` frames a region you computed, for example with
|
|
178
|
+
the helper in [Coordinate Math](#coordinate-math).
|
|
198
179
|
|
|
199
180
|
### Read current camera position
|
|
200
181
|
|
|
201
|
-
Use `onMapCameraChange` to observe what the user is looking at:
|
|
202
|
-
|
|
203
182
|
```swift
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
.onMapCameraChange(frequency: .onEnd) { context in
|
|
208
|
-
visibleRegion = context.region
|
|
183
|
+
Map(position: $camera) { /* content */ }
|
|
184
|
+
.onMapCameraChange(frequency: .onEnd) { ctx in
|
|
185
|
+
visibleRegion = ctx.region
|
|
209
186
|
}
|
|
210
187
|
```
|
|
211
188
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
---
|
|
189
|
+
`.onEnd` fires once the user stops moving the map. `.continuous` fires during
|
|
190
|
+
the gesture and costs more CPU; use it only for live tracking.
|
|
216
191
|
|
|
217
|
-
##
|
|
192
|
+
## Selection
|
|
218
193
|
|
|
219
|
-
###
|
|
194
|
+
### Selecting an MKMapItem
|
|
220
195
|
|
|
221
|
-
|
|
196
|
+
Bind selection to `MKMapItem?` when a pick should lead to place details.
|
|
222
197
|
|
|
223
198
|
```swift
|
|
224
|
-
|
|
199
|
+
struct MuseumMap: View {
|
|
200
|
+
let museums: [MKMapItem]
|
|
201
|
+
@State private var chosen: MKMapItem?
|
|
202
|
+
@State private var streetScene: MKLookAroundScene?
|
|
225
203
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
204
|
+
var body: some View {
|
|
205
|
+
Map(selection: $chosen) {
|
|
206
|
+
ForEach(museums, id: \.self) { museum in
|
|
207
|
+
Marker(item: museum)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
.onChange(of: chosen) { _, museum in
|
|
211
|
+
guard let museum else { return }
|
|
212
|
+
Task { streetScene = try? await MKLookAroundSceneRequest(mapItem: museum).scene }
|
|
213
|
+
}
|
|
229
214
|
}
|
|
230
215
|
}
|
|
231
|
-
.onChange(of: selectedItem) { _, newItem in
|
|
232
|
-
guard let item = newItem else { return }
|
|
233
|
-
Task { await fetchLookAround(for: item) }
|
|
234
|
-
}
|
|
235
216
|
```
|
|
236
217
|
|
|
237
|
-
###
|
|
238
|
-
|
|
239
|
-
```swift
|
|
240
|
-
@State private var selectedPlaceID: Place.ID?
|
|
241
|
-
|
|
242
|
-
Map(selection: $selectedPlaceID) {
|
|
243
|
-
ForEach(places) { place in
|
|
244
|
-
Marker(place.name, coordinate: place.coordinate)
|
|
245
|
-
.tag(place.id)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
```
|
|
218
|
+
### Selecting by your own ID
|
|
249
219
|
|
|
250
|
-
|
|
220
|
+
Bind to `Place.ID?` and tag each marker with `.tag(place.id)`. The tag type and
|
|
221
|
+
the binding type must match.
|
|
251
222
|
|
|
252
|
-
## Search
|
|
223
|
+
## Autocomplete Search
|
|
253
224
|
|
|
254
|
-
|
|
255
|
-
|
|
225
|
+
Suggestions come from `MKLocalSearchCompleter`; picking one runs a full
|
|
226
|
+
`MKLocalSearch` that returns `MKMapItem`s.
|
|
256
227
|
|
|
257
228
|
```swift
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
var completions: [MKLocalSearchCompletion] = []
|
|
261
|
-
var searchResults: [MKMapItem] = []
|
|
262
|
-
var queryFragment: String = "" {
|
|
263
|
-
didSet { completer.queryFragment = queryFragment }
|
|
264
|
-
}
|
|
229
|
+
import MapKit
|
|
230
|
+
import Observation
|
|
265
231
|
|
|
232
|
+
@MainActor
|
|
233
|
+
@Observable
|
|
234
|
+
final class PlaceFinder: NSObject, MKLocalSearchCompleterDelegate {
|
|
235
|
+
private(set) var suggestions: [MKLocalSearchCompletion] = []
|
|
236
|
+
private(set) var matches: [MKMapItem] = []
|
|
266
237
|
private let completer: MKLocalSearchCompleter
|
|
267
238
|
|
|
239
|
+
var fragment = "" {
|
|
240
|
+
didSet { completer.queryFragment = fragment }
|
|
241
|
+
}
|
|
242
|
+
|
|
268
243
|
override init() {
|
|
269
|
-
|
|
244
|
+
let engine = MKLocalSearchCompleter()
|
|
245
|
+
engine.resultTypes = [.address, .pointOfInterest]
|
|
246
|
+
completer = engine
|
|
270
247
|
super.init()
|
|
271
248
|
completer.delegate = self
|
|
272
|
-
completer.resultTypes = [.address, .pointOfInterest]
|
|
273
249
|
}
|
|
274
250
|
|
|
275
|
-
|
|
276
|
-
func updateRegion(_ region: MKCoordinateRegion) {
|
|
251
|
+
func focus(on region: MKCoordinateRegion) {
|
|
277
252
|
completer.region = region
|
|
278
253
|
}
|
|
279
254
|
|
|
280
|
-
func completerDidUpdateResults(_
|
|
281
|
-
|
|
255
|
+
nonisolated func completerDidUpdateResults(_ source: MKLocalSearchCompleter) {
|
|
256
|
+
MainActor.assumeIsolated { suggestions = completer.results }
|
|
282
257
|
}
|
|
283
258
|
|
|
284
|
-
func completer(_
|
|
285
|
-
|
|
259
|
+
nonisolated func completer(_ source: MKLocalSearchCompleter, didFailWithError failure: any Error) {
|
|
260
|
+
MainActor.assumeIsolated { suggestions = [] }
|
|
286
261
|
}
|
|
287
262
|
|
|
288
|
-
func
|
|
289
|
-
let request = MKLocalSearch.Request(completion:
|
|
263
|
+
func choose(_ suggestion: MKLocalSearchCompletion) async {
|
|
264
|
+
let request = MKLocalSearch.Request(completion: suggestion)
|
|
290
265
|
request.resultTypes = [.pointOfInterest, .address]
|
|
266
|
+
let search = MKLocalSearch(request: request)
|
|
291
267
|
do {
|
|
292
|
-
let
|
|
293
|
-
|
|
268
|
+
let reply = try await search.start()
|
|
269
|
+
matches = reply.mapItems
|
|
294
270
|
} catch {
|
|
295
|
-
|
|
271
|
+
matches = []
|
|
296
272
|
}
|
|
297
273
|
}
|
|
298
274
|
}
|
|
299
275
|
```
|
|
300
276
|
|
|
301
|
-
|
|
277
|
+
`focus(on:)` narrows suggestions to the visible region. The delegate copies
|
|
278
|
+
`results` on update and clears the list on failure. It reads the model's own
|
|
279
|
+
`completer` inside `assumeIsolated`, not the callback parameter: the parameter
|
|
280
|
+
is a non-`Sendable` value from a nonisolated context, and Swift 6 will not
|
|
281
|
+
send it into the main-actor closure.
|
|
282
|
+
|
|
283
|
+
### Wiring it into a screen
|
|
302
284
|
|
|
303
285
|
```swift
|
|
304
|
-
struct
|
|
305
|
-
@State private var
|
|
306
|
-
@State private var
|
|
307
|
-
@State private var
|
|
286
|
+
struct PlaceSearchScreen: View {
|
|
287
|
+
@State private var finder = PlaceFinder()
|
|
288
|
+
@State private var typed = ""
|
|
289
|
+
@State private var camera: MapCameraPosition = .automatic
|
|
308
290
|
|
|
309
291
|
var body: some View {
|
|
310
|
-
Map(position: $
|
|
311
|
-
ForEach(
|
|
312
|
-
Marker(item: item)
|
|
313
|
-
}
|
|
292
|
+
Map(position: $camera) {
|
|
293
|
+
ForEach(finder.matches, id: \.self) { Marker(item: $0) }
|
|
314
294
|
}
|
|
315
|
-
.searchable(text: $
|
|
295
|
+
.searchable(text: $typed, prompt: "Search places")
|
|
316
296
|
.searchSuggestions {
|
|
317
|
-
ForEach(
|
|
297
|
+
ForEach(finder.suggestions, id: \.self) { suggestion in
|
|
318
298
|
Button {
|
|
319
|
-
Task { await
|
|
299
|
+
Task { await finder.choose(suggestion) }
|
|
320
300
|
} label: {
|
|
321
301
|
VStack(alignment: .leading) {
|
|
322
|
-
Text(
|
|
323
|
-
Text(
|
|
324
|
-
.font(.caption)
|
|
325
|
-
.foregroundStyle(.secondary)
|
|
302
|
+
Text(suggestion.title)
|
|
303
|
+
Text(suggestion.subtitle).font(.caption).foregroundStyle(.secondary)
|
|
326
304
|
}
|
|
327
305
|
}
|
|
328
306
|
}
|
|
329
307
|
}
|
|
330
|
-
.task(id:
|
|
331
|
-
|
|
308
|
+
.task(id: typed) {
|
|
309
|
+
let quietPeriod = Duration.milliseconds(300)
|
|
310
|
+
try? await Task.sleep(for: quietPeriod)
|
|
332
311
|
guard !Task.isCancelled else { return }
|
|
333
|
-
|
|
312
|
+
finder.fragment = typed
|
|
334
313
|
}
|
|
335
314
|
.onMapCameraChange(frequency: .onEnd) { context in
|
|
336
|
-
|
|
315
|
+
finder.focus(on: context.region)
|
|
337
316
|
}
|
|
338
317
|
}
|
|
339
318
|
}
|
|
340
319
|
```
|
|
341
320
|
|
|
342
|
-
|
|
321
|
+
`.task(id:)` restarts on each keystroke and cancels the previous sleep, which
|
|
322
|
+
is the debounce.
|
|
343
323
|
|
|
344
324
|
## Route Display
|
|
345
325
|
|
|
346
|
-
Calculate directions and draw the route polyline on the map.
|
|
347
|
-
|
|
348
326
|
```swift
|
|
349
|
-
struct
|
|
350
|
-
let
|
|
351
|
-
let
|
|
327
|
+
struct CommuteView: View {
|
|
328
|
+
let home: MKMapItem
|
|
329
|
+
let office: MKMapItem
|
|
352
330
|
@State private var route: MKRoute?
|
|
353
|
-
@State private var
|
|
354
|
-
@State private var
|
|
331
|
+
@State private var camera: MapCameraPosition = .automatic
|
|
332
|
+
@State private var failed = false
|
|
355
333
|
|
|
356
334
|
var body: some View {
|
|
357
|
-
Map(position: $
|
|
335
|
+
Map(position: $camera) {
|
|
358
336
|
if let route {
|
|
359
|
-
MapPolyline(route.polyline)
|
|
360
|
-
.stroke(.blue, lineWidth: 6)
|
|
337
|
+
MapPolyline(route.polyline).stroke(.indigo, lineWidth: 6)
|
|
361
338
|
}
|
|
362
|
-
Marker(item:
|
|
363
|
-
|
|
364
|
-
Marker(item: destination)
|
|
365
|
-
.tint(.red)
|
|
339
|
+
Marker(item: home).tint(.green)
|
|
340
|
+
Marker(item: office).tint(.red)
|
|
366
341
|
}
|
|
367
342
|
.overlay(alignment: .top) {
|
|
368
|
-
if
|
|
369
|
-
Text(
|
|
370
|
-
|
|
371
|
-
.padding()
|
|
372
|
-
.background(.ultraThinMaterial, in:
|
|
373
|
-
|
|
343
|
+
if let route {
|
|
344
|
+
Text(Duration.seconds(route.expectedTravelTime)
|
|
345
|
+
.formatted(.units(allowed: [.hours, .minutes], width: .abbreviated)))
|
|
346
|
+
.padding(8)
|
|
347
|
+
.background(.ultraThinMaterial, in: Capsule())
|
|
348
|
+
} else if failed {
|
|
349
|
+
Text("No route found").padding(8).background(.ultraThinMaterial, in: Capsule())
|
|
374
350
|
}
|
|
375
351
|
}
|
|
376
|
-
.task { await
|
|
352
|
+
.task { await load() }
|
|
377
353
|
}
|
|
378
354
|
|
|
379
|
-
private func
|
|
355
|
+
private func load() async {
|
|
380
356
|
let request = MKDirections.Request()
|
|
381
|
-
request.source =
|
|
382
|
-
request.destination =
|
|
357
|
+
request.source = home
|
|
358
|
+
request.destination = office
|
|
383
359
|
request.transportType = .automobile
|
|
384
|
-
|
|
360
|
+
let directions = MKDirections(request: request)
|
|
385
361
|
do {
|
|
386
|
-
let
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
formatter.unitsStyle = .abbreviated
|
|
391
|
-
formatter.allowedUnits = [.hour, .minute]
|
|
392
|
-
travelTime = formatter.string(from: route.expectedTravelTime) ?? ""
|
|
393
|
-
|
|
394
|
-
withAnimation {
|
|
395
|
-
position = .rect(route.polyline.boundingMapRect)
|
|
396
|
-
}
|
|
362
|
+
let reply = try await directions.calculate()
|
|
363
|
+
guard let best = reply.routes.first else {
|
|
364
|
+
failed = true
|
|
365
|
+
return
|
|
397
366
|
}
|
|
367
|
+
route = best
|
|
368
|
+
withAnimation { camera = .rect(best.polyline.boundingMapRect) }
|
|
398
369
|
} catch {
|
|
399
|
-
|
|
370
|
+
failed = true
|
|
400
371
|
}
|
|
401
372
|
}
|
|
402
373
|
}
|
|
403
374
|
```
|
|
404
375
|
|
|
405
|
-
|
|
376
|
+
The travel time is formatted as hours and minutes in abbreviated units
|
|
377
|
+
(`DateComponentsFormatter` with `.abbreviated` and `[.hour, .minute]` does
|
|
378
|
+
the same on older code). After the route arrives the camera animates to the
|
|
379
|
+
polyline's `boundingMapRect`. Failures set visible state rather than being swallowed.
|
|
380
|
+
|
|
381
|
+
### Alternate routes
|
|
382
|
+
|
|
383
|
+
Set `request.requestsAlternateRoutes = true` and `response.routes` holds
|
|
384
|
+
several. Emphasise the first, mute the rest:
|
|
406
385
|
|
|
407
386
|
```swift
|
|
408
|
-
|
|
409
|
-
let
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// Display all routes, highlight the selected one:
|
|
413
|
-
ForEach(Array(response.routes.enumerated()), id: \.offset) { index, route in
|
|
414
|
-
MapPolyline(route.polyline)
|
|
415
|
-
.stroke(index == 0 ? .blue : .gray.opacity(0.5), lineWidth: index == 0 ? 6 : 3)
|
|
387
|
+
ForEach(Array(routes.enumerated()), id: \.offset) { position, option in
|
|
388
|
+
let isBest = position == 0
|
|
389
|
+
MapPolyline(option.polyline)
|
|
390
|
+
.stroke(isBest ? Color.blue : Color.secondary.opacity(0.5), lineWidth: isBest ? 6 : 3)
|
|
416
391
|
}
|
|
417
392
|
```
|
|
418
393
|
|
|
419
394
|
### ETA Calculation
|
|
420
395
|
|
|
421
|
-
|
|
396
|
+
When only the duration matters, skip the geometry:
|
|
422
397
|
|
|
423
398
|
```swift
|
|
424
|
-
func
|
|
399
|
+
func minutesAway(_ request: MKDirections.Request) async throws -> TimeInterval {
|
|
400
|
+
try await MKDirections(request: request).calculateETA().expectedTravelTime
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Cycling Directions (iOS 14+)
|
|
405
|
+
|
|
406
|
+
`MKDirectionsTransportType.cycling` has existed since iOS 14; it is not an
|
|
407
|
+
iOS 26 addition.
|
|
408
|
+
|
|
409
|
+
```swift
|
|
410
|
+
func bikeRoute(toward stop: MKMapItem) async throws -> MKRoute? {
|
|
425
411
|
let request = MKDirections.Request()
|
|
426
|
-
request.source =
|
|
427
|
-
request.destination =
|
|
412
|
+
request.source = .forCurrentLocation()
|
|
413
|
+
request.destination = stop
|
|
414
|
+
request.transportType = .cycling
|
|
428
415
|
let directions = MKDirections(request: request)
|
|
429
|
-
let
|
|
430
|
-
return
|
|
416
|
+
let reply = try await directions.calculate()
|
|
417
|
+
return reply.routes.first
|
|
431
418
|
}
|
|
432
419
|
```
|
|
433
420
|
|
|
434
|
-
|
|
421
|
+
## Street-Level Look Around
|
|
435
422
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
Show Apple's street-level imagery for a selected location. Availability
|
|
439
|
-
depends on region coverage.
|
|
423
|
+
Look Around is Apple's street-level imagery. Coverage is regional, so always
|
|
424
|
+
plan for "no scene".
|
|
440
425
|
|
|
441
426
|
```swift
|
|
442
|
-
struct
|
|
443
|
-
let
|
|
427
|
+
struct StreetPreview: View {
|
|
428
|
+
let place: MKMapItem
|
|
444
429
|
@State private var scene: MKLookAroundScene?
|
|
445
430
|
|
|
446
431
|
var body: some View {
|
|
447
|
-
|
|
432
|
+
ZStack {
|
|
448
433
|
if let scene {
|
|
449
434
|
LookAroundPreview(scene: .constant(scene))
|
|
450
|
-
.frame(height: 200)
|
|
451
|
-
.clipShape(.rect(cornerRadius: 12))
|
|
452
435
|
} else {
|
|
453
|
-
ContentUnavailableView("No
|
|
454
|
-
systemImage: "eye.slash",
|
|
455
|
-
description: Text("Look Around is not available here."))
|
|
436
|
+
ContentUnavailableView("No street view here", systemImage: "eye.slash")
|
|
456
437
|
}
|
|
457
438
|
}
|
|
458
|
-
.
|
|
439
|
+
.frame(height: 200)
|
|
440
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
441
|
+
.task(id: place) {
|
|
459
442
|
scene = nil
|
|
460
|
-
|
|
461
|
-
scene = try? await request.scene
|
|
443
|
+
scene = try? await MKLookAroundSceneRequest(mapItem: place).scene
|
|
462
444
|
}
|
|
463
445
|
}
|
|
464
446
|
}
|
|
465
447
|
```
|
|
466
448
|
|
|
467
|
-
###
|
|
449
|
+
### Street view floating over the map
|
|
468
450
|
|
|
469
451
|
```swift
|
|
470
|
-
Map(selection: $
|
|
452
|
+
Map(selection: $chosen) { /* markers */ }
|
|
471
453
|
.overlay(alignment: .bottomTrailing) {
|
|
472
|
-
if
|
|
473
|
-
LookAroundPreview(scene: $
|
|
474
|
-
.frame(width: 200, height: 130)
|
|
475
|
-
.clipShape(
|
|
454
|
+
if streetScene != nil {
|
|
455
|
+
LookAroundPreview(scene: $streetScene)
|
|
456
|
+
.frame(width: 200, height: 130, alignment: .center)
|
|
457
|
+
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
476
458
|
.padding()
|
|
477
459
|
}
|
|
478
460
|
}
|
|
479
|
-
.onChange(of:
|
|
480
|
-
guard let item =
|
|
481
|
-
|
|
482
|
-
return
|
|
483
|
-
}
|
|
484
|
-
Task {
|
|
485
|
-
let request = MKLookAroundSceneRequest(mapItem: item)
|
|
486
|
-
lookAroundScene = try? await request.scene
|
|
487
|
-
}
|
|
461
|
+
.onChange(of: chosen) { _, item in
|
|
462
|
+
guard let item else { streetScene = nil; return }
|
|
463
|
+
Task { streetScene = try? await MKLookAroundSceneRequest(mapItem: item).scene }
|
|
488
464
|
}
|
|
489
465
|
```
|
|
490
466
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
## Map Snapshots (MKMapSnapshotter)
|
|
467
|
+
## Still Images with MKMapSnapshotter
|
|
494
468
|
|
|
495
|
-
|
|
496
|
-
|
|
469
|
+
A snapshot is a still image of a map: share sheets, widgets, notification
|
|
470
|
+
attachments, list thumbnails.
|
|
497
471
|
|
|
498
472
|
```swift
|
|
499
|
-
|
|
500
|
-
|
|
473
|
+
import MapKit
|
|
474
|
+
import UIKit
|
|
475
|
+
|
|
476
|
+
let cardSize = CGSize(width: 300, height: 200)
|
|
477
|
+
|
|
478
|
+
func thumbnail(of spot: CLLocationCoordinate2D, fitting frame: CGSize = cardSize) async throws -> UIImage {
|
|
479
|
+
let size = frame
|
|
501
480
|
let options = MKMapSnapshotter.Options()
|
|
502
|
-
options.region = MKCoordinateRegion(
|
|
503
|
-
center: center,
|
|
504
|
-
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
|
|
505
|
-
)
|
|
506
|
-
options.size = size
|
|
507
481
|
options.mapType = .standard
|
|
508
482
|
options.showsBuildings = true
|
|
483
|
+
options.size = size
|
|
484
|
+
options.region = MKCoordinateRegion(center: spot, latitudinalMeters: 1100, longitudinalMeters: 1100)
|
|
485
|
+
|
|
486
|
+
let shot = try await MKMapSnapshotter(options: options).start()
|
|
487
|
+
let pinPoint = shot.point(for: spot)
|
|
488
|
+
let symbol = UIImage(systemName: "mappin.circle.fill")
|
|
489
|
+
let pin = symbol?.withTintColor(.systemRed)
|
|
509
490
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
// Draw a pin at the center
|
|
514
|
-
let image = UIGraphicsImageRenderer(size: size).image { context in
|
|
515
|
-
snapshot.image.draw(at: .zero)
|
|
516
|
-
let point = snapshot.point(for: center)
|
|
517
|
-
let pin = UIImage(systemName: "mappin.circle.fill")?
|
|
518
|
-
.withTintColor(.red, renderingMode: .alwaysOriginal)
|
|
519
|
-
pin?.draw(at: CGPoint(x: point.x - 15, y: point.y - 30),
|
|
520
|
-
blendMode: .normal, alpha: 1.0)
|
|
491
|
+
return UIGraphicsImageRenderer(size: size).image { _ in
|
|
492
|
+
shot.image.draw(at: .zero)
|
|
493
|
+
pin?.draw(at: CGPoint(x: pinPoint.x - 12, y: pinPoint.y - 24))
|
|
521
494
|
}
|
|
522
|
-
return image
|
|
523
495
|
}
|
|
524
496
|
```
|
|
525
497
|
|
|
526
|
-
|
|
498
|
+
A square of 1100 m is roughly 0.01 degrees of latitude: street level.
|
|
499
|
+
`point(for:)` converts a coordinate into the image's coordinate space.
|
|
527
500
|
|
|
528
501
|
## Dense Annotations and Clustering
|
|
529
502
|
|
|
530
|
-
|
|
531
|
-
on the current camera region. Use `MKMapView` when you need explicit cluster
|
|
532
|
-
configuration.
|
|
533
|
-
|
|
534
|
-
```swift
|
|
535
|
-
Map {
|
|
536
|
-
ForEach(allStores) { store in
|
|
537
|
-
Marker(store.name, systemImage: "cart", coordinate: store.coordinate)
|
|
538
|
-
.annotationTitles(.hidden) // Hide titles at low zoom
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
.mapStyle(.standard(pointsOfInterest: .excludingAll))
|
|
542
|
-
```
|
|
503
|
+
SwiftUI `Map` has no cluster configuration. With many markers:
|
|
543
504
|
|
|
544
|
-
|
|
545
|
-
|
|
505
|
+
- hide titles at low zoom with `.annotationTitles(.hidden)` on each `Marker`;
|
|
506
|
+
- show only what falls inside the current camera region;
|
|
507
|
+
- drop built-in points of interest with
|
|
508
|
+
`.mapStyle(.standard(pointsOfInterest: .excludingAll))`.
|
|
546
509
|
|
|
547
|
-
|
|
510
|
+
Real clustering needs `MKMapView` through UIKit interop: give each
|
|
511
|
+
`MKMarkerAnnotationView` the same `clusteringIdentifier`.
|
|
548
512
|
|
|
549
|
-
##
|
|
513
|
+
## MKMapItem Helpers
|
|
550
514
|
|
|
551
515
|
### Open in Apple Maps
|
|
552
516
|
|
|
517
|
+
On iOS 26 build the item from a location; `MKMapItem(placemark:)` is
|
|
518
|
+
deprecated there.
|
|
519
|
+
|
|
553
520
|
```swift
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
item
|
|
557
|
-
|
|
558
|
-
|
|
521
|
+
@available(iOS 26, *)
|
|
522
|
+
func openDrivingDirections(to spot: CLLocationCoordinate2D, name: String) {
|
|
523
|
+
let item = MKMapItem(location: CLLocation(latitude: spot.latitude, longitude: spot.longitude),
|
|
524
|
+
address: nil)
|
|
525
|
+
item.name = name
|
|
526
|
+
item.openInMaps(launchOptions: [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving])
|
|
527
|
+
}
|
|
559
528
|
```
|
|
560
529
|
|
|
561
|
-
|
|
530
|
+
Below iOS 26 the same call starts from
|
|
531
|
+
`MKMapItem(placemark: MKPlacemark(coordinate: spot))`.
|
|
532
|
+
|
|
533
|
+
### Distance
|
|
562
534
|
|
|
563
535
|
```swift
|
|
564
|
-
func
|
|
565
|
-
|
|
566
|
-
let
|
|
567
|
-
|
|
568
|
-
return locA.distance(from: locB) // meters
|
|
536
|
+
func metres(from origin: CLLocationCoordinate2D, to target: CLLocationCoordinate2D) -> CLLocationDistance {
|
|
537
|
+
let here = CLLocation(latitude: origin.latitude, longitude: origin.longitude)
|
|
538
|
+
let there = CLLocation(latitude: target.latitude, longitude: target.longitude)
|
|
539
|
+
return here.distance(from: there)
|
|
569
540
|
}
|
|
570
541
|
```
|
|
571
542
|
|
|
572
|
-
###
|
|
543
|
+
### Formatting distance
|
|
544
|
+
|
|
545
|
+
`MKDistanceFormatter` respects the user's locale and units:
|
|
573
546
|
|
|
574
547
|
```swift
|
|
575
|
-
let
|
|
576
|
-
|
|
577
|
-
let
|
|
548
|
+
let distanceText = MKDistanceFormatter()
|
|
549
|
+
distanceText.unitStyle = .abbreviated
|
|
550
|
+
let shown = distanceText.string(fromDistance: 1500)
|
|
578
551
|
```
|
|
579
552
|
|
|
580
|
-
|
|
553
|
+
`shown` is "0.9 mi" for a US-English user and "1.5 km" for most others.
|
|
581
554
|
|
|
582
|
-
## iOS 26
|
|
555
|
+
## Geocoding and Places on iOS 26
|
|
583
556
|
|
|
584
557
|
### MKGeocodingRequest
|
|
585
558
|
|
|
586
|
-
|
|
559
|
+
Address text in, `MKMapItem`s out, with more detail than `CLGeocoder` gave.
|
|
560
|
+
The initializer is failable.
|
|
587
561
|
|
|
588
562
|
```swift
|
|
589
563
|
@available(iOS 26, *)
|
|
590
|
-
func
|
|
591
|
-
var
|
|
592
|
-
for
|
|
593
|
-
guard let request = MKGeocodingRequest(addressString:
|
|
594
|
-
if let
|
|
595
|
-
|
|
564
|
+
func resolveAll(_ lines: [String]) async -> [MKMapItem] {
|
|
565
|
+
var found: [MKMapItem] = []
|
|
566
|
+
for line in lines {
|
|
567
|
+
guard let request = MKGeocodingRequest(addressString: line) else { continue }
|
|
568
|
+
if let items = try? await request.mapItems {
|
|
569
|
+
found.append(contentsOf: items)
|
|
596
570
|
}
|
|
597
571
|
}
|
|
598
|
-
return
|
|
572
|
+
return found
|
|
599
573
|
}
|
|
600
574
|
```
|
|
601
575
|
|
|
602
576
|
### MKReverseGeocodingRequest
|
|
603
577
|
|
|
604
|
-
Convert coordinates to map items with `MKAddress`:
|
|
605
|
-
|
|
606
578
|
```swift
|
|
607
579
|
@available(iOS 26, *)
|
|
608
|
-
func
|
|
609
|
-
let
|
|
610
|
-
|
|
611
|
-
guard let request = MKReverseGeocodingRequest(location: location) else {
|
|
612
|
-
return nil
|
|
613
|
-
}
|
|
614
|
-
let mapItems = try? await request.mapItems
|
|
615
|
-
return mapItems?.first?.address
|
|
580
|
+
func address(at spot: CLLocation) async throws -> MKAddress? {
|
|
581
|
+
guard let request = MKReverseGeocodingRequest(location: spot) else { return nil }
|
|
582
|
+
return try await request.mapItems.first?.address
|
|
616
583
|
}
|
|
617
584
|
```
|
|
618
585
|
|
|
619
586
|
### MKAddress and MKAddressRepresentations
|
|
620
587
|
|
|
621
|
-
`MKAddress`
|
|
622
|
-
`
|
|
623
|
-
|
|
624
|
-
```swift
|
|
625
|
-
@available(iOS 26, *)
|
|
626
|
-
func formatAddress(for item: MKMapItem) -> String {
|
|
627
|
-
// Use address representations for locale-aware formatting
|
|
628
|
-
item.addressRepresentations?.fullAddress(includingRegion: true, singleLine: true)
|
|
629
|
-
?? item.address?.fullAddress
|
|
630
|
-
?? ""
|
|
631
|
-
}
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### PlaceDescriptor (via GeoToolbox)
|
|
635
|
-
|
|
636
|
-
Create place references from coordinates when you do not have a Place ID:
|
|
588
|
+
`MKAddress` has `fullAddress` and an optional `shortAddress`.
|
|
589
|
+
`MKMapItem.addressRepresentations` formats for a context:
|
|
637
590
|
|
|
638
591
|
```swift
|
|
639
|
-
import GeoToolbox
|
|
640
|
-
|
|
641
592
|
@available(iOS 26, *)
|
|
642
|
-
func
|
|
643
|
-
let
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
)
|
|
647
|
-
|
|
648
|
-
let request = MKMapItemRequest(placeDescriptor: descriptor)
|
|
649
|
-
return try await request.mapItem
|
|
593
|
+
func oneLine(_ item: MKMapItem) -> String {
|
|
594
|
+
let formats = item.addressRepresentations
|
|
595
|
+
if let line = formats?.fullAddress(includingRegion: true, singleLine: true) { return line }
|
|
596
|
+
return item.address?.fullAddress ?? ""
|
|
650
597
|
}
|
|
651
|
-
|
|
652
|
-
// Use mapItem with any MapKit API: Marker(item:), directions, place cards
|
|
653
598
|
```
|
|
654
599
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
```swift
|
|
658
|
-
func cyclingRoute(to destination: MKMapItem) async throws -> MKRoute? {
|
|
659
|
-
let request = MKDirections.Request()
|
|
660
|
-
request.source = .forCurrentLocation()
|
|
661
|
-
request.destination = destination
|
|
662
|
-
request.transportType = .cycling
|
|
663
|
-
let response = try await MKDirections(request: request).calculate()
|
|
664
|
-
return response.routes.first
|
|
665
|
-
}
|
|
666
|
-
```
|
|
667
|
-
|
|
668
|
-
---
|
|
669
|
-
|
|
670
|
-
## User Location Display
|
|
600
|
+
`addressRepresentations` also offers `cityName`, `cityWithContext` and
|
|
601
|
+
`regionName`.
|
|
671
602
|
|
|
672
|
-
|
|
603
|
+
### PlaceDescriptor (via GeoToolbox)
|
|
673
604
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
.mapControls {
|
|
680
|
-
MapUserLocationButton() // Button to re-center on user
|
|
681
|
-
}
|
|
682
|
-
```
|
|
605
|
+
`import GeoToolbox`. A `PlaceDescriptor` names a place by coordinate or
|
|
606
|
+
address when no place ID exists; resolve it with
|
|
607
|
+
`MKMapItemRequest(placeDescriptor:)` and `try await request.mapItem`. The
|
|
608
|
+
result is an ordinary `MKMapItem`, usable with `Marker(item:)`, directions and
|
|
609
|
+
place cards. Example in [SKILL.md](../SKILL.md#placedescriptor-ios-26).
|
|
683
610
|
|
|
684
|
-
|
|
685
|
-
denied, the annotation does not appear and no error is thrown.
|
|
611
|
+
## Showing the User
|
|
686
612
|
|
|
687
|
-
|
|
613
|
+
- `UserAnnotation()` in the map content draws the blue dot and accuracy ring.
|
|
614
|
+
- `MapUserLocationButton()` in `.mapControls` recentres on the user.
|
|
615
|
+
- Without authorization the dot simply is not drawn and no error is raised,
|
|
616
|
+
so a missing dot is a permission question, not a rendering bug.
|
|
688
617
|
|
|
689
|
-
##
|
|
618
|
+
## Maps Inside Scrolling Content
|
|
690
619
|
|
|
691
|
-
|
|
692
|
-
|
|
620
|
+
Map gestures fight with scrolling. Turn them off for a thumbnail, or allow
|
|
621
|
+
only pinch zoom:
|
|
693
622
|
|
|
694
623
|
```swift
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
Marker("Location", coordinate: coord)
|
|
698
|
-
}
|
|
699
|
-
.frame(height: 200)
|
|
700
|
-
.clipShape(.rect(cornerRadius: 12))
|
|
701
|
-
|
|
702
|
-
Text("Details below the map...")
|
|
624
|
+
Map(position: $camera, interactionModes: []) {
|
|
625
|
+
Marker("Venue", coordinate: venue)
|
|
703
626
|
}
|
|
627
|
+
.frame(height: 200)
|
|
628
|
+
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
704
629
|
```
|
|
705
630
|
|
|
706
|
-
|
|
707
|
-
`interactionModes: [.zoom]` to allow pinch-to-zoom without pan conflicts.
|
|
708
|
-
|
|
709
|
-
---
|
|
631
|
+
`interactionModes: [.zoom]` keeps pinch without competing with the scroll.
|
|
710
632
|
|
|
711
|
-
## Coordinate
|
|
633
|
+
## Coordinate Math
|
|
712
634
|
|
|
713
|
-
###
|
|
635
|
+
### Bounding region for a set of points
|
|
714
636
|
|
|
715
637
|
```swift
|
|
716
|
-
func
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
for coord in coords {
|
|
726
|
-
minLat = min(minLat, coord.latitude)
|
|
727
|
-
maxLat = max(maxLat, coord.latitude)
|
|
728
|
-
minLon = min(minLon, coord.longitude)
|
|
729
|
-
maxLon = max(maxLon, coord.longitude)
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
let center = CLLocationCoordinate2D(
|
|
733
|
-
latitude: (minLat + maxLat) / 2,
|
|
734
|
-
longitude: (minLon + maxLon) / 2
|
|
735
|
-
)
|
|
736
|
-
let span = MKCoordinateSpan(
|
|
737
|
-
latitudeDelta: (maxLat - minLat) * 1.3, // 30% padding
|
|
738
|
-
longitudeDelta: (maxLon - minLon) * 1.3
|
|
638
|
+
func enclosingRegion(_ points: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
|
|
639
|
+
let lats = points.map(\.latitude)
|
|
640
|
+
let lons = points.map(\.longitude)
|
|
641
|
+
guard let south = lats.min(), let north = lats.max(),
|
|
642
|
+
let west = lons.min(), let east = lons.max() else { return MKCoordinateRegion() }
|
|
643
|
+
return MKCoordinateRegion(
|
|
644
|
+
center: CLLocationCoordinate2D(latitude: (south + north) / 2, longitude: (west + east) / 2),
|
|
645
|
+
span: MKCoordinateSpan(latitudeDelta: (north - south) * 1.3,
|
|
646
|
+
longitudeDelta: (east - west) * 1.3)
|
|
739
647
|
)
|
|
740
|
-
return MKCoordinateRegion(center: center, span: span)
|
|
741
648
|
}
|
|
742
649
|
```
|
|
743
650
|
|
|
651
|
+
The factor 1.3 leaves 30 percent padding around the points.
|
|
652
|
+
|
|
744
653
|
### CLLocationCoordinate2D Equatable conformance
|
|
745
654
|
|
|
746
|
-
|
|
747
|
-
|
|
655
|
+
The type is not `Equatable`. Add it with `@retroactive`, which (Swift 5.10
|
|
656
|
+
and later) acknowledges conforming another module's type to another module's
|
|
657
|
+
protocol and silences the warning:
|
|
748
658
|
|
|
749
659
|
```swift
|
|
750
660
|
extension CLLocationCoordinate2D: @retroactive Equatable {
|
|
751
|
-
public static func == (
|
|
752
|
-
|
|
661
|
+
public static func == (a: CLLocationCoordinate2D, b: CLLocationCoordinate2D) -> Bool {
|
|
662
|
+
a.latitude == b.latitude && a.longitude == b.longitude
|
|
753
663
|
}
|
|
754
664
|
}
|
|
755
665
|
```
|
|
756
666
|
|
|
757
|
-
Note: Use `@retroactive` (Swift 5.10+) to silence the warning about
|
|
758
|
-
conforming types from other modules.
|
|
759
|
-
|
|
760
|
-
---
|
|
761
|
-
|
|
762
667
|
## Accessibility
|
|
763
668
|
|
|
764
669
|
### Marker accessibility
|
|
765
670
|
|
|
766
|
-
`Marker`
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
```swift
|
|
770
|
-
Marker(store.name, coordinate: store.coordinate)
|
|
771
|
-
.accessibilityLabel("\(store.name), \(store.distanceText) away")
|
|
772
|
-
```
|
|
671
|
+
VoiceOver reads a `Marker`'s title. Add detail when it helps:
|
|
672
|
+
`.accessibilityLabel("\(dock.name), \(distanceText) away")`.
|
|
773
673
|
|
|
774
674
|
### Map accessibility
|
|
775
675
|
|
|
776
|
-
|
|
676
|
+
Summarise the map itself:
|
|
777
677
|
|
|
778
678
|
```swift
|
|
779
|
-
Map {
|
|
679
|
+
Map(position: $camera) { /* markers */ }
|
|
780
680
|
.accessibilityElement()
|
|
781
|
-
.accessibilityLabel("
|
|
681
|
+
.accessibilityLabel("Map of \(docks.count) bike docks nearby")
|
|
782
682
|
```
|
|
783
683
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
## References
|
|
684
|
+
## Apple Documentation
|
|
787
685
|
|
|
788
|
-
-
|
|
789
|
-
-
|
|
790
|
-
-
|
|
791
|
-
-
|
|
792
|
-
-
|
|
793
|
-
-
|
|
686
|
+
- [MapKit for SwiftUI](https://developer.apple.com/documentation/mapkit/mapkit-for-swiftui)
|
|
687
|
+
- [Map](https://developer.apple.com/documentation/mapkit/map)
|
|
688
|
+
- [MapCameraPosition](https://developer.apple.com/documentation/mapkit/mapcameraposition)
|
|
689
|
+
- [MKLocalSearch](https://developer.apple.com/documentation/mapkit/mklocalsearch)
|
|
690
|
+
- [MKDirections](https://developer.apple.com/documentation/mapkit/mkdirections)
|
|
691
|
+
- [MKLookAroundScene](https://developer.apple.com/documentation/mapkit/mklookaroundscene)
|