@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.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,1297 +1,997 @@
1
- # App Intents Advanced Reference
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
- - [`@Parameter Initializer Variants`](#parameter-initializer-variants)
11
- - [EntityQuery Variants (Full Examples)](#entityquery-variants-full-examples)
12
- - [EntityPropertyQuery (Filter and Sort)](#entitypropertyquery-filter-and-sort)
13
- - [Control Center Widget Implementation](#control-center-widget-implementation)
14
- - [SnippetIntent Implementation (iOS 26+)](#snippetintent-implementation-ios-26)
15
- - [Visual Intelligence IntentValueQuery (iOS 26+)](#visual-intelligence-intentvaluequery-ios-26)
16
- - [Assistant Schemas (iOS 18+)](#assistant-schemas-ios-18)
17
- - [Focus Filter Intents](#focus-filter-intents)
18
- - [SiriKit Migration (CustomIntentMigratedAppIntent)](#sirikit-migration-customintentmigratedappintent)
19
- - [Error Handling and Dialog](#error-handling-and-dialog)
20
- - [Confirmation Flows](#confirmation-flows)
21
- - [Authentication Policies](#authentication-policies)
22
- - [URLRepresentableIntent / Entity / Enum (iOS 18+)](#urlrepresentableintent-entity-enum-ios-18)
23
- - [IndexedEntity for Spotlight (iOS 18+)](#indexedentity-for-spotlight-ios-18)
24
- - [`@ComputedProperty(indexingKey:) for Spotlight (iOS 26+)`](#computedpropertyindexingkey-for-spotlight-ios-26)
25
- - [Onscreen Content for Siri (iOS 26+)](#onscreen-content-for-siri-ios-26)
26
- - [Parameter Summary Builder](#parameter-summary-builder)
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
- @Parameter(title: "Name", description: "The user's full name")
38
- var name: String
23
+ ## Parameter initializers
39
24
 
40
- @Parameter(title: "Enabled", default: true)
41
- var enabled: Bool
25
+ ### Plain values
42
26
 
43
- @Parameter(title: "Website")
44
- var url: URL?
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
- ### 2. Numeric with Range and Control Style
34
+ ### Numbers with a range and control style
48
35
 
49
36
  ```swift
50
- @Parameter(title: "Volume", controlStyle: .slider, inclusiveRange: (0.0, 100.0))
51
- var volume: Double
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 controls default to `.stepper`. `Int` supports `.stepper` and `.field`;
61
- `Double` also supports `.slider`.
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
- Provide a dynamic set of options at runtime:
45
+ ### Options computed at run time
66
46
 
67
47
  ```swift
68
- struct CategoryOptionsProvider: DynamicOptionsProvider {
48
+ struct PlaylistNames: DynamicOptionsProvider {
69
49
  func results() async throws -> [String] {
70
- await CategoryStore.shared.allNames()
50
+ await Library.shared.playlistTitles()
71
51
  }
72
52
  }
73
53
 
74
- @Parameter(title: "Category", optionsProvider: CategoryOptionsProvider())
75
- var category: String
54
+ @Parameter(title: "Playlist", optionsProvider: PlaylistNames()) var playlist: String
76
55
  ```
77
56
 
78
- ### 4. With Disambiguation Dialog
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: "Size",
85
- requestValueDialog: "What size would you like?",
86
- requestDisambiguationDialog: "Which size did you mean?"
61
+ title: "Contact",
62
+ requestValueDialog: "Who should get the invite?",
63
+ requestDisambiguationDialog: "Which of these did you mean?"
87
64
  )
88
- var size: CupSize
65
+ var contact: ContactEntity
89
66
  ```
90
67
 
91
- ### 5. With Resolvers
68
+ ### Resolvers
92
69
 
93
- Transform raw input into the target type:
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: "Contact", resolvers: [ContactResolver()])
97
- var contact: ContactEntity
74
+ @Parameter(title: "Guests", resolvers: { IntFromStringResolver() })
75
+ var guests: Int
98
76
  ```
99
77
 
100
- ### 6. Entity Parameter with Query
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
- Specify one custom query for entity resolution when the entity's `defaultQuery`
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: "Trail", query: TrailStringQuery())
107
- var trail: TrailEntity
87
+ @Parameter(title: "Plant", query: ThirstyPlantQuery()) var plant: PlantEntity
108
88
  ```
109
89
 
110
- ### 7. Array Parameters
90
+ ### Arrays
111
91
 
112
92
  ```swift
113
- @Parameter(title: "Items")
114
- var items: [ItemEntity]
93
+ @Parameter(title: "Plants") var plants: [PlantEntity]
115
94
  ```
116
95
 
117
- Current Apple docs list the old `size:` entity-array initializers as
118
- deprecated. Prefer a normal array parameter and validate count in `perform()`
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 items.count == 3 else {
123
- throw $items.needsValueError("Choose exactly three items.")
100
+ guard plants.count == 2 else {
101
+ throw $plants.needsValueError("Pick exactly two plants to compare.")
124
102
  }
125
103
  ```
126
104
 
127
- ### 8. File Parameters
105
+ ### Files
128
106
 
129
107
  ```swift
130
- @Parameter(title: "Document", supportedContentTypes: [.pdf, .plainText])
131
- var document: IntentFile
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
- ### 9. Measurement Parameters
112
+ ### Measurements
138
113
 
139
114
  ```swift
140
- @Parameter(
141
- title: "Distance",
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: "Weight", defaultUnit: .kilograms)
149
- var weight: Measurement<UnitMass>
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
- ### 10. Input Connection Behavior
156
-
157
- Control how parameters connect to Shortcuts input:
122
+ ### Connecting to the previous action
158
123
 
159
124
  ```swift
160
- @Parameter(title: "Text", inputConnectionBehavior: .connectToPreviousIntentResult)
161
- var text: String
125
+ @Parameter(title: "Input", inputConnectionBehavior: .connectToPreviousIntentResult)
126
+ var input: IntentFile
162
127
 
163
- @Parameter(title: "File", inputConnectionBehavior: .optionalIfProvided)
164
- var file: IntentFile?
128
+ @Parameter(title: "Attachment", inputConnectionBehavior: .never)
129
+ var attachment: IntentFile?
165
130
  ```
166
131
 
167
- ### Runtime Parameter Methods
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
- Request values, disambiguation, and confirmation at runtime inside `perform()`:
136
+ ### Asking while perform() runs
170
137
 
171
138
  ```swift
172
139
  func perform() async throws -> some IntentResult {
173
- // Request a missing value
174
- if quantity == nil {
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
- // Disambiguate among options
179
- let resolved = try await $size.requestDisambiguation(
180
- among: [.small, .medium, .large],
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
- ## EntityQuery Variants (Full Examples)
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 (free-text search)
153
+ ### EntityStringQuery
197
154
 
198
155
  ```swift
199
- struct SoupStringQuery: EntityStringQuery {
200
- func entities(matching string: String) async throws -> [SoupEntity] {
201
- SoupStore.shared.search(string).map { SoupEntity(from: $0) }
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
- func entities(for identifiers: [String]) async throws -> [SoupEntity] {
204
- SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
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 (finite set)
167
+ ### EnumerableEntityQuery
210
168
 
211
169
  ```swift
212
- struct AllSoupsQuery: EnumerableEntityQuery {
213
- func allEntities() async throws -> [SoupEntity] {
214
- SoupStore.shared.allSoups.map { SoupEntity(from: $0) }
170
+ struct KitchenTimerQuery: EnumerableEntityQuery {
171
+ func allEntities() async throws -> [KitchenTimerEntity] {
172
+ KitchenTimerEntity.presets
215
173
  }
216
- func entities(for identifiers: [String]) async throws -> [SoupEntity] {
217
- SoupStore.shared.soups.filter { identifiers.contains($0.id) }.map { SoupEntity(from: $0) }
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 (singleton, iOS 18+)
223
-
224
- Use for single-instance entities like app settings.
181
+ ### UniqueAppEntityQuery (iOS 18+)
225
182
 
226
183
  ```swift
227
- struct AppSettingsEntity: UniqueAppEntity {
228
- static let defaultQuery = AppSettingsQuery()
229
- static var typeDisplayRepresentation: TypeDisplayRepresentation = "Settings"
230
- var displayRepresentation: DisplayRepresentation { "App Settings" }
184
+ struct PreferencesEntity: UniqueAppEntity {
185
+ static let defaultQuery = PreferencesQuery()
186
+ static let typeDisplayRepresentation: TypeDisplayRepresentation = "Preferences"
231
187
 
232
- var id: String { "app-settings" }
188
+ let id = "app-preferences"
189
+ var displayRepresentation: DisplayRepresentation { "Preferences" }
233
190
  }
234
191
 
235
- struct AppSettingsQuery: UniqueAppEntityQuery {
236
- func uniqueEntity() async throws -> AppSettingsEntity {
237
- AppSettingsEntity()
192
+ struct PreferencesQuery: UniqueAppEntityQuery {
193
+ func uniqueEntity() async throws -> PreferencesEntity {
194
+ PreferencesEntity()
238
195
  }
239
196
  }
240
197
  ```
241
198
 
242
- ## EntityPropertyQuery (Filter and Sort)
199
+ ## EntityPropertyQuery: filter and sort
243
200
 
244
- The most powerful query variant. Declare filterable properties and sortable
245
- fields for structured Siri and Shortcuts queries.
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
- enum TrailComparator: Sendable {
249
- case nameContains(String)
250
- case nameEquals(String)
251
- case lengthGreaterThan(Measurement<UnitLength>)
252
- case lengthLessThan(Measurement<UnitLength>)
253
- case lengthEquals(Measurement<UnitLength>)
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 = QueryProperties {
260
- Property(\TrailEntity.$name) {
261
- ContainsComparator { TrailComparator.nameContains($0) }
262
- EqualToComparator { TrailComparator.nameEquals($0) }
263
- }
264
- Property(\TrailEntity.$trailLength) {
265
- GreaterThanComparator { TrailComparator.lengthGreaterThan($0) }
266
- LessThanComparator { TrailComparator.lengthLessThan($0) }
267
- EqualToComparator { TrailComparator.lengthEquals($0) }
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 = SortingOptions {
272
- SortableBy(\TrailEntity.$name)
273
- SortableBy(\TrailEntity.$trailLength)
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: [TrailComparator],
236
+ matching comparators: [Match],
278
237
  mode: ComparatorMode,
279
- sortedBy: [EntityQuerySort<TrailEntity>],
238
+ sortedBy: [EntityQuerySort<BookEntity>],
280
239
  limit: Int?
281
- ) async throws -> [TrailEntity] {
282
- var results = TrailStore.shared.allTrails.map { TrailEntity(from: $0) }
283
-
284
- results = results.filter { trail in
285
- let matches = comparators.map { comparator in
286
- switch comparator {
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
- if let limit {
304
- results = Array(results.prefix(limit))
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 identifiers: [Trail.ID]) async throws -> [TrailEntity] {
311
- TrailStore.shared.allTrails
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 -> [TrailEntity] {
317
- TrailStore.shared.featured.map { TrailEntity(from: $0) }
262
+ func suggestedEntities() async throws -> [BookEntity] {
263
+ await Shelf.shared.recentlyOpened()
318
264
  }
319
265
  }
320
266
  ```
321
267
 
322
- The comparator closures map user-supplied values into a query-specific
323
- `ComparatorMappingType`; `entities(matching:)` receives those mapped values, not
324
- raw `EntityQueryComparator` objects.
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
- ### Available comparators
276
+ Comparators and what they apply to:
327
277
 
328
- | Comparator | Supported Types |
278
+ | Comparator | Property kind |
329
279
  |---|---|
330
- | `EqualToComparator` | Equatable properties |
331
- | `NotEqualToComparator` | Equatable properties |
332
- | `ContainsComparator` | Sequence properties |
333
- | `HasPrefixComparator` | `String` |
334
- | `HasSuffixComparator` | `String` |
335
- | `GreaterThanComparator` | Comparable properties |
336
- | `LessThanComparator` | Comparable properties |
337
- | `GreaterThanOrEqualToComparator` | Comparable properties |
338
- | `LessThanOrEqualToComparator` | Comparable properties |
339
- | `IsBetweenComparator` | Comparable properties supported by Shortcuts |
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
- struct LightControlConfig: ControlConfigurationIntent {
349
- static var title: LocalizedStringResource = "Light Control"
350
- @Parameter(title: "Light", default: .livingRoom) var light: LightEntity
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 ToggleLightIntent: AppIntent {
354
- static var title: LocalizedStringResource = "Toggle Light"
355
- static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
311
+ struct SetMisterIntent: SetValueIntent {
312
+ static let title: LocalizedStringResource = "Set Mister"
313
+ static let authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
356
314
 
357
- @Parameter(title: "Light") var light: LightEntity
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
- actionName: .toggle,
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 LightControl: ControlWidget {
328
+ struct MisterControl: ControlWidget {
370
329
  var body: some ControlWidgetConfiguration {
371
- AppIntentControlConfiguration(kind: "LightControl", intent: LightControlConfig.self) { config in
372
- ControlWidgetToggle(config.light.name, isOn: config.light.isOn, action: ToggleLightIntent(light: config.light))
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
- Parameters without defaults must be optional on a `ControlConfigurationIntent`.
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
- ## SnippetIntent Implementation (iOS 26+)
346
+ ## Snippets
381
347
 
382
- Display interactive snippets in system UI. The system may call `perform()`
383
- multiple times, including after snippet button or toggle actions, so keep
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 OrderStatusSnippet: SnippetIntent {
389
- static var title: LocalizedStringResource = "Order Status"
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 status = await OrderTracker.currentStatus()
392
- return .result(view: OrderStatusSnippetView(status: status))
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 CheckOrderStatusIntent: AppIntent {
397
- static var title: LocalizedStringResource = "Check Order Status"
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
- .result(snippetIntent: OrderStatusSnippet())
372
+ await Timers.shared.start(timer.id)
373
+ return .result(snippetIntent: TimerSnippet(timer: timer))
400
374
  }
401
375
  }
402
376
  ```
403
377
 
404
- A snippet-only intent is not discoverable in Shortcuts or Spotlight unless
405
- `isDiscoverable` is `true`.
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 IntentValueQuery (iOS 26+)
382
+ ## Visual Intelligence
408
383
 
409
- Only one `IntentValueQuery` can take `SemanticContentDescriptor`; use
410
- `@UnionValue` when one query must return multiple app entity types.
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 ShoppingVisualResult {
416
- case product(ProductEntity)
417
- case store(StoreEntity)
394
+ enum ShelfMatch {
395
+ case book(BookEntity)
396
+ case author(AuthorEntity)
418
397
  }
419
398
 
420
399
  @available(iOS 26, *)
421
- struct ShoppingVisualQuery: IntentValueQuery {
422
- func values(for input: SemanticContentDescriptor) async throws -> [ShoppingVisualResult] {
400
+ struct ShelfLookup: IntentValueQuery {
401
+ func values(for descriptor: SemanticContentDescriptor) async throws -> [ShelfMatch] {
423
402
  try Task.checkCancellation()
424
- async let productMatches = ProductStore.shared.matches(
425
- labels: input.labels,
426
- pixelBuffer: input.pixelBuffer,
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
- Treat `labels` as high-level English descriptors, not exhaustive synonyms or app
441
- taxonomy; combine them with `pixelBuffer` when available. Return small, ranked,
442
- cancellation-friendly results, and provide an `OpenIntent`, URL representation,
443
- or in-app search handoff for details and more results. Do not implement camera
444
- capture, Vision `VN*` requests, barcode classification, or Spotlight indexing
445
- inside the App Intents query; call an existing bounded app search or image-match
446
- service instead, with explicit result caps and timeouts when work may exceed a
447
- system UI budget.
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 define domain-specific intents that Apple Intelligence
452
- understands natively. Annotate conforming types with schema macros.
421
+ ## Assistant schemas (iOS 18+)
453
422
 
454
- ### Declaration
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 OpenPhotoIntent: AppIntent {
464
- static var title: LocalizedStringResource = "Open Photo"
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
- PhotoViewer.shared.open(target.id)
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 PhotoEntity: AppEntity {
477
- var id: String
478
- static let defaultQuery = PhotoQuery()
479
- static var typeDisplayRepresentation: TypeDisplayRepresentation = "Photo"
480
- var displayRepresentation: DisplayRepresentation {
481
- DisplayRepresentation(title: "\(name)")
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 PhotoType: String, AppEnum {
488
- case photo, video, livePhoto
489
- static var typeDisplayRepresentation: TypeDisplayRepresentation = "Photo Type"
490
- static var caseDisplayRepresentations: [PhotoType: DisplayRepresentation] = [
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
- Avoid the deprecated `AssistantIntent(schema:)`, `AssistantEntity(schema:)`, and
499
- `AssistantEnum(schema:)` macros in new code.
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
- ### Domain catalog
460
+ ### Domains
502
461
 
503
- Use Xcode completion and the current domain docs for exact schema cases. The
504
- major Apple domains are:
462
+ Exact case names change between releases, so rely on Xcode completion and the
463
+ current domain documentation. Broadly:
505
464
 
506
- | Domain | Example actions | Example content |
465
+ | Domain | Content types | Actions |
507
466
  |---|---|---|
508
- | Assistant | side-button conversational app launch | -- |
509
- | Books | open book, create bookmark | book, audiobook |
510
- | Browser | open tab, create bookmark, search web | tab, bookmark, window |
511
- | Camera | capture photo, capture video | -- |
512
- | File management | open, create, move, rename, delete file | file |
513
- | Journaling | create, update, delete, search entry | journal entry |
514
- | Mail | open mailbox, send draft | account, draft, mailbox, message |
515
- | Photos | open asset, create album, search assets | album, asset, person |
516
- | Presentations | open document, add slide | document, slide, template |
517
- | Reader | open document, go to page | document, page |
518
- | Spreadsheet | open document, add sheet | document, sheet, template |
519
- | System and in-app search | search | -- |
520
- | Visual intelligence | semantic content search | -- |
521
- | Whiteboard | open board, create item | board, item |
522
- | Word processor | open document, add page | document, page, template |
523
-
524
- ### isAssistantOnly
525
-
526
- Control whether a schema-conforming type is exclusive to Apple Intelligence or
527
- also available through other system surfaces:
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
- @AppIntent(schema: .photos.openAsset)
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 Filter Intents
492
+ ## Focus filters
538
493
 
539
- Customize app behavior when a Focus mode activates.
494
+ `SetFocusFilterIntent` lets the app change its behaviour while a Focus is on.
540
495
 
541
496
  ```swift
542
- struct WorkFocusFilter: SetFocusFilterIntent {
543
- static var title: LocalizedStringResource = "Work Focus"
544
- static var description = IntentDescription("Configure app for work mode.")
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: "Mute Notifications", default: false)
550
- var muteNotifications: Bool
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
- "Work Mode"
505
+ DisplayRepresentation(title: "Watering \(watering ? "on" : "off"), frost \(frost ? "on" : "off")")
554
506
  }
555
507
 
556
508
  func perform() async throws -> some IntentResult {
557
- AppSettings.shared.workModeEnabled = workOnly
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
- ### Suggest filters for a focus context
574
-
575
- ```swift
576
- extension WorkFocusFilter {
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
- ## SiriKit Migration (CustomIntentMigratedAppIntent)
524
+ ## Moving off SiriKit
586
525
 
587
- Replace SiriKit custom intents (`.intentdefinition` files) while preserving
588
- existing user shortcuts and donations.
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 OrderSoupIntent: CustomIntentMigratedAppIntent {
592
- // Map to the old SiriKit intent class name -- must match exactly
593
- static var intentClassName: String = "OrderSoupIntent"
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: "Soup")
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
- let order = try await OrderService.shared.place(
605
- soup: soup.id,
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
- ### Migration steps
614
-
615
- 1. Create a new `AppIntent` struct conforming to `CustomIntentMigratedAppIntent`.
616
- 2. Set `intentClassName` to the old SiriKit intent class name (exact match).
617
- 3. Recreate parameters using `@Parameter` instead of `.intentdefinition` props.
618
- 4. Implement `perform()` with async/await.
619
- 5. Existing user shortcuts and donations continue working via the class name.
620
- 6. Remove the `.intentdefinition` file once migration is verified.
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
- ### DeprecatedAppIntent (versioning within AppIntents)
623
-
624
- Replace an old `AppIntent` with a newer version:
553
+ ### Retiring an AppIntent
625
554
 
626
555
  ```swift
627
- struct OldSearchIntent: DeprecatedAppIntent {
628
- typealias ReplacementIntent = NewSearchIntent
629
- static var deprecation: IntentDeprecation {
630
- .init(message: "Use the new search intent.")
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
- static var title: LocalizedStringResource = "Search (Deprecated)"
562
+
633
563
  func perform() async throws -> some IntentResult { .result() }
634
564
  }
635
565
  ```
636
566
 
637
- ## Error Handling and Dialog
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 error types (iOS 18+)
574
+ ### Standard errors (iOS 18+)
640
575
 
641
576
  ```swift
642
577
  func perform() async throws -> some IntentResult {
643
- guard await PermissionManager.hasPhotoAccess else {
644
- throw AppIntentError.PermissionRequired.photos
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
- | Error Type | When to Use |
586
+ | Family | Meaning |
660
587
  |---|---|
661
- | `AppIntentError.PermissionRequired` | Missing OS-level permission |
662
- | `AppIntentError.Unrecoverable` | Fatal state with no immediate remedy |
663
- | `AppIntentError.UserActionRequired` | User must sign in, confirm, or set up an account |
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-level errors
592
+ ### Parameter errors
666
593
 
667
594
  ```swift
668
- // Re-prompt for a value
669
- throw $quantity.needsValueError("How many items?")
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
- ### Foreground continuation
599
+ ### Continuing in the app (iOS 26+)
676
600
 
677
601
  ```swift
678
- func perform() async throws -> some IntentResult {
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 results
605
+ ### Dialog in the result
688
606
 
689
607
  ```swift
690
608
  func perform() async throws -> some IntentResult & ProvidesDialog {
691
- return .result(dialog: "Your soup order has been placed.")
609
+ .result(dialog: "Done.")
692
610
  }
693
611
 
694
- func perform() async throws -> some IntentResult & ProvidesDialog & ReturnsValue<OrderEntity> {
695
- let order = try await placeOrder()
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 Flows
704
-
705
- ### Basic confirmation
617
+ ## Confirmation
706
618
 
707
619
  ```swift
708
- func perform() async throws -> some IntentResult {
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
- ### Conditional confirmation
622
+ try await requestConfirmation(conditions: .lowConfidenceSource, actionName: .book, dialog: "Reserve the table?")
719
623
 
720
- ```swift
721
- func perform() async throws -> some IntentResult {
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
- ```swift
734
- func perform() async throws -> some IntentResult {
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
- ### User choice
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
- ```swift
747
- func perform() async throws -> some IntentResult {
748
- let chosen = try await requestChoice(
749
- between: availableOptions,
750
- dialog: "Which option would you like?"
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
- ### ConfirmationActionName options
648
+ ## Authentication
758
649
 
759
- Built-in: `.add`, `.buy`, `.call`, `.create`, `.send`, `.share`, `.start`,
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
- Custom:
766
-
767
- ```swift
768
- .custom(
769
- acceptLabel: "Confirm Purchase",
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
- Control when device authentication is required:
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 TransferMoneyIntent: AppIntent {
783
- static var authenticationPolicy: IntentAuthenticationPolicy = .requiresAuthentication
784
- static var title: LocalizedStringResource = "Transfer Money"
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
- // Device must be unlocked before this runs
667
+ try await Account.shared.close()
788
668
  return .result()
789
669
  }
790
670
  }
791
671
  ```
792
672
 
793
- | Policy | Behavior |
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
- Represent intents, entities, and enums as URLs for deep linking.
817
-
818
- ### URLRepresentableIntent
675
+ Intents, entities and enums can map to deep links.
819
676
 
820
677
  ```swift
821
- struct OpenRecipeIntent: URLRepresentableIntent {
822
- static var title: LocalizedStringResource = "Open Recipe"
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: "Recipe")
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 OpenRecipeIntent {
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
- static var urlRepresentation: URLRepresentation {
862
- "https://myapp.com/category/\(.rawValue)"
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
- URL representations must be universal links, not custom URL schemes.
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
- ## IndexedEntity for Spotlight (iOS 18+)
702
+ ## Spotlight
872
703
 
873
- Conform to `IndexedEntity` to make entities searchable in Spotlight.
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 var typeDisplayRepresentation: TypeDisplayRepresentation = "Article"
879
-
880
- var id: String
709
+ static let typeDisplayRepresentation: TypeDisplayRepresentation = "Article"
881
710
 
882
- @Property(title: "Title")
883
- var title: String
884
-
885
- @Property(title: "Author")
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: "\(title)", subtitle: "\(author)")
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 attrs = defaultAttributeSet
895
- attrs.authorNames = [author]
896
- return attrs
721
+ let attributes = defaultAttributeSet
722
+ attributes.authorNames = writers
723
+ return attributes
897
724
  }
898
725
  }
899
- ```
900
726
 
901
- After creating entities, add instances to a named Spotlight index:
902
-
903
- ```swift
904
- try await CSSearchableIndex(name: "Articles").indexAppEntities(articleEntities)
727
+ // try await CSSearchableIndex(name: "newsroom").indexAppEntities(stories)
905
728
  ```
906
729
 
907
- If you return a fresh `CSSearchableItemAttributeSet` from `attributeSet`, add
908
- the contents of `defaultAttributeSet` yourself when you still need the title,
909
- subtitle, or image from `displayRepresentation`.
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
- If your app already creates `CSSearchableItem` values, call
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
- ### Updating and Deleting Indexed Entities
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 recipeIndex = CSSearchableIndex(name: "Recipes")
924
- try await recipeIndex.indexAppEntities(changedRecipes)
925
- try await recipeIndex.deleteAppEntities(
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
- ### Hide specific entities from Spotlight UI
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
- ```swift
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
- ## `@ComputedProperty(indexingKey:)` for Spotlight (iOS 26+)
753
+ Implement `hideInSpotlight` (iOS 18.4+), for example returning `true` for
754
+ drafts.
942
755
 
943
- Use indexing keys on `@Property` and `@ComputedProperty` for structured
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
- ```swift
948
- struct RecipeEntity: IndexedEntity {
949
- static let defaultQuery = RecipeQuery()
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
- var id: String
953
-
954
- @Property(title: "Name", indexingKey: \.title)
955
- var name: String
762
+ ```swift
763
+ struct TrailEntity: IndexedEntity {
764
+ static let defaultQuery = TrailQuery()
765
+ static let typeDisplayRepresentation: TypeDisplayRepresentation = "Trail"
956
766
 
957
- @Property(title: "Cuisine")
958
- var cuisine: String
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 imageURL: URL? {
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
- ```swift
977
- // Avoid duplicating a wrapped property's indexing key in attributeSet
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
- var attributeSet: CSSearchableItemAttributeSet {
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` | Primary searchable title |
1001
- | `\.contentDescription` | `String` | Detailed description |
1002
- | `\.thumbnailURL` | `URL?` | Thumbnail image |
1003
- | `\.keywords` | `[String]` | Additional search terms |
1004
- | `\.contentURL` | `URL?` | Content location |
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
- ## Onscreen Content for Siri (iOS 26+)
794
+ ## Visible content for Siri (iOS 26+)
1007
795
 
1008
- Make onscreen content available to Siri and Apple Intelligence without an
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 ArticleEntity: AppEntity, Transferable {
1013
- // Standard AppEntity conformance...
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: .article)
809
+ CodableRepresentation(contentType: .json)
1017
810
  }
1018
811
  }
1019
812
 
1020
- // In your view controller or SwiftUI view:
1021
- let activity = NSUserActivity(activityType: "com.myapp.article")
1022
- activity.appEntityIdentifier = AppEntityIdentifier(article)
1023
- // Set as current activity
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
- ## Parameter Summary Builder
819
+ `appEntityIdentifier` needs iOS 18.2 or later, and it takes an
820
+ `EntityIdentifier` built with `init(for:)`.
1027
821
 
1028
- Use `When`, `Switch`, `Case`, and `DefaultCase` for conditional parameter
1029
- summaries that change based on parameter values:
822
+ ## Conditional parameter summaries
1030
823
 
1031
- ```swift
1032
- struct ConfigureWidgetIntent: WidgetConfigurationIntent {
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
- @Parameter(title: "Style")
1036
- var style: WidgetStyle
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: "Refresh Interval", default: .hourly)
1042
- var interval: RefreshInterval
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(\.$showDetails, .equalTo, true) {
1046
- Summary("Show \(\.$style) widget") {
1047
- \.$showDetails
1048
- \.$interval
1049
- }
836
+ When(\.$useCurrent, .equalTo, true) {
837
+ Summary("Forecast here") { \.$useCurrent; \.$style }
1050
838
  } otherwise: {
1051
- Summary("Show \(\.$style) widget") {
1052
- \.$showDetails
1053
- }
839
+ Summary("Forecast for \(\.$city)") { \.$useCurrent; \.$style }
1054
840
  }
1055
841
  }
1056
842
  }
1057
- ```
1058
843
 
1059
- ### Switch/Case for multiple conditions
844
+ struct BrewConfig: WidgetConfigurationIntent {
845
+ static let title: LocalizedStringResource = "Brew"
1060
846
 
1061
- ```swift
1062
- static var parameterSummary: some ParameterSummary {
1063
- Switch(\.$style) {
1064
- Case(.compact) {
1065
- Summary("Compact widget")
1066
- }
1067
- Case(.detailed) {
1068
- Summary("Detailed widget") {
1069
- \.$interval
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 Direct Usage
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
- ### When to Use Core Spotlight Directly vs IndexedEntity
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
- | Approach | When to Use |
868
+ | Situation | Tool |
1092
869
  |---|---|
1093
- | `IndexedEntity` (iOS 18+) | App already uses App Intents; entities are also Siri/Shortcuts-visible |
1094
- | `@ComputedProperty(indexingKey:)` (iOS 26+) | Adds derived metadata to an `IndexedEntity` |
1095
- | Core Spotlight directly | No App Intents adoption; pre-iOS 18 targets; standalone indexing; fine-grained control over expiration, domain grouping, or batch operations |
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
- ### CSSearchableItem and CSSearchableItemAttributeSet
874
+ ### Items and attributes
1098
875
 
1099
- A `CSSearchableItem` uniquely identifies searchable content. Attach a
1100
- `CSSearchableItemAttributeSet` to describe the item's metadata.
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 makeSearchableItem(
1110
- id: String,
1111
- title: String,
1112
- description: String,
1113
- thumbnailData: Data? = nil
1114
- ) -> CSSearchableItem {
1115
- let attributes = CSSearchableItemAttributeSet(contentType: .text)
1116
- attributes.title = title
1117
- attributes.contentDescription = description
1118
- attributes.thumbnailData = thumbnailData
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
- ### CSSearchableIndex - Indexing and Deletion
897
+ Set `expirationDate` only when the system's default expiry does not suit you.
1137
898
 
1138
- Use `CSSearchableIndex` to add, update, and remove items. Use a named index in
1139
- production, and add a protection class when indexing sensitive content. Reserve
1140
- `default()` for prototyping and testing.
899
+ ### The index
1141
900
 
1142
- Docs: [CSSearchableIndex](https://sosumi.ai/documentation/corespotlight/cssearchableindex)
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
- import CoreSpotlight
1146
-
1147
- // Index a single item (add or update)
1148
- func indexItem(_ item: CSSearchableItem) async throws {
1149
- let index = CSSearchableIndex(name: "recipes")
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
- ### Batch Indexing Patterns
912
+ Delete by identifier for single items, by domain when a whole category goes
913
+ away, and everything on logout.
1177
914
 
1178
- For large data sets, index in batches to minimize memory pressure and handle
1179
- errors gracefully. Use `beginBatch()` / `endBatch(withClientState:)` to
1180
- track progress and resume after crashes.
915
+ ### Batching
1181
916
 
1182
- ```swift
1183
- import CoreSpotlight
917
+ Index big sets in chunks to keep memory down and contain failures.
1184
918
 
1185
- func batchIndexRecipes(_ recipes: [Recipe]) async throws {
1186
- let index = CSSearchableIndex(name: "recipes")
1187
-
1188
- // Simple batched approach -- chunk into groups
1189
- let batchSize = 100
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
- // Client-state-based batching for crash recovery
1205
- func batchIndexWithState(_ recipes: [Recipe]) async throws {
1206
- let index = CSSearchableIndex(name: "recipes")
1207
-
1208
- // Check where we left off
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
- let stateData = "\(end)".data(using: .utf8)!
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
- ### Protected Index for Sensitive Content
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
- Use a named index with a data protection class to encrypt indexed content:
944
+ ### Protected index
1237
945
 
1238
946
  ```swift
1239
- let protectedIndex = CSSearchableIndex(
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
- ### Handling Search Results (NSUserActivity)
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
- When a user taps a Spotlight result, the system delivers an `NSUserActivity`
1250
- with `activityType` set to `CSSearchableItemActionType`. Extract the item
1251
- identifier from `userInfo` to navigate to the correct content.
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
- import CoreSpotlight
1255
- import UIKit
1256
-
1257
- // UIKit: In AppDelegate or SceneDelegate
1258
- func application(
1259
- _ application: UIApplication,
1260
- continue userActivity: NSUserActivity,
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: Use onContinueUserActivity
1272
- struct ContentView: View {
1273
- var body: some View {
1274
- NavigationStack {
1275
- RecipeListView()
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
- ### Query Continuation
978
+ ### Search in App
1287
979
 
1288
- When a user taps "Search in App" from Spotlight, handle the query string:
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
- // activityType == CSQueryContinuationActionType
1292
- .onContinueUserActivity(CSQueryContinuationActionType) { activity in
1293
- if let query = activity.userInfo?[CSSearchQueryString] as? String {
1294
- searchViewModel.searchText = query
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)