@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

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 (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. 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)