@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,1297 +1,997 @@
|
|
|
1
|
-
# App Intents
|
|
2
|
-
|
|
3
|
-
Extended App Intents patterns beyond the basics covered in the main skill.
|
|
4
|
-
Covers `@Parameter` variants, EntityPropertyQuery, assistant schemas, focus
|
|
5
|
-
filters, SiriKit migration, error handling, confirmation flows, authentication,
|
|
6
|
-
URL-representable types, and Spotlight indexing.
|
|
1
|
+
# App Intents: advanced guide
|
|
7
2
|
|
|
8
3
|
## Contents
|
|
9
4
|
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
12
|
-
- [EntityPropertyQuery
|
|
13
|
-
- [
|
|
14
|
-
- [
|
|
15
|
-
- [Visual Intelligence
|
|
16
|
-
- [Assistant
|
|
17
|
-
- [Focus
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [Confirmation
|
|
21
|
-
- [Authentication
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [
|
|
27
|
-
- [Core Spotlight Direct Usage](#core-spotlight-direct-usage)
|
|
28
|
-
|
|
29
|
-
## `@Parameter` Initializer Variants
|
|
30
|
-
|
|
31
|
-
### 1. Basic (String, Bool, URL, Date)
|
|
32
|
-
|
|
33
|
-
```swift
|
|
34
|
-
@Parameter(title: "Name")
|
|
35
|
-
var name: String
|
|
5
|
+
- [Parameter initializers](#parameter-initializers)
|
|
6
|
+
- [Entity queries](#entity-queries)
|
|
7
|
+
- [EntityPropertyQuery: filter and sort](#entitypropertyquery-filter-and-sort)
|
|
8
|
+
- [Controls](#controls)
|
|
9
|
+
- [Snippets](#snippets)
|
|
10
|
+
- [Visual Intelligence](#visual-intelligence)
|
|
11
|
+
- [Assistant schemas (iOS 18+)](#assistant-schemas-ios-18)
|
|
12
|
+
- [Focus filters](#focus-filters)
|
|
13
|
+
- [Moving off SiriKit](#moving-off-sirikit)
|
|
14
|
+
- [Errors and dialogs](#errors-and-dialogs)
|
|
15
|
+
- [Confirmation](#confirmation)
|
|
16
|
+
- [Authentication](#authentication)
|
|
17
|
+
- [URL representations (iOS 18+)](#url-representations-ios-18)
|
|
18
|
+
- [Spotlight](#spotlight)
|
|
19
|
+
- [Visible content for Siri (iOS 26+)](#visible-content-for-siri-ios-26)
|
|
20
|
+
- [Conditional parameter summaries](#conditional-parameter-summaries)
|
|
21
|
+
- [Core Spotlight without App Intents](#core-spotlight-without-app-intents)
|
|
36
22
|
|
|
37
|
-
|
|
38
|
-
var name: String
|
|
23
|
+
## Parameter initializers
|
|
39
24
|
|
|
40
|
-
|
|
41
|
-
var enabled: Bool
|
|
25
|
+
### Plain values
|
|
42
26
|
|
|
43
|
-
|
|
44
|
-
var
|
|
27
|
+
```swift
|
|
28
|
+
@Parameter(title: "Note") var note: String
|
|
29
|
+
@Parameter(title: "Note", description: "Text saved with the entry") var describedNote: String
|
|
30
|
+
@Parameter(title: "Pin to Top", default: true) var pinned: Bool
|
|
31
|
+
@Parameter(title: "Link") var link: URL?
|
|
45
32
|
```
|
|
46
33
|
|
|
47
|
-
###
|
|
34
|
+
### Numbers with a range and control style
|
|
48
35
|
|
|
49
36
|
```swift
|
|
50
|
-
@Parameter(title: "
|
|
51
|
-
var
|
|
52
|
-
|
|
53
|
-
@Parameter(title: "Rating", controlStyle: .stepper, inclusiveRange: (1, 5))
|
|
54
|
-
var rating: Int
|
|
55
|
-
|
|
56
|
-
@Parameter(title: "Temperature", default: 72.0, inclusiveRange: (60.0, 90.0))
|
|
57
|
-
var temperature: Double
|
|
37
|
+
@Parameter(title: "Mix", controlStyle: .slider, inclusiveRange: (0.0, 100.0)) var mixPercent: Double
|
|
38
|
+
@Parameter(title: "Stars", controlStyle: .stepper, inclusiveRange: (1, 5)) var stars: Int
|
|
39
|
+
@Parameter(title: "Rounds", default: 3, inclusiveRange: (1, 10)) var rounds: Int
|
|
58
40
|
```
|
|
59
41
|
|
|
60
|
-
Numeric
|
|
61
|
-
`Double`
|
|
62
|
-
|
|
63
|
-
### 3. With Options Provider (Dynamic List)
|
|
42
|
+
Numeric parameters default to `.stepper`. `Int` offers `.stepper` and `.field`;
|
|
43
|
+
`Double` adds `.slider`.
|
|
64
44
|
|
|
65
|
-
|
|
45
|
+
### Options computed at run time
|
|
66
46
|
|
|
67
47
|
```swift
|
|
68
|
-
struct
|
|
48
|
+
struct PlaylistNames: DynamicOptionsProvider {
|
|
69
49
|
func results() async throws -> [String] {
|
|
70
|
-
await
|
|
50
|
+
await Library.shared.playlistTitles()
|
|
71
51
|
}
|
|
72
52
|
}
|
|
73
53
|
|
|
74
|
-
@Parameter(title: "
|
|
75
|
-
var category: String
|
|
54
|
+
@Parameter(title: "Playlist", optionsProvider: PlaylistNames()) var playlist: String
|
|
76
55
|
```
|
|
77
56
|
|
|
78
|
-
###
|
|
79
|
-
|
|
80
|
-
Request clarification when the system cannot resolve a value:
|
|
57
|
+
### Prompts for missing and ambiguous values
|
|
81
58
|
|
|
82
59
|
```swift
|
|
83
60
|
@Parameter(
|
|
84
|
-
title: "
|
|
85
|
-
requestValueDialog: "
|
|
86
|
-
requestDisambiguationDialog: "Which
|
|
61
|
+
title: "Contact",
|
|
62
|
+
requestValueDialog: "Who should get the invite?",
|
|
63
|
+
requestDisambiguationDialog: "Which of these did you mean?"
|
|
87
64
|
)
|
|
88
|
-
var
|
|
65
|
+
var contact: ContactEntity
|
|
89
66
|
```
|
|
90
67
|
|
|
91
|
-
###
|
|
68
|
+
### Resolvers
|
|
92
69
|
|
|
93
|
-
|
|
70
|
+
`resolvers:` takes a result-builder closure of resolver specifications that
|
|
71
|
+
turn raw input into the parameter's type:
|
|
94
72
|
|
|
95
73
|
```swift
|
|
96
|
-
@Parameter(title: "
|
|
97
|
-
var
|
|
74
|
+
@Parameter(title: "Guests", resolvers: { IntFromStringResolver() })
|
|
75
|
+
var guests: Int
|
|
98
76
|
```
|
|
99
77
|
|
|
100
|
-
|
|
78
|
+
Resolvers such as `IntFromStringResolver`, `IntFromDoubleResolver` or
|
|
79
|
+
`URLFromStringResolver` accept a different input type than the parameter
|
|
80
|
+
stores. The builder is a closure, not an array literal.
|
|
81
|
+
|
|
82
|
+
### Entity parameter with its own query
|
|
101
83
|
|
|
102
|
-
|
|
103
|
-
is not the right search behavior:
|
|
84
|
+
When `defaultQuery` searches the wrong way for this intent, supply another one:
|
|
104
85
|
|
|
105
86
|
```swift
|
|
106
|
-
@Parameter(title: "
|
|
107
|
-
var trail: TrailEntity
|
|
87
|
+
@Parameter(title: "Plant", query: ThirstyPlantQuery()) var plant: PlantEntity
|
|
108
88
|
```
|
|
109
89
|
|
|
110
|
-
###
|
|
90
|
+
### Arrays
|
|
111
91
|
|
|
112
92
|
```swift
|
|
113
|
-
@Parameter(title: "
|
|
114
|
-
var items: [ItemEntity]
|
|
93
|
+
@Parameter(title: "Plants") var plants: [PlantEntity]
|
|
115
94
|
```
|
|
116
95
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
when the action needs a fixed number of values.
|
|
96
|
+
The older `size:` initializers for entity arrays are deprecated. When an intent
|
|
97
|
+
needs an exact count, check it in `perform()`:
|
|
120
98
|
|
|
121
99
|
```swift
|
|
122
|
-
guard
|
|
123
|
-
throw $
|
|
100
|
+
guard plants.count == 2 else {
|
|
101
|
+
throw $plants.needsValueError("Pick exactly two plants to compare.")
|
|
124
102
|
}
|
|
125
103
|
```
|
|
126
104
|
|
|
127
|
-
###
|
|
105
|
+
### Files
|
|
128
106
|
|
|
129
107
|
```swift
|
|
130
|
-
@Parameter(title: "
|
|
131
|
-
var
|
|
132
|
-
|
|
133
|
-
@Parameter(title: "Image", supportedContentTypes: [.png, .jpeg])
|
|
134
|
-
var image: IntentFile?
|
|
108
|
+
@Parameter(title: "Manual", supportedContentTypes: [.plainText, .pdf]) var manual: IntentFile
|
|
109
|
+
@Parameter(title: "Cover", supportedContentTypes: [.png, .jpeg]) var cover: IntentFile?
|
|
135
110
|
```
|
|
136
111
|
|
|
137
|
-
###
|
|
112
|
+
### Measurements
|
|
138
113
|
|
|
139
114
|
```swift
|
|
140
|
-
@Parameter(
|
|
141
|
-
|
|
142
|
-
defaultUnit: .miles,
|
|
143
|
-
defaultUnitAdjustForLocale: true,
|
|
144
|
-
supportsNegativeNumbers: false
|
|
145
|
-
)
|
|
146
|
-
var distance: Measurement<UnitLength>
|
|
115
|
+
@Parameter(title: "Route", defaultUnit: .kilometers, defaultUnitAdjustForLocale: true, supportsNegativeNumbers: false)
|
|
116
|
+
var route: Measurement<UnitLength>
|
|
147
117
|
|
|
148
|
-
@Parameter(title: "
|
|
149
|
-
var
|
|
150
|
-
|
|
151
|
-
@Parameter(title: "Temperature", defaultUnit: .fahrenheit)
|
|
152
|
-
var temp: Measurement<UnitTemperature>
|
|
118
|
+
@Parameter(title: "Load", defaultUnit: .kilograms) var load: Measurement<UnitMass>
|
|
119
|
+
@Parameter(title: "Oven", defaultUnit: .fahrenheit) var oven: Measurement<UnitTemperature>
|
|
153
120
|
```
|
|
154
121
|
|
|
155
|
-
###
|
|
156
|
-
|
|
157
|
-
Control how parameters connect to Shortcuts input:
|
|
122
|
+
### Connecting to the previous action
|
|
158
123
|
|
|
159
124
|
```swift
|
|
160
|
-
@Parameter(title: "
|
|
161
|
-
var
|
|
125
|
+
@Parameter(title: "Input", inputConnectionBehavior: .connectToPreviousIntentResult)
|
|
126
|
+
var input: IntentFile
|
|
162
127
|
|
|
163
|
-
@Parameter(title: "
|
|
164
|
-
var
|
|
128
|
+
@Parameter(title: "Attachment", inputConnectionBehavior: .never)
|
|
129
|
+
var attachment: IntentFile?
|
|
165
130
|
```
|
|
166
131
|
|
|
167
|
-
|
|
132
|
+
`InputConnectionBehavior` has three cases: `.default`,
|
|
133
|
+
`.connectToPreviousIntentResult` (take the previous Shortcuts action's output)
|
|
134
|
+
and `.never` (do not wire it automatically).
|
|
168
135
|
|
|
169
|
-
|
|
136
|
+
### Asking while perform() runs
|
|
170
137
|
|
|
171
138
|
```swift
|
|
172
139
|
func perform() async throws -> some IntentResult {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
throw $quantity.needsValueError("How many would you like?")
|
|
140
|
+
guard let when = reminderDate else {
|
|
141
|
+
throw $reminderDate.needsValueError("When should I remind you?")
|
|
176
142
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
dialog: "Which size?"
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
// Confirm a value
|
|
185
|
-
try await $amount.requestConfirmation(for: amount, dialog: "Charge \(amount)?")
|
|
186
|
-
|
|
143
|
+
let chosen = try await $plant.requestDisambiguation(among: matches, dialog: "Which plant?")
|
|
144
|
+
let confirmed = try await $plant.requestConfirmation(for: chosen, dialog: "Use \(chosen.name)?")
|
|
145
|
+
guard confirmed else { return .result() }
|
|
146
|
+
await Scheduler.shared.remind(chosen.id, at: when)
|
|
187
147
|
return .result()
|
|
188
148
|
}
|
|
189
149
|
```
|
|
190
150
|
|
|
191
|
-
##
|
|
192
|
-
|
|
193
|
-
Full implementations of the query variants summarized in the main skill. Variant
|
|
194
|
-
1 (base `EntityQuery`) is shown in the skill; variants 2-4 are below.
|
|
151
|
+
## Entity queries
|
|
195
152
|
|
|
196
|
-
### EntityStringQuery
|
|
153
|
+
### EntityStringQuery
|
|
197
154
|
|
|
198
155
|
```swift
|
|
199
|
-
struct
|
|
200
|
-
func entities(matching
|
|
201
|
-
|
|
156
|
+
struct RecipeQuery: EntityStringQuery {
|
|
157
|
+
func entities(matching text: String) async throws -> [RecipeEntity] {
|
|
158
|
+
await Cookbook.shared.search(text).map(RecipeEntity.init(from:))
|
|
202
159
|
}
|
|
203
|
-
|
|
204
|
-
|
|
160
|
+
|
|
161
|
+
func entities(for ids: [RecipeEntity.ID]) async throws -> [RecipeEntity] {
|
|
162
|
+
await Cookbook.shared.recipes(withIDs: ids).map(RecipeEntity.init(from:))
|
|
205
163
|
}
|
|
206
164
|
}
|
|
207
165
|
```
|
|
208
166
|
|
|
209
|
-
### EnumerableEntityQuery
|
|
167
|
+
### EnumerableEntityQuery
|
|
210
168
|
|
|
211
169
|
```swift
|
|
212
|
-
struct
|
|
213
|
-
func allEntities() async throws -> [
|
|
214
|
-
|
|
170
|
+
struct KitchenTimerQuery: EnumerableEntityQuery {
|
|
171
|
+
func allEntities() async throws -> [KitchenTimerEntity] {
|
|
172
|
+
KitchenTimerEntity.presets
|
|
215
173
|
}
|
|
216
|
-
|
|
217
|
-
|
|
174
|
+
|
|
175
|
+
func entities(for ids: [KitchenTimerEntity.ID]) async throws -> [KitchenTimerEntity] {
|
|
176
|
+
KitchenTimerEntity.presets.filter { ids.contains($0.id) }
|
|
218
177
|
}
|
|
219
178
|
}
|
|
220
179
|
```
|
|
221
180
|
|
|
222
|
-
### UniqueAppEntityQuery (
|
|
223
|
-
|
|
224
|
-
Use for single-instance entities like app settings.
|
|
181
|
+
### UniqueAppEntityQuery (iOS 18+)
|
|
225
182
|
|
|
226
183
|
```swift
|
|
227
|
-
struct
|
|
228
|
-
static let defaultQuery =
|
|
229
|
-
static
|
|
230
|
-
var displayRepresentation: DisplayRepresentation { "App Settings" }
|
|
184
|
+
struct PreferencesEntity: UniqueAppEntity {
|
|
185
|
+
static let defaultQuery = PreferencesQuery()
|
|
186
|
+
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Preferences"
|
|
231
187
|
|
|
232
|
-
|
|
188
|
+
let id = "app-preferences"
|
|
189
|
+
var displayRepresentation: DisplayRepresentation { "Preferences" }
|
|
233
190
|
}
|
|
234
191
|
|
|
235
|
-
struct
|
|
236
|
-
func uniqueEntity() async throws ->
|
|
237
|
-
|
|
192
|
+
struct PreferencesQuery: UniqueAppEntityQuery {
|
|
193
|
+
func uniqueEntity() async throws -> PreferencesEntity {
|
|
194
|
+
PreferencesEntity()
|
|
238
195
|
}
|
|
239
196
|
}
|
|
240
197
|
```
|
|
241
198
|
|
|
242
|
-
## EntityPropertyQuery
|
|
199
|
+
## EntityPropertyQuery: filter and sort
|
|
243
200
|
|
|
244
|
-
The most
|
|
245
|
-
|
|
201
|
+
The most capable query. It declares which properties people can filter on and
|
|
202
|
+
which they can sort by, so Siri and Shortcuts can build structured queries
|
|
203
|
+
("books by this author, newest first").
|
|
246
204
|
|
|
247
205
|
```swift
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
struct TrailPropertyQuery: EntityPropertyQuery {
|
|
257
|
-
typealias ComparatorMappingType = TrailComparator
|
|
206
|
+
struct BookQuery: EntityPropertyQuery {
|
|
207
|
+
enum Match: Sendable {
|
|
208
|
+
case authorIs(String)
|
|
209
|
+
case titleHasPrefix(String)
|
|
210
|
+
case pagesOver(Int)
|
|
211
|
+
}
|
|
212
|
+
typealias ComparatorMappingType = Match
|
|
258
213
|
|
|
259
|
-
static var properties
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
214
|
+
static var properties: QueryProperties {
|
|
215
|
+
QueryProperties {
|
|
216
|
+
Property(\BookEntity.$author) {
|
|
217
|
+
EqualToComparator { Match.authorIs($0) }
|
|
218
|
+
}
|
|
219
|
+
Property(\BookEntity.$title) {
|
|
220
|
+
HasPrefixComparator { Match.titleHasPrefix($0) }
|
|
221
|
+
}
|
|
222
|
+
Property(\BookEntity.$pages) {
|
|
223
|
+
GreaterThanComparator { Match.pagesOver($0) }
|
|
224
|
+
}
|
|
268
225
|
}
|
|
269
226
|
}
|
|
270
227
|
|
|
271
|
-
static var sortingOptions
|
|
272
|
-
|
|
273
|
-
|
|
228
|
+
static var sortingOptions: SortingOptions {
|
|
229
|
+
SortingOptions {
|
|
230
|
+
SortableBy(\BookEntity.$title)
|
|
231
|
+
SortableBy(\BookEntity.$pages)
|
|
232
|
+
}
|
|
274
233
|
}
|
|
275
234
|
|
|
276
235
|
func entities(
|
|
277
|
-
matching comparators: [
|
|
236
|
+
matching comparators: [Match],
|
|
278
237
|
mode: ComparatorMode,
|
|
279
|
-
sortedBy: [EntityQuerySort<
|
|
238
|
+
sortedBy: [EntityQuerySort<BookEntity>],
|
|
280
239
|
limit: Int?
|
|
281
|
-
) async throws -> [
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
let
|
|
286
|
-
|
|
287
|
-
case .nameContains(let value):
|
|
288
|
-
trail.name.localizedCaseInsensitiveContains(value)
|
|
289
|
-
case .nameEquals(let value):
|
|
290
|
-
trail.name.localizedStandardCompare(value) == .orderedSame
|
|
291
|
-
case .lengthGreaterThan(let value):
|
|
292
|
-
trail.trailLength > value
|
|
293
|
-
case .lengthLessThan(let value):
|
|
294
|
-
trail.trailLength < value
|
|
295
|
-
case .lengthEquals(let value):
|
|
296
|
-
trail.trailLength == value
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
guard !matches.isEmpty else { return true }
|
|
300
|
-
return mode == .and ? matches.allSatisfy { $0 } : matches.contains(true)
|
|
240
|
+
) async throws -> [BookEntity] {
|
|
241
|
+
let shelf = await Shelf.shared.allBooks()
|
|
242
|
+
let hits = shelf.filter { book in
|
|
243
|
+
guard !comparators.isEmpty else { return true }
|
|
244
|
+
let results = comparators.map { passes(book, $0) }
|
|
245
|
+
return mode == .and ? results.allSatisfy { $0 } : results.contains(true)
|
|
301
246
|
}
|
|
247
|
+
return limit.map { Array(hits.prefix($0)) } ?? hits
|
|
248
|
+
}
|
|
302
249
|
|
|
303
|
-
|
|
304
|
-
|
|
250
|
+
private func passes(_ book: BookEntity, _ match: Match) -> Bool {
|
|
251
|
+
switch match {
|
|
252
|
+
case .authorIs(let name): book.author == name
|
|
253
|
+
case .titleHasPrefix(let start): book.title.hasPrefix(start)
|
|
254
|
+
case .pagesOver(let count): book.pages > count
|
|
305
255
|
}
|
|
306
|
-
|
|
307
|
-
return results
|
|
308
256
|
}
|
|
309
257
|
|
|
310
|
-
func entities(for
|
|
311
|
-
|
|
312
|
-
.filter { identifiers.contains($0.id) }
|
|
313
|
-
.map { TrailEntity(from: $0) }
|
|
258
|
+
func entities(for ids: [BookEntity.ID]) async throws -> [BookEntity] {
|
|
259
|
+
await Shelf.shared.books(withIDs: ids)
|
|
314
260
|
}
|
|
315
261
|
|
|
316
|
-
func suggestedEntities() async throws -> [
|
|
317
|
-
|
|
262
|
+
func suggestedEntities() async throws -> [BookEntity] {
|
|
263
|
+
await Shelf.shared.recentlyOpened()
|
|
318
264
|
}
|
|
319
265
|
}
|
|
320
266
|
```
|
|
321
267
|
|
|
322
|
-
|
|
323
|
-
`ComparatorMappingType
|
|
324
|
-
|
|
268
|
+
Each comparator closure converts the person's value into your
|
|
269
|
+
`ComparatorMappingType`. The matching method is therefore handed your own
|
|
270
|
+
`Match` cases rather than `EntityQueryComparator` instances. An empty list
|
|
271
|
+
means match everything; `.and` requires every comparator, otherwise any one is
|
|
272
|
+
enough. Apply `limit` last.
|
|
273
|
+
`properties` and `sortingOptions` are computed. Neither `QueryProperties` nor
|
|
274
|
+
`SortingOptions` is `Sendable`, so Swift 6 rejects them as `static let`.
|
|
325
275
|
|
|
326
|
-
|
|
276
|
+
Comparators and what they apply to:
|
|
327
277
|
|
|
328
|
-
| Comparator |
|
|
278
|
+
| Comparator | Property kind |
|
|
329
279
|
|---|---|
|
|
330
|
-
| `EqualToComparator` | Equatable
|
|
331
|
-
| `
|
|
332
|
-
| `
|
|
333
|
-
| `
|
|
334
|
-
| `
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
## Control Center Widget Implementation
|
|
342
|
-
|
|
343
|
-
Full `ControlConfigurationIntent` + `ControlWidget` wiring. The configuration
|
|
344
|
-
intent is a parameter contract; state changes run from a separate action intent
|
|
345
|
-
with an appropriate `authenticationPolicy` and `requestConfirmation`.
|
|
280
|
+
| `EqualToComparator`, `NotEqualToComparator` | `Equatable` |
|
|
281
|
+
| `ContainsComparator` | sequences |
|
|
282
|
+
| `HasPrefixComparator`, `HasSuffixComparator` | `String` |
|
|
283
|
+
| `GreaterThanComparator`, `LessThanComparator`, `GreaterThanOrEqualToComparator`, `LessThanOrEqualToComparator` | `Comparable` |
|
|
284
|
+
| `IsBetweenComparator` | `Comparable` types that Shortcuts supports |
|
|
285
|
+
|
|
286
|
+
## Controls
|
|
287
|
+
|
|
288
|
+
The configuration intent is only a parameter contract. The change of state
|
|
289
|
+
happens in a separate action intent with a suitable `authenticationPolicy` and
|
|
290
|
+
a confirmation step.
|
|
346
291
|
|
|
347
292
|
```swift
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
293
|
+
import WidgetKit
|
|
294
|
+
import SwiftUI
|
|
295
|
+
|
|
296
|
+
enum GreenhouseZone: String, AppEnum {
|
|
297
|
+
case nursery, tropical, succulents
|
|
298
|
+
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Zone"
|
|
299
|
+
static let caseDisplayRepresentations: [GreenhouseZone: DisplayRepresentation] = [
|
|
300
|
+
.nursery: "Nursery", .tropical: "Tropical", .succulents: "Succulents",
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
struct MisterControlConfig: ControlConfigurationIntent {
|
|
305
|
+
static let title: LocalizedStringResource = "Mister"
|
|
306
|
+
|
|
307
|
+
@Parameter(title: "Zone", default: .nursery)
|
|
308
|
+
var zone: GreenhouseZone
|
|
351
309
|
}
|
|
352
310
|
|
|
353
|
-
struct
|
|
354
|
-
static
|
|
355
|
-
static
|
|
311
|
+
struct SetMisterIntent: SetValueIntent {
|
|
312
|
+
static let title: LocalizedStringResource = "Set Mister"
|
|
313
|
+
static let authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
|
|
356
314
|
|
|
357
|
-
@Parameter(title: "
|
|
315
|
+
@Parameter(title: "Zone") var zone: GreenhouseZone
|
|
316
|
+
@Parameter(title: "Running") var value: Bool
|
|
317
|
+
|
|
318
|
+
init() {}
|
|
319
|
+
init(zone: GreenhouseZone) { self.zone = zone }
|
|
358
320
|
|
|
359
321
|
func perform() async throws -> some IntentResult {
|
|
360
|
-
try await requestConfirmation(
|
|
361
|
-
|
|
362
|
-
dialog: "Toggle \(light.name)?"
|
|
363
|
-
)
|
|
364
|
-
try await LightService.shared.toggle(light.id)
|
|
322
|
+
try await requestConfirmation(actionName: .set, dialog: "Change the mister in \(zone)?")
|
|
323
|
+
await Irrigation.shared.setMister(value, in: zone)
|
|
365
324
|
return .result()
|
|
366
325
|
}
|
|
367
326
|
}
|
|
368
327
|
|
|
369
|
-
struct
|
|
328
|
+
struct MisterControl: ControlWidget {
|
|
370
329
|
var body: some ControlWidgetConfiguration {
|
|
371
|
-
AppIntentControlConfiguration(kind: "
|
|
372
|
-
ControlWidgetToggle(
|
|
330
|
+
AppIntentControlConfiguration(kind: "MisterControl", intent: MisterControlConfig.self) { config in
|
|
331
|
+
ControlWidgetToggle(
|
|
332
|
+
"Mister",
|
|
333
|
+
isOn: Irrigation.shared.isMisting(config.zone),
|
|
334
|
+
action: SetMisterIntent(zone: config.zone)
|
|
335
|
+
) { isOn in
|
|
336
|
+
Label(isOn ? "On" : "Off", systemImage: "humidifier.and.droplets")
|
|
337
|
+
}
|
|
373
338
|
}
|
|
374
339
|
}
|
|
375
340
|
}
|
|
376
341
|
```
|
|
377
342
|
|
|
378
|
-
|
|
343
|
+
Any configuration parameter without a default must be optional. The default
|
|
344
|
+
here is an `AppEnum` case because defaults must be compile-time values.
|
|
379
345
|
|
|
380
|
-
##
|
|
346
|
+
## Snippets
|
|
381
347
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
`SnippetIntent.perform()` side-effect-free and do mutations in the calling
|
|
385
|
-
action intent or a separate button/toggle action.
|
|
348
|
+
`SnippetIntent.perform()` can run several times, for example after each tap
|
|
349
|
+
inside the snippet. Keep it free of side effects and mutate elsewhere.
|
|
386
350
|
|
|
387
351
|
```swift
|
|
388
|
-
struct
|
|
389
|
-
static
|
|
352
|
+
struct TimerSnippet: SnippetIntent {
|
|
353
|
+
static let title: LocalizedStringResource = "Timer Status"
|
|
354
|
+
|
|
355
|
+
@Parameter(title: "Timer") var timer: KitchenTimerEntity
|
|
356
|
+
|
|
357
|
+
init() {}
|
|
358
|
+
init(timer: KitchenTimerEntity) { self.timer = timer }
|
|
359
|
+
|
|
390
360
|
func perform() async throws -> some IntentResult & ShowsSnippetView {
|
|
391
|
-
let
|
|
392
|
-
return .result(view:
|
|
361
|
+
let remaining = await Timers.shared.remaining(for: timer.id)
|
|
362
|
+
return .result(view: TimerSnippetView(name: timer.name, remaining: remaining))
|
|
393
363
|
}
|
|
394
364
|
}
|
|
395
365
|
|
|
396
|
-
struct
|
|
397
|
-
static
|
|
366
|
+
struct StartTimerIntent: AppIntent {
|
|
367
|
+
static let title: LocalizedStringResource = "Start Timer"
|
|
368
|
+
|
|
369
|
+
@Parameter(title: "Timer") var timer: KitchenTimerEntity
|
|
370
|
+
|
|
398
371
|
func perform() async throws -> some IntentResult & ShowsSnippetIntent {
|
|
399
|
-
.
|
|
372
|
+
await Timers.shared.start(timer.id)
|
|
373
|
+
return .result(snippetIntent: TimerSnippet(timer: timer))
|
|
400
374
|
}
|
|
401
375
|
}
|
|
402
376
|
```
|
|
403
377
|
|
|
404
|
-
|
|
405
|
-
`isDiscoverable`
|
|
378
|
+
`TimerSnippet` is not listed in Shortcuts or Spotlight unless it sets
|
|
379
|
+
`isDiscoverable` to `true`. Declaring `init(timer:)` means `init()` must be
|
|
380
|
+
written out too, since the system still creates intents through it.
|
|
406
381
|
|
|
407
|
-
## Visual Intelligence
|
|
382
|
+
## Visual Intelligence
|
|
408
383
|
|
|
409
|
-
|
|
410
|
-
|
|
384
|
+
`SemanticContentDescriptor` comes from the VisualIntelligence framework. Only
|
|
385
|
+
one `IntentValueQuery` can take it; return several entity types through a
|
|
386
|
+
`@UnionValue` enum.
|
|
411
387
|
|
|
412
388
|
```swift
|
|
389
|
+
import AppIntents
|
|
390
|
+
import VisualIntelligence
|
|
391
|
+
|
|
413
392
|
@available(iOS 26, *)
|
|
414
393
|
@UnionValue
|
|
415
|
-
enum
|
|
416
|
-
case
|
|
417
|
-
case
|
|
394
|
+
enum ShelfMatch {
|
|
395
|
+
case book(BookEntity)
|
|
396
|
+
case author(AuthorEntity)
|
|
418
397
|
}
|
|
419
398
|
|
|
420
399
|
@available(iOS 26, *)
|
|
421
|
-
struct
|
|
422
|
-
func values(for
|
|
400
|
+
struct ShelfLookup: IntentValueQuery {
|
|
401
|
+
func values(for descriptor: SemanticContentDescriptor) async throws -> [ShelfMatch] {
|
|
423
402
|
try Task.checkCancellation()
|
|
424
|
-
async let
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
limit: 5
|
|
428
|
-
)
|
|
429
|
-
async let storeMatches = StoreStore.shared.matches(
|
|
430
|
-
labels: input.labels,
|
|
431
|
-
pixelBuffer: input.pixelBuffer,
|
|
432
|
-
limit: 3
|
|
433
|
-
)
|
|
434
|
-
let ranked = await rank(productMatches, storeMatches)
|
|
403
|
+
async let books = Catalog.shared.books(forLabels: descriptor.labels, frame: descriptor.pixelBuffer, limit: 5)
|
|
404
|
+
async let authors = Catalog.shared.authors(forLabels: descriptor.labels, limit: 3)
|
|
405
|
+
let ranked = try await Ranker.merge(books: books, authors: authors)
|
|
435
406
|
return Array(ranked.prefix(8))
|
|
436
407
|
}
|
|
437
408
|
}
|
|
438
409
|
```
|
|
439
410
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
## Assistant Schemas (iOS 18+)
|
|
411
|
+
- `labels` are broad English descriptions of what was seen. They are not a
|
|
412
|
+
full synonym list and not your app's taxonomy; combine them with
|
|
413
|
+
`pixelBuffer` when it is present.
|
|
414
|
+
- Give every result a route to details and more results: an `OpenIntent`, a
|
|
415
|
+
URL representation, or a handoff to in-app search.
|
|
416
|
+
- Keep heavy work out of the query: no capturing from the camera, no `VN*`
|
|
417
|
+
Vision requests, no barcode classifying, no Spotlight indexing. Call existing search or image-matching services
|
|
418
|
+
with explicit result caps, plus timeouts when the work can outlast the
|
|
419
|
+
system UI's time budget.
|
|
450
420
|
|
|
451
|
-
Assistant schemas
|
|
452
|
-
understands natively. Annotate conforming types with schema macros.
|
|
421
|
+
## Assistant schemas (iOS 18+)
|
|
453
422
|
|
|
454
|
-
|
|
423
|
+
Schemas are domain-specific shapes that Apple Intelligence understands
|
|
424
|
+
directly. Annotate the matching types:
|
|
455
425
|
|
|
456
426
|
```swift
|
|
457
|
-
// Preferred macro (iOS 18+)
|
|
458
|
-
@AppIntent(schema: .photos.openAsset)
|
|
459
|
-
struct OpenPhotoIntent: AppIntent { ... }
|
|
460
|
-
|
|
461
|
-
// CORRECT: Using preferred macro
|
|
462
427
|
@AppIntent(schema: .photos.openAsset)
|
|
463
|
-
struct
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
@Parameter(title: "Asset")
|
|
467
|
-
var target: PhotoEntity
|
|
428
|
+
struct OpenSnapshotIntent: OpenIntent {
|
|
429
|
+
var target: SnapshotEntity
|
|
468
430
|
|
|
469
431
|
func perform() async throws -> some IntentResult {
|
|
470
|
-
|
|
432
|
+
await Navigator.shared.show(snapshot: target.id)
|
|
471
433
|
return .result()
|
|
472
434
|
}
|
|
473
435
|
}
|
|
474
436
|
|
|
475
437
|
@AppEntity(schema: .photos.asset)
|
|
476
|
-
struct
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
var
|
|
481
|
-
|
|
482
|
-
}
|
|
483
|
-
var name: String
|
|
438
|
+
struct SnapshotEntity: IndexedEntity {
|
|
439
|
+
static let defaultQuery = SnapshotQuery()
|
|
440
|
+
let id: String
|
|
441
|
+
var title: String?
|
|
442
|
+
var assetType: SnapshotKind?
|
|
443
|
+
// plus the remaining properties the .photos.asset schema requires
|
|
444
|
+
var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(title ?? "Snapshot")") }
|
|
484
445
|
}
|
|
485
446
|
|
|
486
447
|
@AppEnum(schema: .photos.assetType)
|
|
487
|
-
enum
|
|
488
|
-
case photo, video
|
|
489
|
-
static
|
|
490
|
-
|
|
491
|
-
.photo: "Photo",
|
|
492
|
-
.video: "Video",
|
|
493
|
-
.livePhoto: "Live Photo"
|
|
448
|
+
enum SnapshotKind: String, AppEnum {
|
|
449
|
+
case photo, video
|
|
450
|
+
static let caseDisplayRepresentations: [SnapshotKind: DisplayRepresentation] = [
|
|
451
|
+
.photo: "Photo", .video: "Video",
|
|
494
452
|
]
|
|
495
453
|
}
|
|
496
454
|
```
|
|
497
455
|
|
|
498
|
-
|
|
499
|
-
`
|
|
456
|
+
The schema fixes which properties a type must declare; Xcode's diagnostics
|
|
457
|
+
list any that are missing. Do not use the deprecated `AssistantIntent(schema:)`,
|
|
458
|
+
`AssistantEntity(schema:)` or `AssistantEnum(schema:)` in new code.
|
|
500
459
|
|
|
501
|
-
###
|
|
460
|
+
### Domains
|
|
502
461
|
|
|
503
|
-
|
|
504
|
-
|
|
462
|
+
Exact case names change between releases, so rely on Xcode completion and the
|
|
463
|
+
current domain documentation. Broadly:
|
|
505
464
|
|
|
506
|
-
| Domain |
|
|
465
|
+
| Domain | Content types | Actions |
|
|
507
466
|
|---|---|---|
|
|
508
|
-
| Assistant |
|
|
509
|
-
| Books |
|
|
510
|
-
| Browser |
|
|
511
|
-
| Camera |
|
|
512
|
-
| File management |
|
|
513
|
-
| Journaling |
|
|
514
|
-
| Mail |
|
|
515
|
-
| Photos |
|
|
516
|
-
| Presentations |
|
|
517
|
-
| Reader |
|
|
518
|
-
| Spreadsheet |
|
|
519
|
-
|
|
|
520
|
-
| Visual intelligence |
|
|
521
|
-
| Whiteboard |
|
|
522
|
-
| Word processor |
|
|
523
|
-
|
|
524
|
-
###
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
467
|
+
| Assistant | | launching the app for a conversation via the side button |
|
|
468
|
+
| Books | audiobooks, books | bookmarking, opening a title |
|
|
469
|
+
| Browser | windows, tabs, bookmarks | web search, bookmarking, opening a tab |
|
|
470
|
+
| Camera | | taking a photo or a video |
|
|
471
|
+
| File management | files | opening, creating, moving, renaming or deleting |
|
|
472
|
+
| Journaling | journal entries | creating, updating, deleting or searching entries |
|
|
473
|
+
| Mail | messages, mailboxes, drafts, accounts | sending a draft, opening a mailbox |
|
|
474
|
+
| Photos | people, assets, albums | searching assets, making an album, opening an asset |
|
|
475
|
+
| Presentations | templates, slides, documents | adding a slide, opening a document |
|
|
476
|
+
| Reader | pages, documents | jumping to a page, opening a document |
|
|
477
|
+
| Spreadsheet | templates, sheets, documents | adding a sheet, opening a document |
|
|
478
|
+
| Search (system and in-app) | | searching |
|
|
479
|
+
| Visual intelligence | | searching by semantic content |
|
|
480
|
+
| Whiteboard | items, boards | adding an item, opening a board |
|
|
481
|
+
| Word processor | templates, pages, documents | adding a page, opening a document |
|
|
482
|
+
|
|
483
|
+
### Beyond Apple Intelligence
|
|
484
|
+
|
|
485
|
+
A schema-conforming type is assistant-only by default. Opt out to keep it in
|
|
486
|
+
Shortcuts and elsewhere:
|
|
528
487
|
|
|
529
488
|
```swift
|
|
530
|
-
|
|
531
|
-
struct OpenPhotoIntent: AppIntent {
|
|
532
|
-
static let isAssistantOnly = false // Also available in Shortcuts
|
|
533
|
-
// ...
|
|
534
|
-
}
|
|
489
|
+
static let isAssistantOnly = false
|
|
535
490
|
```
|
|
536
491
|
|
|
537
|
-
## Focus
|
|
492
|
+
## Focus filters
|
|
538
493
|
|
|
539
|
-
|
|
494
|
+
`SetFocusFilterIntent` lets the app change its behaviour while a Focus is on.
|
|
540
495
|
|
|
541
496
|
```swift
|
|
542
|
-
struct
|
|
543
|
-
static
|
|
544
|
-
static
|
|
545
|
-
|
|
546
|
-
@Parameter(title: "Show Only Work Projects", default: true)
|
|
547
|
-
var workOnly: Bool
|
|
497
|
+
struct QuietGardenFilter: SetFocusFilterIntent {
|
|
498
|
+
static let title: LocalizedStringResource = "Garden Alerts"
|
|
499
|
+
static let description: IntentDescription? = "Choose which garden alerts reach you during this Focus."
|
|
548
500
|
|
|
549
|
-
@Parameter(title: "
|
|
550
|
-
var
|
|
501
|
+
@Parameter(title: "Watering Reminders", default: true) var watering: Bool
|
|
502
|
+
@Parameter(title: "Frost Warnings", default: true) var frost: Bool
|
|
551
503
|
|
|
552
504
|
var displayRepresentation: DisplayRepresentation {
|
|
553
|
-
"
|
|
505
|
+
DisplayRepresentation(title: "Watering \(watering ? "on" : "off"), frost \(frost ? "on" : "off")")
|
|
554
506
|
}
|
|
555
507
|
|
|
556
508
|
func perform() async throws -> some IntentResult {
|
|
557
|
-
|
|
558
|
-
AppSettings.shared.notificationsMuted = muteNotifications
|
|
509
|
+
AlertPreferences.shared.apply(watering: watering, frost: frost)
|
|
559
510
|
return .result()
|
|
560
511
|
}
|
|
561
|
-
}
|
|
562
|
-
```
|
|
563
|
-
|
|
564
|
-
### Access current focus filter
|
|
565
|
-
|
|
566
|
-
```swift
|
|
567
|
-
let currentFilter = try? SetFocusFilterIntent.current
|
|
568
|
-
if let workFilter = currentFilter as? WorkFocusFilter {
|
|
569
|
-
// Apply work-mode behavior
|
|
570
|
-
}
|
|
571
|
-
```
|
|
572
512
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
static func suggestedFocusFilters(
|
|
578
|
-
for context: FocusFilterSuggestionContext
|
|
579
|
-
) async -> [WorkFocusFilter] {
|
|
580
|
-
[WorkFocusFilter(workOnly: true, muteNotifications: true)]
|
|
513
|
+
static func suggestedFocusFilters(for situation: FocusFilterSuggestionContext) async -> [QuietGardenFilter] {
|
|
514
|
+
let sleep = QuietGardenFilter()
|
|
515
|
+
sleep.watering = false
|
|
516
|
+
return [sleep]
|
|
581
517
|
}
|
|
582
518
|
}
|
|
519
|
+
|
|
520
|
+
// Reading the active filter elsewhere:
|
|
521
|
+
// let active = try? await QuietGardenFilter.current
|
|
583
522
|
```
|
|
584
523
|
|
|
585
|
-
##
|
|
524
|
+
## Moving off SiriKit
|
|
586
525
|
|
|
587
|
-
|
|
588
|
-
|
|
526
|
+
`CustomIntentMigratedAppIntent` replaces a custom intent from an
|
|
527
|
+
`.intentdefinition` file while keeping the shortcuts and donations people
|
|
528
|
+
already have.
|
|
589
529
|
|
|
590
530
|
```swift
|
|
591
|
-
struct
|
|
592
|
-
|
|
593
|
-
static
|
|
594
|
-
|
|
595
|
-
static var title: LocalizedStringResource = "Order Soup"
|
|
531
|
+
struct ReorderSeedsIntent: AppIntent, CustomIntentMigratedAppIntent {
|
|
532
|
+
static let intentClassName = "ReorderSeedsIntent"
|
|
533
|
+
static let title: LocalizedStringResource = "Reorder Seeds"
|
|
596
534
|
|
|
597
|
-
@Parameter(title: "
|
|
598
|
-
var soup: SoupEntity
|
|
599
|
-
|
|
600
|
-
@Parameter(title: "Quantity", default: 1)
|
|
601
|
-
var quantity: Int
|
|
535
|
+
@Parameter(title: "Variety") var variety: String
|
|
602
536
|
|
|
603
537
|
func perform() async throws -> some IntentResult {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
quantity: quantity
|
|
607
|
-
)
|
|
608
|
-
return .result(dialog: "Ordered \(quantity) bowls.")
|
|
538
|
+
try await SeedShop.shared.reorder(variety)
|
|
539
|
+
return .result()
|
|
609
540
|
}
|
|
610
541
|
}
|
|
611
542
|
```
|
|
612
543
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
6.
|
|
544
|
+
1. Create an `AppIntent` struct that adopts `CustomIntentMigratedAppIntent`.
|
|
545
|
+
2. Set `intentClassName` to exactly the class name the old SiriKit intent
|
|
546
|
+
generated.
|
|
547
|
+
3. Declare the old properties again as `@Parameter` values.
|
|
548
|
+
4. Write `perform()` with async/await.
|
|
549
|
+
5. Existing shortcuts and donations resolve to the new type through that class
|
|
550
|
+
name.
|
|
551
|
+
6. Delete the `.intentdefinition` file only once the migration is verified.
|
|
621
552
|
|
|
622
|
-
###
|
|
623
|
-
|
|
624
|
-
Replace an old `AppIntent` with a newer version:
|
|
553
|
+
### Retiring an AppIntent
|
|
625
554
|
|
|
626
555
|
```swift
|
|
627
|
-
struct
|
|
628
|
-
typealias ReplacementIntent =
|
|
629
|
-
static
|
|
630
|
-
|
|
556
|
+
struct LegacyWaterIntent: DeprecatedAppIntent {
|
|
557
|
+
typealias ReplacementIntent = WaterPlantIntent
|
|
558
|
+
static let title: LocalizedStringResource = "Water (Old)"
|
|
559
|
+
static var deprecation: IntentDeprecation<WaterPlantIntent> {
|
|
560
|
+
IntentDeprecation(message: "Use Water Plant instead.", replacedBy: WaterPlantIntent.self)
|
|
631
561
|
}
|
|
632
|
-
|
|
562
|
+
|
|
633
563
|
func perform() async throws -> some IntentResult { .result() }
|
|
634
564
|
}
|
|
635
565
|
```
|
|
636
566
|
|
|
637
|
-
|
|
567
|
+
`IntentDeprecation` also has `init(message:)` for an intent with no
|
|
568
|
+
replacement (its `ReplacementIntent` is then `Never`).
|
|
569
|
+
`deprecation` is computed because `IntentDeprecation` is not `Sendable`; a
|
|
570
|
+
`static let` of it is a Swift 6 error.
|
|
571
|
+
|
|
572
|
+
## Errors and dialogs
|
|
638
573
|
|
|
639
|
-
### Standard
|
|
574
|
+
### Standard errors (iOS 18+)
|
|
640
575
|
|
|
641
576
|
```swift
|
|
642
577
|
func perform() async throws -> some IntentResult {
|
|
643
|
-
guard
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
guard let item = try await fetchItem() else {
|
|
648
|
-
throw AppIntentError.Unrecoverable.entityNotFound
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
guard !requiresManualSetup else {
|
|
652
|
-
throw AppIntentError.UserActionRequired.accountSetup
|
|
653
|
-
}
|
|
654
|
-
|
|
578
|
+
guard PhotoAccess.isGranted else { throw AppIntentError.PermissionRequired.photos }
|
|
579
|
+
guard let album = await Albums.shared.find(albumID) else { throw AppIntentError.Unrecoverable.entityNotFound }
|
|
580
|
+
guard Account.shared.isReady else { throw AppIntentError.UserActionRequired.accountSetup }
|
|
581
|
+
await Albums.shared.pin(album)
|
|
655
582
|
return .result()
|
|
656
583
|
}
|
|
657
584
|
```
|
|
658
585
|
|
|
659
|
-
|
|
|
586
|
+
| Family | Meaning |
|
|
660
587
|
|---|---|
|
|
661
|
-
| `AppIntentError.PermissionRequired` |
|
|
662
|
-
| `AppIntentError.Unrecoverable` |
|
|
663
|
-
| `AppIntentError.UserActionRequired` |
|
|
588
|
+
| `AppIntentError.PermissionRequired` | an OS permission is missing |
|
|
589
|
+
| `AppIntentError.Unrecoverable` | a fatal state with no immediate fix |
|
|
590
|
+
| `AppIntentError.UserActionRequired` | something is needed from the person first: signing in, confirming, creating an account |
|
|
664
591
|
|
|
665
|
-
### Parameter
|
|
592
|
+
### Parameter errors
|
|
666
593
|
|
|
667
594
|
```swift
|
|
668
|
-
|
|
669
|
-
throw $
|
|
670
|
-
|
|
671
|
-
// Force disambiguation
|
|
672
|
-
throw $size.needsDisambiguation(among: [.small, .medium, .large])
|
|
595
|
+
throw $album.needsValueError("Which album?")
|
|
596
|
+
throw $album.needsDisambiguationError(among: candidates, dialog: "Several albums match. Pick one.")
|
|
673
597
|
```
|
|
674
598
|
|
|
675
|
-
###
|
|
599
|
+
### Continuing in the app (iOS 26+)
|
|
676
600
|
|
|
677
601
|
```swift
|
|
678
|
-
|
|
679
|
-
if needsUserInteraction {
|
|
680
|
-
try await continueInForeground("Open the app to finish.")
|
|
681
|
-
}
|
|
682
|
-
// ...
|
|
683
|
-
return .result()
|
|
684
|
-
}
|
|
602
|
+
try await continueInForeground("Finish pairing the sensor in the app.")
|
|
685
603
|
```
|
|
686
604
|
|
|
687
|
-
### Dialog in
|
|
605
|
+
### Dialog in the result
|
|
688
606
|
|
|
689
607
|
```swift
|
|
690
608
|
func perform() async throws -> some IntentResult & ProvidesDialog {
|
|
691
|
-
|
|
609
|
+
.result(dialog: "Done.")
|
|
692
610
|
}
|
|
693
611
|
|
|
694
|
-
func perform() async throws -> some IntentResult & ProvidesDialog & ReturnsValue<
|
|
695
|
-
|
|
696
|
-
return .result(
|
|
697
|
-
value: OrderEntity(from: order),
|
|
698
|
-
dialog: "Order #\(order.number) is confirmed."
|
|
699
|
-
)
|
|
612
|
+
func perform() async throws -> some IntentResult & ProvidesDialog & ReturnsValue<PlantEntity> {
|
|
613
|
+
.result(value: plant, dialog: "Found \(plant.name).")
|
|
700
614
|
}
|
|
701
615
|
```
|
|
702
616
|
|
|
703
|
-
## Confirmation
|
|
704
|
-
|
|
705
|
-
### Basic confirmation
|
|
617
|
+
## Confirmation
|
|
706
618
|
|
|
707
619
|
```swift
|
|
708
|
-
|
|
709
|
-
try await requestConfirmation(
|
|
710
|
-
actionName: .send,
|
|
711
|
-
dialog: "Send \(quantity) messages?"
|
|
712
|
-
)
|
|
713
|
-
// User confirmed -- proceed
|
|
714
|
-
return .result()
|
|
715
|
-
}
|
|
716
|
-
```
|
|
620
|
+
try await requestConfirmation(actionName: .pay, dialog: "Pay this invoice?")
|
|
717
621
|
|
|
718
|
-
|
|
622
|
+
try await requestConfirmation(conditions: .lowConfidenceSource, actionName: .book, dialog: "Reserve the table?")
|
|
719
623
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
try await requestConfirmation(
|
|
723
|
-
conditions: .always,
|
|
724
|
-
actionName: .order,
|
|
725
|
-
dialog: "Place order for \(quantity) \(soup.name)?"
|
|
726
|
-
)
|
|
727
|
-
return .result()
|
|
624
|
+
try await requestConfirmation(actionName: .order, dialog: "Order these seeds?") {
|
|
625
|
+
CartPreview(items: cart)
|
|
728
626
|
}
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
### Confirmation with SwiftUI content
|
|
732
627
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
try await requestConfirmation(
|
|
736
|
-
actionName: .buy,
|
|
737
|
-
dialog: "Purchase \(item.name) for \(item.price)?",
|
|
738
|
-
view: OrderPreviewView(item: item)
|
|
739
|
-
)
|
|
740
|
-
return .result()
|
|
741
|
-
}
|
|
628
|
+
let pick = try await requestChoice(between: [.init(title: "Morning"), .init(title: "Evening")],
|
|
629
|
+
dialog: "When should I water?")
|
|
742
630
|
```
|
|
743
631
|
|
|
744
|
-
|
|
632
|
+
- Execution continues past `requestConfirmation` only once the person agrees.
|
|
633
|
+
- `ConfirmationConditions` currently offers `.lowConfidenceSource`, which asks
|
|
634
|
+
only when the request came from a low-confidence source. The default empty
|
|
635
|
+
set always asks.
|
|
636
|
+
- The trailing-closure form (iOS 18, SwiftUI overlay) shows a preview view with
|
|
637
|
+
the prompt. On iOS 26 a `snippetIntent:` variant shows an interactive
|
|
638
|
+
snippet instead.
|
|
639
|
+
- `requestChoice(between:dialog:)` (iOS 26) returns the option the person picked.
|
|
745
640
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
// Use chosen value
|
|
753
|
-
return .result()
|
|
754
|
-
}
|
|
755
|
-
```
|
|
641
|
+
Built-in action names: `.add`, `.addData`, `.book`, `.buy`, `.call`, `.checkIn`,
|
|
642
|
+
`.continue`, `.create`, `.do`, `.download`, `.filter`, `.find`, `.get`, `.go`,
|
|
643
|
+
`.log`, `.open`, `.order`, `.pay`, `.play`, `.playSound`, `.post`, `.request`,
|
|
644
|
+
`.run`, `.search`, `.send`, `.set`, `.share`, `.start`, `.startNavigation`,
|
|
645
|
+
`.toggle`, `.turnOff`, `.turnOn`, `.view`. For anything else use
|
|
646
|
+
`.custom(acceptLabel:acceptAlternatives:denyLabel:denyAlternatives:destructive:)`.
|
|
756
647
|
|
|
757
|
-
|
|
648
|
+
## Authentication
|
|
758
649
|
|
|
759
|
-
|
|
760
|
-
`.toggle`, `.turnOn`, `.turnOff`, `.open`, `.play`, `.post`, `.search`,
|
|
761
|
-
`.book`, `.download`, `.pay`, `.order`, `.run`, `.get`, `.go`, `.log`,
|
|
762
|
-
`.set`, `.view`, `.find`, `.filter`, `.continue`, `.do`, `.addData`,
|
|
763
|
-
`.checkIn`, `.request`, `.playSound`, `.startNavigation`.
|
|
650
|
+
Set a static `authenticationPolicy: IntentAuthenticationPolicy`:
|
|
764
651
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
acceptAlternatives: ["Yes", "Buy it"],
|
|
771
|
-
denyLabel: "Cancel",
|
|
772
|
-
denyAlternatives: ["No", "Never mind"],
|
|
773
|
-
destructive: false
|
|
774
|
-
)
|
|
775
|
-
```
|
|
776
|
-
|
|
777
|
-
## Authentication Policies
|
|
652
|
+
| Policy | Effect |
|
|
653
|
+
|---|---|
|
|
654
|
+
| `.alwaysAllowed` | runs without authentication |
|
|
655
|
+
| `.requiresAuthentication` | the device must be unlocked before `perform()` |
|
|
656
|
+
| `.requiresLocalDeviceAuthentication` | Face ID or Touch ID is required |
|
|
778
657
|
|
|
779
|
-
|
|
658
|
+
An intent that deletes an account but declares no policy can run from a locked
|
|
659
|
+
device. Give it `.requiresLocalDeviceAuthentication`:
|
|
780
660
|
|
|
781
661
|
```swift
|
|
782
|
-
struct
|
|
783
|
-
static
|
|
784
|
-
static
|
|
662
|
+
struct CloseAccountIntent: AppIntent {
|
|
663
|
+
static let title: LocalizedStringResource = "Close Account"
|
|
664
|
+
static let authenticationPolicy: IntentAuthenticationPolicy = .requiresLocalDeviceAuthentication
|
|
785
665
|
|
|
786
666
|
func perform() async throws -> some IntentResult {
|
|
787
|
-
|
|
667
|
+
try await Account.shared.close()
|
|
788
668
|
return .result()
|
|
789
669
|
}
|
|
790
670
|
}
|
|
791
671
|
```
|
|
792
672
|
|
|
793
|
-
|
|
794
|
-
|---|---|
|
|
795
|
-
| `.alwaysAllowed` | No authentication required |
|
|
796
|
-
| `.requiresAuthentication` | Device must be unlocked |
|
|
797
|
-
| `.requiresLocalDeviceAuthentication` | Face ID / Touch ID required |
|
|
798
|
-
|
|
799
|
-
```swift
|
|
800
|
-
// WRONG: Sensitive action without authentication
|
|
801
|
-
struct DeleteAccountIntent: AppIntent {
|
|
802
|
-
// Missing authenticationPolicy -- runs on locked device
|
|
803
|
-
func perform() async throws -> some IntentResult { ... }
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// CORRECT: Require authentication for sensitive actions
|
|
807
|
-
struct DeleteAccountIntent: AppIntent {
|
|
808
|
-
static var authenticationPolicy: IntentAuthenticationPolicy = .requiresLocalDeviceAuthentication
|
|
809
|
-
static var title: LocalizedStringResource = "Delete Account"
|
|
810
|
-
func perform() async throws -> some IntentResult { ... }
|
|
811
|
-
}
|
|
812
|
-
```
|
|
813
|
-
|
|
814
|
-
## URLRepresentableIntent / Entity / Enum (iOS 18+)
|
|
673
|
+
## URL representations (iOS 18+)
|
|
815
674
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
### URLRepresentableIntent
|
|
675
|
+
Intents, entities and enums can map to deep links.
|
|
819
676
|
|
|
820
677
|
```swift
|
|
821
|
-
struct
|
|
822
|
-
static
|
|
678
|
+
struct ShowRecipeIntent: URLRepresentableIntent {
|
|
679
|
+
static let title: LocalizedStringResource = "Show Recipe"
|
|
680
|
+
static var urlRepresentation: URLRepresentation { "https://example.com/recipes/\(\.$target)" }
|
|
823
681
|
|
|
824
|
-
@Parameter(title: "
|
|
825
|
-
var target: RecipeEntity
|
|
682
|
+
@Parameter(title: "Dish") var target: RecipeEntity
|
|
826
683
|
|
|
827
|
-
static var parameterSummary: some ParameterSummary {
|
|
828
|
-
Summary("Open \(\.$target)")
|
|
829
|
-
}
|
|
684
|
+
static var parameterSummary: some ParameterSummary { Summary("Show \(\.$target)") }
|
|
830
685
|
|
|
831
|
-
func perform() async throws -> some IntentResult & OpensIntent {
|
|
832
|
-
return .result()
|
|
833
|
-
}
|
|
686
|
+
func perform() async throws -> some IntentResult & OpensIntent { .result() }
|
|
834
687
|
}
|
|
835
688
|
|
|
836
|
-
extension
|
|
837
|
-
static var urlRepresentation: URLRepresentation {
|
|
838
|
-
"https://myapp.com/recipes/\(\.$target)"
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
### URLRepresentableEntity
|
|
844
|
-
|
|
845
|
-
```swift
|
|
846
|
-
struct RecipeEntity: URLRepresentableEntity {
|
|
847
|
-
// ... standard AppEntity members ...
|
|
848
|
-
|
|
849
|
-
static var urlRepresentation: URLRepresentation {
|
|
850
|
-
"https://myapp.com/recipes/\(.id)"
|
|
851
|
-
}
|
|
689
|
+
extension RecipeEntity: URLRepresentableEntity {
|
|
690
|
+
static var urlRepresentation: URLRepresentation { "https://example.com/recipes/\(.id)" }
|
|
852
691
|
}
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
### URLRepresentableEnum
|
|
856
|
-
|
|
857
|
-
```swift
|
|
858
|
-
enum RecipeCategory: String, URLRepresentableEnum {
|
|
859
|
-
case breakfast, lunch, dinner
|
|
860
692
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// ... standard AppEnum members ...
|
|
693
|
+
extension MealCourse: URLRepresentableEnum {
|
|
694
|
+
static var urlRepresentation: URLRepresentation { "https://example.com/courses/\(.rawValue)" }
|
|
866
695
|
}
|
|
867
696
|
```
|
|
868
697
|
|
|
869
|
-
|
|
698
|
+
Only universal links qualify here; a custom URL scheme does not.
|
|
699
|
+
Every `urlRepresentation` is a computed property: `URLRepresentation` is not
|
|
700
|
+
`Sendable`, so Swift 6 rejects it as a `static let`.
|
|
870
701
|
|
|
871
|
-
##
|
|
702
|
+
## Spotlight
|
|
872
703
|
|
|
873
|
-
|
|
704
|
+
### IndexedEntity with a custom attribute set
|
|
874
705
|
|
|
875
706
|
```swift
|
|
876
707
|
struct ArticleEntity: IndexedEntity {
|
|
877
708
|
static let defaultQuery = ArticleQuery()
|
|
878
|
-
static
|
|
879
|
-
|
|
880
|
-
var id: String
|
|
709
|
+
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Article"
|
|
881
710
|
|
|
882
|
-
|
|
883
|
-
var
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
var author: String
|
|
711
|
+
let id: String
|
|
712
|
+
@Property(title: "Headline") var headline: String
|
|
713
|
+
@Property(title: "Section") var section: String
|
|
714
|
+
var writers: [String]
|
|
887
715
|
|
|
888
716
|
var displayRepresentation: DisplayRepresentation {
|
|
889
|
-
DisplayRepresentation(title: "\(
|
|
717
|
+
DisplayRepresentation(title: "\(headline)", subtitle: "\(section)")
|
|
890
718
|
}
|
|
891
719
|
|
|
892
|
-
// Start with defaultAttributeSet to keep displayRepresentation metadata
|
|
893
720
|
var attributeSet: CSSearchableItemAttributeSet {
|
|
894
|
-
let
|
|
895
|
-
|
|
896
|
-
return
|
|
721
|
+
let attributes = defaultAttributeSet
|
|
722
|
+
attributes.authorNames = writers
|
|
723
|
+
return attributes
|
|
897
724
|
}
|
|
898
725
|
}
|
|
899
|
-
```
|
|
900
726
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
```swift
|
|
904
|
-
try await CSSearchableIndex(name: "Articles").indexAppEntities(articleEntities)
|
|
727
|
+
// try await CSSearchableIndex(name: "newsroom").indexAppEntities(stories)
|
|
905
728
|
```
|
|
906
729
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
730
|
+
- Returning a fresh `CSSearchableItemAttributeSet` drops the title, subtitle
|
|
731
|
+
and image derived from `displayRepresentation` unless you copy the
|
|
732
|
+
`defaultAttributeSet` values in yourself.
|
|
733
|
+
- If the app already builds `CSSearchableItem` values, call
|
|
734
|
+
`associateAppEntity(_:priority:)` on each item's attribute set so one result
|
|
735
|
+
is shown instead of two. The entity type also needs an `OpenIntent`.
|
|
910
736
|
|
|
911
|
-
|
|
912
|
-
`associateAppEntity(_:priority:)` on the item's attribute set and provide an
|
|
913
|
-
`OpenIntent` for the entity type so Spotlight results can open the right app
|
|
914
|
-
content.
|
|
737
|
+
### Updating and removing
|
|
915
738
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
Update and delete changed records in the same named index. For large syncs, use
|
|
919
|
-
`beginBatch()`, `endBatch(withClientState:)`, and `fetchLastClientState()` so
|
|
920
|
-
indexing can resume after a crash or jetsam.
|
|
739
|
+
Update and delete through the same named index you added to.
|
|
921
740
|
|
|
922
741
|
```swift
|
|
923
|
-
let
|
|
924
|
-
try await
|
|
925
|
-
try await
|
|
926
|
-
identifiedBy: deletedRecipeIDs,
|
|
927
|
-
ofType: RecipeEntity.self
|
|
928
|
-
)
|
|
742
|
+
let index = CSSearchableIndex(name: "articles")
|
|
743
|
+
try await index.indexAppEntities(changed)
|
|
744
|
+
try await index.deleteAppEntities(identifiedBy: removedIDs, ofType: ArticleEntity.self)
|
|
929
745
|
```
|
|
930
746
|
|
|
931
|
-
|
|
747
|
+
For large syncs, wrap chunks in `beginBatch()` and
|
|
748
|
+
`endBatch(withClientState:)`, and read `fetchLastClientState()` at launch so
|
|
749
|
+
indexing picks up where it stopped after a crash or a jetsam kill.
|
|
932
750
|
|
|
933
|
-
|
|
934
|
-
extension ArticleEntity {
|
|
935
|
-
var hideInSpotlight: Bool {
|
|
936
|
-
isDraft // Draft articles should not appear in search
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
```
|
|
751
|
+
### Keeping entities out of Spotlight UI
|
|
940
752
|
|
|
941
|
-
|
|
753
|
+
Implement `hideInSpotlight` (iOS 18.4+), for example returning `true` for
|
|
754
|
+
drafts.
|
|
942
755
|
|
|
943
|
-
|
|
944
|
-
Spotlight metadata. The value is a Swift key path into
|
|
945
|
-
`CSSearchableItemAttributeSet`.
|
|
756
|
+
### indexingKey on properties (iOS 18.4 and 26)
|
|
946
757
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Recipe"
|
|
758
|
+
Both `@Property` and `@ComputedProperty` accept an `indexingKey`: a key path
|
|
759
|
+
naming a `CSSearchableItemAttributeSet` attribute. The `@Property` form needs
|
|
760
|
+
iOS 18.4, `@ComputedProperty` needs iOS 26.
|
|
951
761
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
762
|
+
```swift
|
|
763
|
+
struct TrailEntity: IndexedEntity {
|
|
764
|
+
static let defaultQuery = TrailQuery()
|
|
765
|
+
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Trail"
|
|
956
766
|
|
|
957
|
-
|
|
958
|
-
var
|
|
767
|
+
let id: UUID
|
|
768
|
+
@Property(title: "Trail", indexingKey: \.title) var label: String
|
|
769
|
+
@Property(title: "Region") var region: String
|
|
770
|
+
var lengthKilometres: Double
|
|
771
|
+
var photoPath: String?
|
|
959
772
|
|
|
960
773
|
@ComputedProperty(indexingKey: \.contentDescription)
|
|
961
|
-
var summary: String {
|
|
962
|
-
"\(name) -- \(cuisine) cuisine"
|
|
963
|
-
}
|
|
774
|
+
var summary: String { "\(lengthKilometres) km in \(region)" }
|
|
964
775
|
|
|
965
776
|
@ComputedProperty(indexingKey: \.thumbnailURL)
|
|
966
|
-
var
|
|
967
|
-
URL(string: "https://myapp.com/images/\(id).jpg")
|
|
968
|
-
}
|
|
777
|
+
var thumbnail: URL? { photoPath.map { URL(filePath: $0) } }
|
|
969
778
|
|
|
970
|
-
var displayRepresentation: DisplayRepresentation {
|
|
971
|
-
DisplayRepresentation(title: "\(name)", subtitle: "\(cuisine)")
|
|
972
|
-
}
|
|
779
|
+
var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(label)") }
|
|
973
780
|
}
|
|
974
781
|
```
|
|
975
782
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
struct RecipeEntity: IndexedEntity {
|
|
979
|
-
@Property(title: "Name", indexingKey: \.title)
|
|
980
|
-
var name: String
|
|
783
|
+
Setting `title` again inside a custom `attributeSet` while a `@Property`
|
|
784
|
+
already maps to `\.title` is redundant. Let `indexingKey` do it.
|
|
981
785
|
|
|
982
|
-
|
|
983
|
-
let attrs = CSSearchableItemAttributeSet(contentType: .text)
|
|
984
|
-
attrs.title = name // Redundant: @Property already supplies this key
|
|
985
|
-
return attrs
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
// Prefer indexingKey for metadata already exposed on the entity
|
|
990
|
-
struct RecipeEntity: IndexedEntity {
|
|
991
|
-
@Property(title: "Name", indexingKey: \.title)
|
|
992
|
-
var name: String
|
|
993
|
-
}
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
### Available indexing keys
|
|
997
|
-
|
|
998
|
-
| Key | Property Type | Purpose |
|
|
786
|
+
| Key path | Type | Meaning |
|
|
999
787
|
|---|---|---|
|
|
1000
|
-
| `\.title` | `String` |
|
|
1001
|
-
| `\.contentDescription` | `String` |
|
|
1002
|
-
| `\.thumbnailURL` | `URL?` |
|
|
1003
|
-
| `\.keywords` | `[String]` |
|
|
1004
|
-
| `\.contentURL` | `URL?` |
|
|
788
|
+
| `\.title` | `String` | main searchable title |
|
|
789
|
+
| `\.contentDescription` | `String` | longer description |
|
|
790
|
+
| `\.thumbnailURL` | `URL?` | thumbnail image |
|
|
791
|
+
| `\.keywords` | `[String]` | extra search terms |
|
|
792
|
+
| `\.contentURL` | `URL?` | where the content lives |
|
|
1005
793
|
|
|
1006
|
-
##
|
|
794
|
+
## Visible content for Siri (iOS 26+)
|
|
1007
795
|
|
|
1008
|
-
|
|
1009
|
-
assistant schema
|
|
796
|
+
Content the person is looking at can reach Siri and Apple Intelligence without
|
|
797
|
+
an assistant schema. Make the entity `Transferable` and attach it to the
|
|
798
|
+
current user activity:
|
|
1010
799
|
|
|
1011
800
|
```swift
|
|
1012
|
-
struct
|
|
1013
|
-
|
|
801
|
+
struct TrailEntityCard: AppEntity, Transferable, Codable {
|
|
802
|
+
static let defaultQuery = TrailCardQuery()
|
|
803
|
+
static let typeDisplayRepresentation: TypeDisplayRepresentation = "Trail"
|
|
804
|
+
let id: UUID
|
|
805
|
+
var name: String
|
|
806
|
+
var displayRepresentation: DisplayRepresentation { DisplayRepresentation(title: "\(name)") }
|
|
1014
807
|
|
|
1015
808
|
static var transferRepresentation: some TransferRepresentation {
|
|
1016
|
-
CodableRepresentation(contentType: .
|
|
809
|
+
CodableRepresentation(contentType: .json)
|
|
1017
810
|
}
|
|
1018
811
|
}
|
|
1019
812
|
|
|
1020
|
-
// In
|
|
1021
|
-
let activity = NSUserActivity(activityType: "com.
|
|
1022
|
-
activity.appEntityIdentifier =
|
|
1023
|
-
//
|
|
813
|
+
// In the detail screen:
|
|
814
|
+
// let activity = NSUserActivity(activityType: "com.example.trail.viewing")
|
|
815
|
+
// activity.appEntityIdentifier = EntityIdentifier(for: card)
|
|
816
|
+
// activity.becomeCurrent()
|
|
1024
817
|
```
|
|
1025
818
|
|
|
1026
|
-
|
|
819
|
+
`appEntityIdentifier` needs iOS 18.2 or later, and it takes an
|
|
820
|
+
`EntityIdentifier` built with `init(for:)`.
|
|
1027
821
|
|
|
1028
|
-
|
|
1029
|
-
summaries that change based on parameter values:
|
|
822
|
+
## Conditional parameter summaries
|
|
1030
823
|
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
static var title: LocalizedStringResource = "Configure Widget"
|
|
824
|
+
`When`, `Switch`, `Case` and `DefaultCase` build summaries that change with the
|
|
825
|
+
parameter values.
|
|
1034
826
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
@Parameter(title: "Show Details", default: false)
|
|
1039
|
-
var showDetails: Bool
|
|
827
|
+
```swift
|
|
828
|
+
struct ForecastWidgetConfig: WidgetConfigurationIntent {
|
|
829
|
+
static let title: LocalizedStringResource = "Forecast"
|
|
1040
830
|
|
|
1041
|
-
@Parameter(title: "
|
|
1042
|
-
var
|
|
831
|
+
@Parameter(title: "Use Current Location", default: true) var useCurrent: Bool
|
|
832
|
+
@Parameter(title: "City") var city: String?
|
|
833
|
+
@Parameter(title: "Style", default: .compact) var style: ForecastStyle
|
|
1043
834
|
|
|
1044
835
|
static var parameterSummary: some ParameterSummary {
|
|
1045
|
-
When(\.$
|
|
1046
|
-
Summary("
|
|
1047
|
-
\.$showDetails
|
|
1048
|
-
\.$interval
|
|
1049
|
-
}
|
|
836
|
+
When(\.$useCurrent, .equalTo, true) {
|
|
837
|
+
Summary("Forecast here") { \.$useCurrent; \.$style }
|
|
1050
838
|
} otherwise: {
|
|
1051
|
-
Summary("
|
|
1052
|
-
\.$showDetails
|
|
1053
|
-
}
|
|
839
|
+
Summary("Forecast for \(\.$city)") { \.$useCurrent; \.$style }
|
|
1054
840
|
}
|
|
1055
841
|
}
|
|
1056
842
|
}
|
|
1057
|
-
```
|
|
1058
843
|
|
|
1059
|
-
|
|
844
|
+
struct BrewConfig: WidgetConfigurationIntent {
|
|
845
|
+
static let title: LocalizedStringResource = "Brew"
|
|
1060
846
|
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
Summary("
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
DefaultCase {
|
|
1073
|
-
Summary("Widget") {
|
|
1074
|
-
\.$style
|
|
1075
|
-
}
|
|
847
|
+
@Parameter(title: "Method", default: .pourOver) var method: BrewMethod
|
|
848
|
+
@Parameter(title: "Grams", default: 18) var grams: Int
|
|
849
|
+
@Parameter(title: "Temperature", default: 94) var temperature: Int
|
|
850
|
+
|
|
851
|
+
static var parameterSummary: some ParameterSummary {
|
|
852
|
+
Switch(\.$method) {
|
|
853
|
+
Case(.espresso) { Summary("Espresso, \(\.$grams) g") }
|
|
854
|
+
Case(.pourOver) { Summary("Pour over at \(\.$temperature) C") { \.$grams } }
|
|
855
|
+
DefaultCase { Summary("Brew with \(\.$method)") }
|
|
1076
856
|
}
|
|
1077
857
|
}
|
|
1078
858
|
}
|
|
1079
859
|
```
|
|
1080
860
|
|
|
1081
|
-
## Core Spotlight
|
|
1082
|
-
|
|
1083
|
-
Use Core Spotlight directly when you need full control over indexing without
|
|
1084
|
-
adopting App Intents, or when targeting iOS versions before IndexedEntity
|
|
1085
|
-
(pre-iOS 18). For apps already using App Intents, prefer `IndexedEntity`
|
|
1086
|
-
(iOS 18+) plus `@Property(indexingKey:)` / `@ComputedProperty(indexingKey:)`
|
|
1087
|
-
(iOS 26+) where possible.
|
|
861
|
+
## Core Spotlight without App Intents
|
|
1088
862
|
|
|
1089
|
-
|
|
863
|
+
Talk to Core Spotlight yourself when the app has no App Intents, supports
|
|
864
|
+
releases before iOS 18, or needs every knob. An app that already uses App Intents should
|
|
865
|
+
prefer `IndexedEntity` (iOS 18) and `indexingKey` (iOS 18.4 on `@Property`,
|
|
866
|
+
iOS 26 on `@ComputedProperty`).
|
|
1090
867
|
|
|
1091
|
-
|
|
|
868
|
+
| Situation | Tool |
|
|
1092
869
|
|---|---|
|
|
1093
|
-
|
|
|
1094
|
-
|
|
|
1095
|
-
|
|
|
870
|
+
| Entities already exist for Siri and Shortcuts | `IndexedEntity` |
|
|
871
|
+
| Derived metadata on an `IndexedEntity` | `@ComputedProperty(indexingKey:)` |
|
|
872
|
+
| No App Intents, pre-iOS 18, standalone indexing, or tight control of expiration, domains and batches | Core Spotlight directly |
|
|
1096
873
|
|
|
1097
|
-
###
|
|
874
|
+
### Items and attributes
|
|
1098
875
|
|
|
1099
|
-
A `CSSearchableItem`
|
|
1100
|
-
`CSSearchableItemAttributeSet`
|
|
1101
|
-
|
|
1102
|
-
Docs: [CSSearchableItem](https://sosumi.ai/documentation/corespotlight/cssearchableitem),
|
|
1103
|
-
[CSSearchableItemAttributeSet](https://sosumi.ai/documentation/corespotlight/cssearchableitemattributeset)
|
|
876
|
+
A `CSSearchableItem` identifies one piece of searchable content; its
|
|
877
|
+
`CSSearchableItemAttributeSet` holds the metadata.
|
|
1104
878
|
|
|
1105
879
|
```swift
|
|
1106
880
|
import CoreSpotlight
|
|
1107
881
|
import UniformTypeIdentifiers
|
|
1108
882
|
|
|
1109
|
-
func
|
|
1110
|
-
|
|
1111
|
-
title
|
|
1112
|
-
|
|
1113
|
-
thumbnailData
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
// Optional: improve search ranking and categorization
|
|
1121
|
-
attributes.keywords = ["recipe", "cooking"]
|
|
1122
|
-
attributes.displayName = title
|
|
1123
|
-
attributes.contentURL = URL(string: "myapp://recipes/\(id)")
|
|
1124
|
-
|
|
1125
|
-
let item = CSSearchableItem(
|
|
1126
|
-
uniqueIdentifier: id,
|
|
1127
|
-
domainIdentifier: "com.myapp.recipes",
|
|
1128
|
-
attributeSet: attributes
|
|
1129
|
-
)
|
|
1130
|
-
// Set an expiration date when the default automatic expiration is wrong
|
|
1131
|
-
item.expirationDate = Date.now.addingTimeInterval(60 * 60 * 24 * 90)
|
|
883
|
+
func searchableItem(for note: FieldNote) -> CSSearchableItem {
|
|
884
|
+
let details = CSSearchableItemAttributeSet(contentType: .text)
|
|
885
|
+
details.title = note.heading
|
|
886
|
+
details.contentDescription = note.body
|
|
887
|
+
details.thumbnailData = note.sketchPNG
|
|
888
|
+
details.keywords = note.tags
|
|
889
|
+
details.displayName = note.heading
|
|
890
|
+
details.contentURL = note.shareURL
|
|
891
|
+
let item = CSSearchableItem(uniqueIdentifier: note.id, domainIdentifier: note.notebookID, attributeSet: details)
|
|
892
|
+
item.expirationDate = .now.addingTimeInterval(90 * 24 * 60 * 60)
|
|
1132
893
|
return item
|
|
1133
894
|
}
|
|
1134
895
|
```
|
|
1135
896
|
|
|
1136
|
-
|
|
897
|
+
Set `expirationDate` only when the system's default expiry does not suit you.
|
|
1137
898
|
|
|
1138
|
-
|
|
1139
|
-
production, and add a protection class when indexing sensitive content. Reserve
|
|
1140
|
-
`default()` for prototyping and testing.
|
|
899
|
+
### The index
|
|
1141
900
|
|
|
1142
|
-
|
|
901
|
+
Use a named index in shipping code; keep `CSSearchableIndex.default()` for
|
|
902
|
+
prototypes and tests. Add a protection class when the content is sensitive.
|
|
1143
903
|
|
|
1144
904
|
```swift
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
try await index.indexSearchableItems([item])
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
// Delete specific items by identifier
|
|
1154
|
-
func deleteItems(identifiers: [String]) async throws {
|
|
1155
|
-
let index = CSSearchableIndex(name: "recipes")
|
|
1156
|
-
try await index.deleteSearchableItems(
|
|
1157
|
-
withIdentifiers: identifiers
|
|
1158
|
-
)
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
// Delete all items in a domain (e.g., after user deletes a category)
|
|
1162
|
-
func deleteItemsInDomain(_ domain: String) async throws {
|
|
1163
|
-
let index = CSSearchableIndex(name: "recipes")
|
|
1164
|
-
try await index.deleteSearchableItems(
|
|
1165
|
-
withDomainIdentifiers: [domain]
|
|
1166
|
-
)
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
// Delete everything (e.g., on logout)
|
|
1170
|
-
func deleteAllItems() async throws {
|
|
1171
|
-
let index = CSSearchableIndex(name: "recipes")
|
|
1172
|
-
try await index.deleteAllSearchableItems()
|
|
1173
|
-
}
|
|
905
|
+
let notes = CSSearchableIndex(name: "field-notes")
|
|
906
|
+
try await notes.indexSearchableItems(items)
|
|
907
|
+
try await notes.deleteSearchableItems(withIdentifiers: ["note-17"])
|
|
908
|
+
try await notes.deleteSearchableItems(withDomainIdentifiers: ["notebook-archived"])
|
|
909
|
+
try await notes.deleteAllSearchableItems()
|
|
1174
910
|
```
|
|
1175
911
|
|
|
1176
|
-
|
|
912
|
+
Delete by identifier for single items, by domain when a whole category goes
|
|
913
|
+
away, and everything on logout.
|
|
1177
914
|
|
|
1178
|
-
|
|
1179
|
-
errors gracefully. Use `beginBatch()` / `endBatch(withClientState:)` to
|
|
1180
|
-
track progress and resume after crashes.
|
|
915
|
+
### Batching
|
|
1181
916
|
|
|
1182
|
-
|
|
1183
|
-
import CoreSpotlight
|
|
917
|
+
Index big sets in chunks to keep memory down and contain failures.
|
|
1184
918
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
for batch in stride(from: 0, to: recipes.count, by: batchSize) {
|
|
1191
|
-
let end = min(batch + batchSize, recipes.count)
|
|
1192
|
-
let items = recipes[batch..<end].map { recipe in
|
|
1193
|
-
makeSearchableItem(
|
|
1194
|
-
id: recipe.id,
|
|
1195
|
-
title: recipe.name,
|
|
1196
|
-
description: recipe.summary,
|
|
1197
|
-
thumbnailData: recipe.thumbnailData
|
|
1198
|
-
)
|
|
1199
|
-
}
|
|
1200
|
-
try await index.indexSearchableItems(items)
|
|
919
|
+
```swift
|
|
920
|
+
func indexAll(_ items: [CSSearchableItem], into index: CSSearchableIndex) async throws {
|
|
921
|
+
for start in stride(from: 0, to: items.count, by: 100) {
|
|
922
|
+
let chunk = Array(items[start..<min(start + 100, items.count)])
|
|
923
|
+
try await index.indexSearchableItems(chunk)
|
|
1201
924
|
}
|
|
1202
925
|
}
|
|
1203
926
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
let lastState = try? await index.fetchLastClientState()
|
|
1210
|
-
let startOffset = lastState
|
|
1211
|
-
.flatMap { String(data: $0, encoding: .utf8) }
|
|
1212
|
-
.flatMap(Int.init) ?? 0
|
|
1213
|
-
|
|
1214
|
-
let batchSize = 100
|
|
1215
|
-
for batch in stride(from: startOffset, to: recipes.count, by: batchSize) {
|
|
1216
|
-
let end = min(batch + batchSize, recipes.count)
|
|
1217
|
-
let items = recipes[batch..<end].map { recipe in
|
|
1218
|
-
makeSearchableItem(
|
|
1219
|
-
id: recipe.id,
|
|
1220
|
-
title: recipe.name,
|
|
1221
|
-
description: recipe.summary
|
|
1222
|
-
)
|
|
1223
|
-
}
|
|
1224
|
-
|
|
927
|
+
func resumableIndex(_ items: [CSSearchableItem], into index: CSSearchableIndex) async throws {
|
|
928
|
+
let saved = try await index.fetchLastClientState()
|
|
929
|
+
var offset = Int(String(decoding: saved, as: UTF8.self)) ?? 0
|
|
930
|
+
while offset < items.count {
|
|
931
|
+
let end = min(offset + 100, items.count)
|
|
1225
932
|
index.beginBatch()
|
|
1226
|
-
try await index.indexSearchableItems(items)
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
try await index.endBatch(withClientState: stateData)
|
|
933
|
+
try await index.indexSearchableItems(Array(items[offset..<end]))
|
|
934
|
+
try await index.endBatch(withClientState: Data(String(end).utf8))
|
|
935
|
+
offset = end
|
|
1230
936
|
}
|
|
1231
937
|
}
|
|
1232
938
|
```
|
|
1233
939
|
|
|
1234
|
-
|
|
940
|
+
Swift imports `fetchLastClientState()` as returning non-optional `Data`, so
|
|
941
|
+
there is no optional to unwrap; state that does not parse as a number starts
|
|
942
|
+
the run at 0.
|
|
1235
943
|
|
|
1236
|
-
|
|
944
|
+
### Protected index
|
|
1237
945
|
|
|
1238
946
|
```swift
|
|
1239
|
-
let
|
|
1240
|
-
name: "secure-notes",
|
|
1241
|
-
protectionClass: .complete // Only accessible when device is unlocked
|
|
1242
|
-
)
|
|
1243
|
-
|
|
1244
|
-
try await protectedIndex.indexSearchableItems(sensitiveItems)
|
|
947
|
+
let vault = CSSearchableIndex(name: "health-notes", protectionClass: .complete)
|
|
1245
948
|
```
|
|
1246
949
|
|
|
1247
|
-
|
|
950
|
+
Content in a `.complete` index is encrypted and readable only while the device
|
|
951
|
+
is unlocked.
|
|
952
|
+
|
|
953
|
+
### Opening a tapped result
|
|
1248
954
|
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
955
|
+
A tapped result arrives as an `NSUserActivity` whose `activityType` is
|
|
956
|
+
`CSSearchableItemActionType`; the item ID is in
|
|
957
|
+
`userInfo[CSSearchableItemActivityIdentifier]`.
|
|
1252
958
|
|
|
1253
959
|
```swift
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
|
|
1262
|
-
) -> Bool {
|
|
1263
|
-
if userActivity.activityType == CSSearchableItemActionType,
|
|
1264
|
-
let identifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
|
|
1265
|
-
navigateToItem(withIdentifier: identifier)
|
|
1266
|
-
return true
|
|
1267
|
-
}
|
|
1268
|
-
return false
|
|
960
|
+
// UIKit
|
|
961
|
+
func application(_ app: UIApplication, continue activity: NSUserActivity,
|
|
962
|
+
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
|
|
963
|
+
guard activity.activityType == CSSearchableItemActionType,
|
|
964
|
+
let noteID = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String else { return false }
|
|
965
|
+
Router.shared.openNote(noteID)
|
|
966
|
+
return true
|
|
1269
967
|
}
|
|
1270
968
|
|
|
1271
|
-
// SwiftUI
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
}
|
|
1277
|
-
.onContinueUserActivity(CSSearchableItemActionType) { activity in
|
|
1278
|
-
if let id = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
|
|
1279
|
-
navigateToRecipe(id: id)
|
|
1280
|
-
}
|
|
969
|
+
// SwiftUI
|
|
970
|
+
NotesRoot()
|
|
971
|
+
.onContinueUserActivity(CSSearchableItemActionType) { activity in
|
|
972
|
+
if let noteID = activity.userInfo?[CSSearchableItemActivityIdentifier] as? String {
|
|
973
|
+
router.openNote(noteID)
|
|
1281
974
|
}
|
|
1282
975
|
}
|
|
1283
|
-
}
|
|
1284
976
|
```
|
|
1285
977
|
|
|
1286
|
-
###
|
|
978
|
+
### Search in App
|
|
1287
979
|
|
|
1288
|
-
When
|
|
980
|
+
When the person chooses "Search in App" from Spotlight, the activity type is
|
|
981
|
+
`CSQueryContinuationActionType` and the text is in
|
|
982
|
+
`userInfo[CSSearchQueryString]`:
|
|
1289
983
|
|
|
1290
984
|
```swift
|
|
1291
|
-
|
|
1292
|
-
.onContinueUserActivity(CSQueryContinuationActionType) { activity in
|
|
1293
|
-
|
|
1294
|
-
|
|
985
|
+
NotesRoot()
|
|
986
|
+
.onContinueUserActivity(CSQueryContinuationActionType) { activity in
|
|
987
|
+
if let text = activity.userInfo?[CSSearchQueryString] as? String {
|
|
988
|
+
router.search(text)
|
|
989
|
+
}
|
|
1295
990
|
}
|
|
1296
|
-
}
|
|
1297
991
|
```
|
|
992
|
+
|
|
993
|
+
### Documentation
|
|
994
|
+
|
|
995
|
+
- [CSSearchableItem](https://developer.apple.com/documentation/corespotlight/cssearchableitem)
|
|
996
|
+
- [CSSearchableItemAttributeSet](https://developer.apple.com/documentation/corespotlight/cssearchableitemattributeset)
|
|
997
|
+
- [CSSearchableIndex](https://developer.apple.com/documentation/corespotlight/cssearchableindex)
|