@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,810 +1,807 @@
1
- # Core ML Swift Integration Reference
1
+ # Core ML in Swift: extended patterns
2
2
 
3
- Complete implementation patterns for loading, configuring, and running Core ML
4
- models in Swift. All patterns target iOS 26+ with Swift 6.3, backward-compatible
5
- to iOS 14 unless noted.
3
+ Companion to `../SKILL.md`. Baseline iOS 26 and Swift 6.3; lower floors are
4
+ called out per API, and most of this works back to iOS 14 when the newer calls
5
+ are left out.
6
6
 
7
7
  ## Contents
8
- - Actor-Based Model Loading and Caching
9
- - Auto-Generated Class Usage
10
- - Manual MLFeatureProvider
11
- - Prediction in Async Workflows
12
- - MLBatchProvider for Batch Inference
13
- - Stateful Predictions with MLState (iOS 18+)
14
- - Image Preprocessing
15
- - MLMultiArray Creation and Manipulation
16
- - MLTensor Advanced Operations
17
- - Vision + Core ML Pipelines
18
- - NaturalLanguage Integration
19
- - MLComputePlan Detailed Usage (iOS 17.4+)
20
- - Background Loading and Memory Management
21
- - Error Handling Patterns
22
- - Testing Patterns
23
-
24
- ## Actor-Based Model Loading and Caching
25
-
26
- Use an actor to manage model lifecycle, prevent concurrent loading, and cache
27
- compiled models persistently.
8
+
9
+ - [An actor that owns models](#an-actor-that-owns-models)
10
+ - [Generated classes](#generated-classes)
11
+ - [A hand-written feature provider](#a-hand-written-feature-provider)
12
+ - [Predictions in async code](#predictions-in-async-code)
13
+ - [Batch inference](#batch-inference)
14
+ - [Stateful sessions with MLState (iOS 18+)](#stateful-sessions-with-mlstate-ios-18)
15
+ - [Pixel buffers](#pixel-buffers)
16
+ - [MLMultiArray](#mlmultiarray)
17
+ - [MLTensor (iOS 18+)](#mltensor-ios-18)
18
+ - [Vision and Core ML together](#vision-and-core-ml-together)
19
+ - [NaturalLanguage models](#naturallanguage-models)
20
+ - [MLComputePlan in detail (iOS 17.4+)](#mlcomputeplan-in-detail-ios-174)
21
+ - [Lifecycle: warm up and release](#lifecycle-warm-up-and-release)
22
+ - [Errors](#errors)
23
+ - [Tests with Swift Testing](#tests-with-swift-testing)
24
+ - [Memory practices](#memory-practices)
25
+
26
+ ## An actor that owns models
27
+
28
+ An actor serializes loading, guarantees one instance per model, and keeps
29
+ compiled models on disk between launches.
28
30
 
29
31
  ```swift
30
32
  import CoreML
31
33
 
32
- actor ModelManager {
33
- private var loadedModels: [String: MLModel] = [:]
34
- private let cacheDirectory: URL
34
+ enum ModelStoreError: Error {
35
+ case missing(String)
36
+ case predictionFailed(String)
37
+ }
38
+
39
+ actor ModelStore {
40
+ static let shared = ModelStore()
41
+
42
+ private var loaded: [String: MLModel] = [:]
43
+ private let compiledFolder: URL
35
44
 
36
45
  init() {
37
- let appSupport = FileManager.default.urls(
38
- for: .applicationSupportDirectory, in: .userDomainMask
39
- ).first!
40
- cacheDirectory = appSupport.appendingPathComponent("CompiledModels", isDirectory: true)
41
- try? FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
46
+ compiledFolder = URL.applicationSupportDirectory.appending(path: "CompiledModels", directoryHint: .isDirectory)
47
+ try? FileManager.default.createDirectory(at: compiledFolder, withIntermediateDirectories: true)
42
48
  }
43
49
 
44
- func model(named name: String, configuration: MLModelConfiguration = .init()) async throws -> MLModel {
45
- if let cached = loadedModels[name] {
46
- return cached
47
- }
50
+ func prepare(_ name: String, settings: sending MLModelConfiguration = MLModelConfiguration()) async throws {
51
+ _ = try await model(name, settings: settings)
52
+ }
48
53
 
49
- let compiledURL = try await compiledModelURL(for: name)
50
- let model = try await MLModel.load(contentsOf: compiledURL, configuration: configuration)
51
- loadedModels[name] = model
52
- return model
54
+ func predict(_ name: String, from input: sending MLFeatureProvider) async throws -> sending MLFeatureProvider {
55
+ nonisolated(unsafe) let engine = try await model(name)
56
+ return try await engine.prediction(from: input)
53
57
  }
54
58
 
55
- func unloadModel(named name: String) {
56
- loadedModels.removeValue(forKey: name)
59
+ func release(_ name: String) {
60
+ loaded[name] = nil
57
61
  }
58
62
 
59
- func unloadAll() {
60
- loadedModels.removeAll()
63
+ func releaseEverything() {
64
+ loaded.removeAll()
61
65
  }
62
66
 
63
- private func compiledModelURL(for name: String) async throws -> URL {
64
- // Check for pre-compiled model in cache
65
- let cachedURL = cacheDirectory.appendingPathComponent("\(name).mlmodelc")
66
- if FileManager.default.fileExists(atPath: cachedURL.path) {
67
- return cachedURL
67
+ private func model(_ name: String, settings: sending MLModelConfiguration = MLModelConfiguration()) async throws -> MLModel {
68
+ if let ready = loaded[name] {
69
+ return ready
68
70
  }
71
+ let location = try await compiledLocation(for: name)
72
+ let fresh = try await MLModel.load(contentsOf: location, configuration: settings)
73
+ loaded[name] = fresh
74
+ return fresh
75
+ }
69
76
 
70
- // Check for pre-compiled model in bundle
71
- if let bundledCompiledURL = Bundle.main.url(forResource: name, withExtension: "mlmodelc") {
72
- return bundledCompiledURL
77
+ private func compiledLocation(for key: String) async throws -> URL {
78
+ let files = FileManager.default
79
+ let cached = compiledFolder.appending(path: key + ".mlmodelc")
80
+ if files.fileExists(atPath: cached.path()) {
81
+ return cached
73
82
  }
74
-
75
- // Compile from .mlpackage and cache
76
- guard let packageURL = Bundle.main.url(forResource: name, withExtension: "mlpackage") else {
77
- throw ModelManagerError.modelNotFound(name)
83
+ if let shipped = Bundle.main.url(forResource: key, withExtension: "mlmodelc") {
84
+ return shipped
78
85
  }
79
- let tempCompiledURL = try await MLModel.compileModel(at: packageURL)
80
-
81
- // Move compiled model to persistent cache
82
- if FileManager.default.fileExists(atPath: cachedURL.path) {
83
- try FileManager.default.removeItem(at: cachedURL)
86
+ guard let package = Bundle.main.url(forResource: key, withExtension: "mlpackage") else {
87
+ throw ModelStoreError.missing(key)
84
88
  }
85
- try FileManager.default.moveItem(at: tempCompiledURL, to: cachedURL)
86
- return cachedURL
89
+ let temporary = try await MLModel.compileModel(at: package)
90
+ try? files.removeItem(at: cached)
91
+ try files.moveItem(at: temporary, to: cached)
92
+ return cached
87
93
  }
88
94
  }
89
-
90
- enum ModelManagerError: Error {
91
- case modelNotFound(String)
92
- case predictionFailed(String)
93
- }
94
95
  ```
95
96
 
96
- ### Usage with SwiftUI
97
+ Lookup order: a previously compiled copy in Application Support, then a
98
+ `.mlmodelc` shipped in the bundle, then compiling a bundled `.mlpackage` and
99
+ moving the result into the cache. Anything else is `missing`.
97
100
 
98
- ```swift
99
- @MainActor
100
- @Observable
101
- final class ClassifierViewModel {
102
- var classLabel: String = ""
103
- var confidence: Double = 0
104
- var isLoading = false
105
- var errorMessage: String?
101
+ `MLModel` is not `Sendable`, so the store never hands one out: returning it to
102
+ a main-actor caller fails to compile under Swift 6. Callers warm a model with
103
+ `prepare` and run it with `predict`. Inputs, outputs and the configuration are
104
+ `sending`, so the caller passes a freshly built value and owns the result. The
105
+ `nonisolated(unsafe)` copy in `predict` lets the cached model reach the async
106
+ prediction call, which may run concurrently on one model (see "Predictions in
107
+ async code" below). For downloaded
108
+ models, put the version in the cache file name so a new version compiles once.
106
109
 
107
- private let modelManager = ModelManager()
110
+ ### Driving it from SwiftUI
108
111
 
109
- func classify(image: CGImage) async {
110
- isLoading = true
111
- defer { isLoading = false }
112
+ Most examples below feed one image feature, so they share a tiny helper:
112
113
 
113
- do {
114
- let config = MLModelConfiguration()
115
- config.computeUnits = .all
114
+ ```swift
115
+ import CoreML
116
116
 
117
- let model = try await modelManager.model(named: "ImageClassifier", configuration: config)
117
+ func photoInput(_ frame: CVPixelBuffer) throws -> MLFeatureProvider {
118
+ try MLDictionaryFeatureProvider(dictionary: ["photo": MLFeatureValue(pixelBuffer: frame)])
119
+ }
120
+ ```
118
121
 
119
- let pixelBuffer = try createPixelBuffer(from: image, width: 224, height: 224)
120
- let input = try MLDictionaryFeatureProvider(dictionary: [
121
- "image": MLFeatureValue(pixelBuffer: pixelBuffer),
122
- ])
123
- let output = try model.prediction(from: input)
122
+ ```swift
123
+ import Observation
124
+ import CoreML
124
125
 
125
- classLabel = output.featureValue(for: "classLabel")?.stringValue ?? "Unknown"
126
- confidence = output.featureValue(for: "classLabelProbs")?
127
- .dictionaryValue[classLabel]?
128
- .doubleValue ?? 0
126
+ @MainActor
127
+ @Observable
128
+ final class DishRecognizer {
129
+ var dish = ""
130
+ var certainty = 0.0
131
+ var working = false
132
+ var failure: String?
133
+
134
+ func recognize(_ picture: CGImage) async {
135
+ working = true
136
+ defer { working = false }
137
+ do {
138
+ let frame = try makeFrame(from: picture, width: 224, height: 224)
139
+ let answer = try await ModelStore.shared.predict("DishClassifier", from: photoInput(frame))
140
+ dish = answer.featureValue(for: "classLabel")?.stringValue ?? ""
141
+ certainty = answer.featureValue(for: "classLabelProbs")?.dictionaryValue[dish]?.doubleValue ?? 0
129
142
  } catch {
130
- errorMessage = "Classification failed: \(error.localizedDescription)"
143
+ failure = error.localizedDescription
131
144
  }
132
145
  }
133
146
  }
134
147
  ```
135
148
 
136
- ## Auto-Generated Class Usage
149
+ `makeFrame(from:width:height:)` is defined in the pixel buffer section below.
150
+ The frame is created inside `recognize`, which is what allows its feature
151
+ provider to be sent to the store. The dictionary lookup reads the probability of the winning
152
+ label out of the `classLabelProbs` feature.
137
153
 
138
- When you add a `.mlmodel` or `.mlpackage` to your Xcode project, Xcode
139
- generates a Swift class with typed inputs and outputs.
154
+ ## Generated classes
155
+
156
+ For a model file named `SceneTagger`, Xcode generates `SceneTagger`,
157
+ `SceneTaggerInput` and `SceneTaggerOutput`.
140
158
 
141
159
  ```swift
142
160
  import CoreML
161
+ import Vision
143
162
 
144
- // Xcode generates: MyImageClassifier, MyImageClassifierInput, MyImageClassifierOutput
145
-
146
- // Synchronous prediction with generated types
147
- func classifyWithGeneratedClass(pixelBuffer: CVPixelBuffer) throws -> (label: String, confidence: Double) {
148
- let config = MLModelConfiguration()
149
- config.computeUnits = .all
150
-
151
- let classifier = try MyImageClassifier(configuration: config)
152
- let input = MyImageClassifierInput(image: pixelBuffer)
153
- let output = try classifier.prediction(input: input)
163
+ func bestDish(_ frame: CVPixelBuffer, using classifier: SceneTagger) throws -> (name: String, probability: Double) {
164
+ let verdict = try classifier.prediction(input: SceneTaggerInput(image: frame))
165
+ return (verdict.classLabel, verdict.classLabelProbs[verdict.classLabel] ?? 0)
166
+ }
154
167
 
155
- let topLabel = output.classLabel
156
- let topConfidence = output.classLabelProbs[topLabel] ?? 0
157
- return (topLabel, topConfidence)
168
+ func legacyVisionModel(for classifier: SceneTagger) throws -> VNCoreMLModel {
169
+ try VNCoreMLModel(for: classifier.model)
158
170
  }
159
171
  ```
160
172
 
161
- ### Accessing the Underlying MLModel
162
-
163
- ```swift
164
- // Get the underlying MLModel from a generated class
165
- let classifier = try MyImageClassifier(configuration: config)
166
- let mlModel = classifier.model
167
-
168
- // Useful for Vision integration
169
- let vnModel = try VNCoreMLModel(for: mlModel)
170
- ```
173
+ The generated class exposes the underlying `MLModel` as `.model`, which is
174
+ what `VNCoreMLModel(for:)` and `CoreMLModelContainer(model:)` expect.
171
175
 
172
- ## Manual MLFeatureProvider
176
+ ## A hand-written feature provider
173
177
 
174
- Implement `MLFeatureProvider` when you need custom input construction.
178
+ Implement `MLFeatureProvider` when the input is assembled in code:
175
179
 
176
180
  ```swift
177
181
  import CoreML
178
182
 
179
- final class CustomImageInput: MLFeatureProvider {
180
- let image: CVPixelBuffer
181
- let confidenceThreshold: Double
183
+ final class GlareCheckInput: MLFeatureProvider {
184
+ let frame: CVPixelBuffer
185
+ let sensitivity: Double
182
186
 
183
- var featureNames: Set<String> {
184
- ["image", "confidence_threshold"]
187
+ init(frame: CVPixelBuffer, sensitivity: Double = 0.5) {
188
+ self.frame = frame
189
+ self.sensitivity = sensitivity
185
190
  }
186
191
 
187
- func featureValue(for featureName: String) -> MLFeatureValue? {
188
- switch featureName {
189
- case "image":
190
- return MLFeatureValue(pixelBuffer: image)
191
- case "confidence_threshold":
192
- return MLFeatureValue(double: confidenceThreshold)
193
- default:
194
- return nil
195
- }
192
+ var featureNames: Set<String> {
193
+ ["photo", "sensitivity"]
196
194
  }
197
195
 
198
- init(image: CVPixelBuffer, confidenceThreshold: Double = 0.5) {
199
- self.image = image
200
- self.confidenceThreshold = confidenceThreshold
196
+ func featureValue(for requested: String) -> MLFeatureValue? {
197
+ if requested == "sensitivity" {
198
+ return MLFeatureValue(double: sensitivity)
199
+ }
200
+ return requested == "photo" ? MLFeatureValue(pixelBuffer: frame) : nil
201
201
  }
202
202
  }
203
203
 
204
- // Usage
205
- let input = CustomImageInput(image: pixelBuffer, confidenceThreshold: 0.7)
206
- let output = try model.prediction(from: input)
204
+ func hasGlare(_ frame: CVPixelBuffer, model: MLModel) async throws -> Bool {
205
+ let verdict = try await model.prediction(from: GlareCheckInput(frame: frame))
206
+ return verdict.featureValue(for: "glare")?.int64Value == 1
207
+ }
207
208
  ```
208
209
 
209
- ## Prediction in Async Workflows
210
+ Unknown feature names return `nil`.
211
+
212
+ ## Predictions in async code
210
213
 
211
- `MLModel.prediction(...)` is synchronous. Use Swift concurrency to keep loading,
212
- preprocessing, and caller coordination off the main actor, then call prediction
213
- without `await`.
214
+ Keep loading, preprocessing and coordination off the main actor.
214
215
 
215
- ### Single Prediction from an Actor
216
+ - iOS 17 and later: `prediction(from:options:) async throws` is the natural
217
+ call in async code and may run concurrently on one model. The compiler
218
+ picks it inside `async` functions, so write `try await`.
219
+ - The synchronous `prediction(from:)` is for synchronous contexts and for
220
+ deployment targets below iOS 17.
216
221
 
217
222
  ```swift
218
- actor PredictionService {
219
- private let model: MLModel
223
+ import CoreML
224
+
225
+ actor InferenceService {
226
+ nonisolated(unsafe) private let engine: MLModel
220
227
 
221
- init(model: MLModel) {
222
- self.model = model
228
+ init(engine: MLModel) {
229
+ self.engine = engine
223
230
  }
224
231
 
225
- func predict(input: any MLFeatureProvider) async throws -> any MLFeatureProvider {
226
- try model.prediction(from: input)
232
+ func run(_ features: sending MLFeatureProvider) async throws -> sending MLFeatureProvider {
233
+ try await engine.prediction(from: features)
234
+ }
235
+
236
+ func runBlocking(_ features: MLFeatureProvider) throws -> MLFeatureProvider {
237
+ try engine.prediction(from: features)
227
238
  }
228
239
  }
229
240
  ```
230
241
 
231
- ### Streaming Predictions
242
+ `runBlocking` is a synchronous actor method, so the synchronous overload
243
+ applies there. Calling the async overload hands the non-`Sendable` model and
244
+ input to a nonisolated method, which Swift 6 rejects for plain actor state
245
+ ("sending 'self.engine' risks causing data races"). The model is therefore
246
+ stored `nonisolated(unsafe)`, relying on the async API being safe to call
247
+ concurrently, and `run` takes and returns `sending` values.
248
+
249
+ ### Streaming frames
232
250
 
233
251
  ```swift
234
- func classifyFrames(_ frames: AsyncStream<CVPixelBuffer>) async throws -> AsyncThrowingStream<String, Error> {
235
- let model = try await ModelManager().model(named: "Classifier")
236
-
237
- return AsyncThrowingStream { continuation in
238
- Task {
239
- do {
240
- for await frame in frames {
241
- let input = try MLDictionaryFeatureProvider(dictionary: [
242
- "image": MLFeatureValue(pixelBuffer: frame),
243
- ])
244
- let output = try model.prediction(from: input)
245
- let label = output.featureValue(for: "classLabel")?.stringValue ?? "unknown"
246
- continuation.yield(label)
247
- }
248
- continuation.finish()
249
- } catch {
250
- continuation.finish(throwing: error)
252
+ import CoreML
253
+
254
+ func liveLabels(from frames: AsyncStream<CameraSample>, model: sending MLModel) -> AsyncThrowingStream<String, Error> {
255
+ let (labels, sink) = AsyncThrowingStream<String, Error>.makeStream()
256
+ let worker = Task {
257
+ do {
258
+ for await sample in frames {
259
+ let verdict = try await model.prediction(from: photoInput(sample.buffer))
260
+ sink.yield(verdict.featureValue(for: "classLabel")?.stringValue ?? "")
251
261
  }
262
+ sink.finish()
263
+ } catch {
264
+ sink.finish(throwing: error)
252
265
  }
253
266
  }
267
+ sink.onTermination = { _ in worker.cancel() }
268
+ return labels
269
+ }
270
+
271
+ struct CameraSample: @unchecked Sendable {
272
+ let buffer: CVPixelBuffer
254
273
  }
255
274
  ```
256
275
 
257
- ## MLBatchProvider for Batch Inference
276
+ `CVPixelBuffer` is not `Sendable`; the wrapper is acceptable only when nothing
277
+ touches the buffer after it is handed over. The model parameter is `sending`
278
+ for the same reason: the worker task takes it over, and the caller must not use
279
+ that instance again. The stream comes from `makeStream()` rather than the
280
+ closure initializer, because a `Task` started inside that closure cannot
281
+ capture the model under Swift 6.
282
+
283
+ ## Batch inference
258
284
 
259
285
  ```swift
260
286
  import CoreML
261
287
 
262
- func batchClassify(images: [CVPixelBuffer], model: MLModel) throws -> [(label: String, confidence: Double)] {
263
- let batchInputs = try MLArrayBatchProvider(array: images.map { buffer in
264
- try MLDictionaryFeatureProvider(dictionary: [
265
- "image": MLFeatureValue(pixelBuffer: buffer),
266
- ])
267
- })
268
-
269
- let batchOutput = try model.predictions(fromBatch: batchInputs)
270
-
271
- var results: [(String, Double)] = []
272
- for i in 0..<batchOutput.count {
273
- let features = batchOutput.features(at: i)
274
- let label = features.featureValue(for: "classLabel")?.stringValue ?? "unknown"
275
- let probs = features.featureValue(for: "classLabelProbs")?.dictionaryValue ?? [:]
276
- let confidence = (probs[label] as? NSNumber)?.doubleValue ?? 0
277
- results.append((label, confidence))
278
- }
279
- return results
288
+ func labelBatch(_ frames: [CVPixelBuffer], model: MLModel) throws -> [(String, Double)] {
289
+ let rows = try frames.map(photoInput)
290
+ let results = try model.predictions(fromBatch: MLArrayBatchProvider(array: rows))
291
+ var pairs: [(String, Double)] = []
292
+ for index in 0..<results.count {
293
+ let row = results.features(at: index)
294
+ let top = row.featureValue(for: "classLabel")?.stringValue ?? ""
295
+ let table = row.featureValue(for: "classLabelProbs")?.dictionaryValue
296
+ pairs.append((top, (table?[top] as? NSNumber)?.doubleValue ?? 0))
297
+ }
298
+ return pairs
280
299
  }
281
- ```
282
300
 
283
- Batch prediction has two valid API labels:
284
-
285
- ```swift
286
- // No explicit prediction options
287
- let output = try model.predictions(fromBatch: batchInputs)
288
-
289
- // Explicit prediction options
290
- let options = MLPredictionOptions()
291
- let outputWithOptions = try model.predictions(from: batchInputs, options: options)
301
+ func labelBatchWithOptions(_ batch: MLBatchProvider, model: MLModel) throws -> MLBatchProvider {
302
+ let options = MLPredictionOptions()
303
+ return try model.predictions(from: batch, options: options)
304
+ }
292
305
  ```
293
306
 
294
- Do not write `predictions(from:)` for the no-options batch path; the `from:`
295
- label belongs to the overload that also takes `options:`.
307
+ The two correct spellings: `predictions(fromBatch:)` without options and
308
+ `predictions(from:options:)` with them. `predictions(from:)` alone is not the
309
+ options-free batch call.
296
310
 
297
- ## Stateful Predictions with MLState (iOS 18+)
311
+ ## Stateful sessions with MLState (iOS 18+)
298
312
 
299
- `MLState` enables models that maintain internal state across predictions. This is
300
- essential for sequence models (text generation, audio classification, time-series)
301
- where each prediction depends on previous context.
313
+ Text generation, audio classification and time-series models carry context
314
+ from one call to the next. `MLState` holds that context.
302
315
 
303
316
  ```swift
304
317
  import CoreML
305
318
 
306
- /// Audio classification that accumulates context over time
307
- actor AudioClassifier {
308
- private let model: MLModel
309
- private var state: MLState?
319
+ enum ListeningError: Error {
320
+ case notListening
321
+ }
310
322
 
311
- init(model: MLModel) {
312
- self.model = model
313
- }
323
+ actor ChirpDetector {
324
+ private let engine: MLModel
325
+ private var memory: MLState?
314
326
 
315
- /// Start a new classification session
316
- func beginSession() {
317
- state = model.makeState()
327
+ init(engine: MLModel) {
328
+ self.engine = engine
318
329
  }
319
330
 
320
- /// Classify the next audio frame using accumulated state
321
- func classify(audioFeatures: MLMultiArray) async throws -> String {
322
- guard let state else {
323
- throw ClassifierError.noActiveSession
324
- }
325
-
326
- let input = try MLDictionaryFeatureProvider(dictionary: [
327
- "audio_features": MLFeatureValue(multiArray: audioFeatures)
328
- ])
329
-
330
- let output = try model.prediction(from: input, using: state)
331
- return output.featureValue(for: "label")?.stringValue ?? "unknown"
331
+ func startListening() {
332
+ memory = engine.makeState()
332
333
  }
333
334
 
334
- /// End the session and release state
335
- func endSession() {
336
- state = nil
335
+ func detect(_ window: MLMultiArray) throws -> String? {
336
+ guard let memory else { throw ListeningError.notListening }
337
+ let input = try MLDictionaryFeatureProvider(dictionary: ["samples": MLFeatureValue(multiArray: window)])
338
+ let heard = try engine.prediction(from: input, using: memory)
339
+ return heard.featureValue(for: "species")?.stringValue
337
340
  }
338
- }
339
341
 
340
- enum ClassifierError: Error {
341
- case noActiveSession
342
+ func stopListening() {
343
+ memory = nil
344
+ }
342
345
  }
343
346
  ```
344
347
 
345
- ### Key Rules for MLState
348
+ Rules:
346
349
 
347
- - **Serialized use:** `MLState` is `Sendable`, but predictions that use the same
348
- state must be serialized. `Sendable` permits transfer across concurrency
349
- domains; it does not permit concurrent predictions on one state. Do not read or
350
- write state buffers while a prediction is in flight.
351
- - **Async options overload:** Use the synchronous `prediction(from:using:)`
352
- overload for simple serialized loops. If you need `MLPredictionOptions`, iOS
353
- 18+ also provides async `prediction(from:using:options:)`; keep one in-flight
354
- prediction per state.
355
- - **Independent streams:** Call `model.makeState()` per stream when processing
356
- multiple concurrent sequences (e.g., multiple audio channels).
357
- - **Resettable:** Create a new state to reset accumulated context. There is no
358
- explicit reset method -- just discard the old state and create fresh.
359
- - **Memory:** State holds model-specific internal buffers. Release it when the
360
- session ends to free memory.
350
+ - One prediction at a time per state. Being `Sendable` means a state may be
351
+ handed to another task or actor; overlapping calls on it are still unsafe,
352
+ and its buffers stay untouched until the current call returns.
353
+ - In plain serial code, such as the synchronous actor method above, call
354
+ `prediction(from:using:)` without `await`. In async code, or when you need
355
+ `MLPredictionOptions`, use `prediction(from:using:options:) async` (iOS 18);
356
+ still one call in flight per state.
357
+ - Concurrent sequences, such as several audio channels, get one
358
+ `makeState()` each.
359
+ - There is no reset call. Throw the state away and make a new one.
360
+ - A state holds buffers specific to its model; drop it once the session is over.
361
361
 
362
- ## Image Preprocessing
362
+ ## Pixel buffers
363
363
 
364
- ### CVPixelBuffer from CGImage
364
+ A throwing version of the conversion helper:
365
365
 
366
366
  ```swift
367
- import CoreVideo
368
367
  import CoreGraphics
368
+ import CoreVideo
369
369
 
370
- func createPixelBuffer(from cgImage: CGImage, width: Int, height: Int) throws -> CVPixelBuffer {
371
- let attrs: [CFString: Any] = [
372
- kCVPixelBufferCGImageCompatibilityKey: true,
373
- kCVPixelBufferCGBitmapContextCompatibilityKey: true,
374
- ]
370
+ enum FrameError: Error {
371
+ case bufferAllocation
372
+ case drawingContext
373
+ }
375
374
 
376
- var pixelBuffer: CVPixelBuffer?
377
- let status = CVPixelBufferCreate(
378
- kCFAllocatorDefault,
379
- width, height,
380
- kCVPixelFormatType_32ARGB,
381
- attrs as CFDictionary,
382
- &pixelBuffer
383
- )
384
- guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
385
- throw ImageError.pixelBufferCreationFailed
386
- }
375
+ func makeFrame(from picture: CGImage, width: Int, height: Int) throws -> CVPixelBuffer {
376
+ let attributes = [
377
+ kCVPixelBufferCGImageCompatibilityKey: true,
378
+ kCVPixelBufferCGBitmapContextCompatibilityKey: true
379
+ ] as CFDictionary
380
+ var created: CVPixelBuffer?
381
+ let format = kCVPixelFormatType_32ARGB
382
+ guard CVPixelBufferCreate(nil, width, height, format, attributes, &created) == kCVReturnSuccess,
383
+ let frame = created else {
384
+ throw FrameError.bufferAllocation
385
+ }
386
+
387
+ CVPixelBufferLockBaseAddress(frame, [])
388
+ defer { CVPixelBufferUnlockBaseAddress(frame, []) }
389
+
390
+ let base = CVPixelBufferGetBaseAddress(frame)
391
+ let stride = CVPixelBufferGetBytesPerRow(frame)
392
+ let layout = CGImageAlphaInfo.noneSkipFirst.rawValue
393
+ let rgb = CGColorSpaceCreateDeviceRGB()
394
+ guard let canvas = CGContext(data: base, width: width, height: height, bitsPerComponent: 8,
395
+ bytesPerRow: stride, space: rgb, bitmapInfo: layout) else {
396
+ throw FrameError.drawingContext
397
+ }
398
+ canvas.draw(picture, in: CGRect(origin: .zero, size: CGSize(width: width, height: height)))
399
+ return frame
400
+ }
401
+ ```
387
402
 
388
- CVPixelBufferLockBaseAddress(buffer, [])
389
- defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
403
+ From a `CIImage`, create a `kCVPixelFormatType_32BGRA` buffer and render into it
404
+ with `CIContext.render(_:to:)`:
390
405
 
391
- guard let context = CGContext(
392
- data: CVPixelBufferGetBaseAddress(buffer),
393
- width: width,
394
- height: height,
395
- bitsPerComponent: 8,
396
- bytesPerRow: CVPixelBufferGetBytesPerRow(buffer),
397
- space: CGColorSpaceCreateDeviceRGB(),
398
- bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue
399
- ) else {
400
- throw ImageError.contextCreationFailed
401
- }
402
-
403
- context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
404
- return buffer
405
- }
406
+ ```swift
407
+ import CoreImage
408
+ import CoreVideo
406
409
 
407
- enum ImageError: Error {
408
- case pixelBufferCreationFailed
409
- case contextCreationFailed
410
+ func makeFrame(from image: CIImage, renderer: CIContext) -> CVPixelBuffer? {
411
+ var created: CVPixelBuffer?
412
+ CVPixelBufferCreate(kCFAllocatorDefault, Int(image.extent.width), Int(image.extent.height),
413
+ kCVPixelFormatType_32BGRA, nil, &created)
414
+ guard let frame = created else { return nil }
415
+ renderer.render(image, to: frame)
416
+ return frame
410
417
  }
411
418
  ```
412
419
 
413
- For `CIImage` sources, use `CIContext.render(_:to:)` into a `CVPixelBuffer`
414
- created with `kCVPixelFormatType_32BGRA`.
420
+ Center-crop before scaling when the model expects a square and the photo is
421
+ not: `image.cropped(to:)` with a square rect centered on `image.extent`, then
422
+ scale to the model's side length. Per-channel normalization is shown in the
423
+ tensor section.
415
424
 
416
- ## MLMultiArray Creation and Manipulation
425
+ ## MLMultiArray
417
426
 
418
427
  ```swift
419
428
  import CoreML
420
429
 
421
- let array = try MLMultiArray(shape: [1, 3, 224, 224], dataType: .float32)
422
- for i in 0..<array.count { array[i] = NSNumber(value: Float.random(in: 0...1)) }
423
-
424
- let values: [Float] = [1.0, 2.0, 3.0, 4.0, 5.0]
425
- let mlArray = try MLMultiArray(values)
430
+ func planarImageTensor() throws -> MLMultiArray {
431
+ let planes = try MLMultiArray(shape: [1, 3, 256, 256], dataType: .float32)
432
+ for offset in 0..<planes.count {
433
+ planes[offset] = NSNumber(value: Float(offset % 255) / 255)
434
+ }
435
+ return planes
436
+ }
426
437
 
427
- // Access elements
428
- let element = array[[0, 0, 112, 112] as [NSNumber]].floatValue
438
+ func features(from values: [Float]) throws -> MLFeatureValue {
439
+ let array = try MLMultiArray(values)
440
+ return MLFeatureValue(multiArray: array)
441
+ }
429
442
 
430
- // Convert to Swift array
431
- func toFloatArray(_ multiArray: MLMultiArray) -> [Float] {
432
- let pointer = multiArray.dataPointer.assumingMemoryBound(to: Float.self)
433
- return Array(UnsafeBufferPointer(start: pointer, count: multiArray.count))
443
+ func pixelValue(_ planes: MLMultiArray, channel: Int, row: Int, column: Int) -> Float {
444
+ planes[[0, channel, row, column] as [NSNumber]].floatValue
434
445
  }
435
446
 
436
- let featureValue = MLFeatureValue(multiArray: array)
447
+ func floats(from array: MLMultiArray) -> [Float] {
448
+ MLShapedArray<Float>(array).scalars
449
+ }
437
450
  ```
438
451
 
439
- ## MLTensor Advanced Operations (iOS 18+)
452
+ Linear subscripts (`planes[offset]`) walk the whole buffer; index arrays
453
+ address one element. Converting through `MLShapedArray<Float>` avoids raw
454
+ pointer work; if you do use `dataPointer.assumingMemoryBound(to: Float.self)`
455
+ with `UnsafeBufferPointer`, keep the array alive for the whole access.
456
+
457
+ Tokenized text arrives as an `[Int32]` of token ids shaped `[1, sequence]`;
458
+ audio features as `[1, frames, bins]`. Both follow the same create, fill,
459
+ wrap-in-`MLFeatureValue(multiArray:)` pattern.
460
+
461
+ ## MLTensor (iOS 18+)
440
462
 
441
463
  ```swift
442
464
  import CoreML
443
465
 
444
- // Creation patterns
445
- let tensor1D = MLTensor([1.0, 2.0, 3.0, 4.0])
446
- let zeros = MLTensor(zeros: [3, 224, 224], scalarType: Float.self)
447
- let ones = MLTensor(ones: [2, 2], scalarType: Float.self)
448
-
449
- // Reshaping
450
- let reshaped = tensor1D.reshaped(to: [2, 2])
451
- let expanded = tensor1D.expandingShape(at: 0) // [1, 4]
466
+ func tensorTour() async -> (MLShapedArray<Float>, MLMultiArray) {
467
+ let readings = MLTensor([1.5, 3.0, 4.5, 6.0, 7.5, 9.0] as [Float])
468
+ let blank = MLTensor(zeros: [2, 3], scalarType: Float.self)
469
+ let unit = MLTensor(ones: [2, 3], scalarType: Float.self)
470
+
471
+ let square = readings.reshaped(to: [2, 3])
472
+ let row = readings.expandingShape(at: 0)
473
+ let summed = square + unit + blank
474
+ let average = readings.mean()
475
+ let winner = readings.argmax()
476
+ let odds = row.softmax(alongAxis: -1)
477
+ let joined = MLTensor(concatenating: [square, summed], alongAxis: 0)
478
+
479
+ _ = (average, winner, joined)
480
+ let materialized = await odds.shapedArray(of: Float.self)
481
+ return (materialized, MLMultiArray(materialized))
482
+ }
452
483
 
453
- // Arithmetic and reduction
454
- let sum = tensor1D + ones.reshaped(to: [4])
455
- let mean = tensor1D.mean()
456
- let argmax = tensor1D.argmax()
484
+ func imageNetNormalized(_ pixels: MLTensor) -> MLTensor {
485
+ let mean = MLTensor([0.485, 0.456, 0.406] as [Float]).reshaped(to: [1, 3, 1, 1])
486
+ let spread = MLTensor([0.229, 0.224, 0.225] as [Float]).reshaped(to: [1, 3, 1, 1])
487
+ return (pixels - mean) / spread
488
+ }
457
489
 
458
- // Activation functions
459
- let softmaxed = tensor1D.softmax(alongAxis: -1)
490
+ func tensorFromArray(_ array: MLMultiArray) -> MLTensor {
491
+ MLTensor(MLShapedArray<Float>(array))
492
+ }
493
+ ```
460
494
 
461
- // Interop with MLShapedArray / MLMultiArray
462
- let shaped = await tensor1D.shapedArray(of: Float.self) // MLShapedArray<Float>
463
- let multiArray = try MLMultiArray(shaped)
464
- let shapedAgain = MLShapedArray<Float>(multiArray)
495
+ `expandingShape(at: 0)` turns shape `[6]` into `[1, 6]`. Values only exist
496
+ after `await shapedArray(of:)`. Keep to operations Apple documents: no
497
+ `MLTensor(multiArray)`, no `std()` or `standardDeviation()`, no peeking into
498
+ the lazy storage and no blocking read, unless Apple's documentation names the
499
+ exact API and its availability. A standard deviation can be built from documented parts:
465
500
 
466
- // Concatenation
467
- let a = MLTensor([1.0, 2.0])
468
- let b = MLTensor([3.0, 4.0])
469
- let concatenated = MLTensor(concatenating: [a, b], alongAxis: 0)
501
+ ```swift
502
+ import CoreML
470
503
 
471
- // Normalization pattern (e.g., ImageNet preprocessing)
472
- func normalize(_ tensor: MLTensor, mean: [Float], std: [Float]) -> MLTensor {
473
- let meanTensor = MLTensor(mean)
474
- let stdTensor = MLTensor(std)
475
- return (tensor - meanTensor) / stdTensor
504
+ func spread(of values: MLTensor) -> MLTensor {
505
+ let centered = values - values.mean()
506
+ return (centered * centered).mean().squareRoot()
476
507
  }
477
508
  ```
478
509
 
479
- ## Vision + Core ML Pipelines
510
+ ## Vision and Core ML together
480
511
 
481
- ### Modern API (iOS 18+)
512
+ ### Swift-native Vision (iOS 18+)
482
513
 
483
514
  ```swift
484
- import Vision
485
515
  import CoreML
516
+ import Observation
517
+ import Vision
486
518
 
487
- @MainActor
488
519
  @Observable
489
- final class ObjectDetectionViewModel {
490
- var detections: [Detection] = []
491
- var isProcessing = false
492
-
493
- struct Detection: Identifiable {
520
+ @MainActor
521
+ final class ShelfDetector {
522
+ struct Finding: Identifiable {
494
523
  let id = UUID()
495
- let label: String
496
- let confidence: Float
497
- let boundingBox: CGRect
524
+ let name: String
525
+ let score: Float
526
+ let box: NormalizedRect
498
527
  }
499
528
 
500
- func detect(in image: CGImage) async {
501
- isProcessing = true
502
- defer { isProcessing = false }
529
+ var findings: [Finding] = []
530
+ private let detector: MLModel
531
+
532
+ init(detector: MLModel) {
533
+ self.detector = detector
534
+ }
503
535
 
536
+ func scan(_ photo: CGImage) async {
504
537
  do {
505
- let config = MLModelConfiguration()
506
- config.computeUnits = .all
507
-
508
- let detector = try MyObjectDetector(configuration: config)
509
- let request = CoreMLRequest(model: .init(detector.model))
510
-
511
- let results = try await request.perform(on: image)
512
- detections = results.compactMap { observation in
513
- guard let object = observation as? RecognizedObjectObservation,
514
- let topLabel = object.labels.first else { return nil }
515
- return Detection(
516
- label: topLabel.identifier,
517
- confidence: topLabel.confidence,
518
- boundingBox: object.boundingBox
519
- )
538
+ let request = CoreMLRequest(model: try CoreMLModelContainer(model: detector))
539
+ let raw = try await request.perform(on: photo)
540
+ findings = raw.compactMap { item in
541
+ guard let object = item as? RecognizedObjectObservation,
542
+ let best = object.labels.first else { return nil }
543
+ return Finding(name: best.identifier, score: best.confidence, box: object.boundingBox)
520
544
  }
521
545
  } catch {
522
- detections = []
546
+ findings = []
523
547
  }
524
548
  }
525
549
  }
526
- ```
527
-
528
- ```swift
529
- func classifyImage(_ image: CGImage) async throws -> [(label: String, confidence: Float)] {
530
- let classifier = try MyImageClassifier(configuration: .init())
531
- let request = CoreMLRequest(model: .init(classifier.model))
532
550
 
533
- let results = try await request.perform(on: image)
534
- return results.compactMap { observation in
535
- guard let classification = observation as? ClassificationObservation else { return nil }
536
- return (classification.identifier, classification.confidence)
537
- }
551
+ func topClasses(in photo: CGImage, model: MLModel) async throws -> [(String, Float)] {
552
+ let request = CoreMLRequest(model: try CoreMLModelContainer(model: model))
553
+ return try await request.perform(on: photo)
554
+ .compactMap { $0 as? ClassificationObservation }
555
+ .map { ($0.identifier, $0.confidence) }
538
556
  }
539
557
  ```
540
558
 
541
- ### Legacy API (Pre-iOS 18)
559
+ With a generated class, pass `generated.model` into `CoreMLModelContainer`.
560
+
561
+ ### Legacy Vision (before iOS 18)
542
562
 
543
563
  ```swift
544
- import Vision
545
564
  import CoreML
565
+ import Vision
546
566
 
547
- func detectLegacy(in image: CGImage) async throws -> [VNRecognizedObjectObservation] {
548
- let config = MLModelConfiguration()
549
- config.computeUnits = .all
550
-
551
- let detector = try MyObjectDetector(configuration: config)
552
- let vnModel = try VNCoreMLModel(for: detector.model)
567
+ actor LegacyShelfDetector {
568
+ private let visionModel: VNCoreMLModel
553
569
 
554
- let request = VNCoreMLRequest(model: vnModel)
555
- request.imageCropAndScaleOption = .scaleFill
570
+ init(model: MLModel) throws {
571
+ visionModel = try VNCoreMLModel(for: model)
572
+ }
556
573
 
557
- let handler = VNImageRequestHandler(cgImage: image)
558
- return try await Task.detached {
559
- try handler.perform([request])
560
- return request.results as? [VNRecognizedObjectObservation] ?? []
561
- }.value
574
+ func objects(in photo: CGImage) throws -> [(label: String, confidence: Float, box: CGRect)] {
575
+ let request = VNCoreMLRequest(model: visionModel)
576
+ request.imageCropAndScaleOption = .scaleFill
577
+ try VNImageRequestHandler(cgImage: photo).perform([request])
578
+ let found = request.results as? [VNRecognizedObjectObservation] ?? []
579
+ return found.compactMap { object in
580
+ object.labels.first.map { ($0.identifier, $0.confidence, object.boundingBox) }
581
+ }
582
+ }
562
583
  }
563
- ```
564
584
 
565
- ```swift
566
- func classifyImageLegacy(_ image: CGImage) async throws -> [(label: String, confidence: Float)] {
567
- let classifier = try MyImageClassifier(configuration: .init())
568
- let vnModel = try VNCoreMLModel(for: classifier.model)
569
- let request = VNCoreMLRequest(model: vnModel)
585
+ func legacyTopFive(in photo: CGImage, model: MLModel) throws -> [VNClassificationObservation] {
586
+ let request = VNCoreMLRequest(model: try VNCoreMLModel(for: model))
570
587
  request.imageCropAndScaleOption = .centerCrop
571
-
572
- let handler = VNImageRequestHandler(cgImage: image)
573
- return try await Task.detached {
574
- try handler.perform([request])
575
- guard let results = request.results as? [VNClassificationObservation] else { return [] }
576
- return results.prefix(5).map { ($0.identifier, $0.confidence) }
577
- }.value
588
+ try VNImageRequestHandler(cgImage: photo).perform([request])
589
+ let ranked = request.results as? [VNClassificationObservation] ?? []
590
+ return Array(ranked.prefix(5))
578
591
  }
579
592
  ```
580
593
 
581
- ## NaturalLanguage Integration
594
+ The legacy handler blocks while it works, so it runs inside an actor (or a
595
+ `Task.detached` whose captures are all `Sendable`) rather than on the main
596
+ actor. Vision observation classes are not `Sendable`; convert them to plain
597
+ values before they leave the actor. `.scaleFill` stretches the whole image into the model's input; `.centerCrop`
598
+ keeps the middle square, which suits most classifiers.
599
+
600
+ ## NaturalLanguage models
582
601
 
583
- Use `NLModel` to load Core ML models trained for NLP tasks.
602
+ Text classifiers trained with Create ML load through `NLModel`:
584
603
 
585
604
  ```swift
605
+ import CoreML
586
606
  import NaturalLanguage
587
607
 
588
- func analyzeSentiment(text: String) throws -> (label: String, confidence: Double)? {
589
- let modelURL = Bundle.main.url(forResource: "SentimentClassifier", withExtension: "mlmodelc")!
590
- let nlModel = try NLModel(contentsOf: modelURL)
591
-
592
- guard let label = nlModel.predictedLabel(for: text) else { return nil }
593
- let hypotheses = nlModel.predictedLabelHypotheses(for: text, maximumCount: 1)
594
- let confidence = hypotheses[label] ?? 0
595
- return (label, confidence)
608
+ func ticketCategory(for message: String, modelURL: URL) throws -> (String, Double)? {
609
+ let classifier = try NLModel(contentsOf: modelURL)
610
+ guard let category = classifier.predictedLabel(for: message) else { return nil }
611
+ let score = classifier.predictedLabelHypotheses(for: message, maximumCount: 1)[category] ?? 0
612
+ return (category, score)
596
613
  }
597
614
  ```
598
615
 
599
- ## MLComputePlan Detailed Usage (iOS 17.4+)
616
+ ## MLComputePlan in detail (iOS 17.4+)
617
+
618
+ A logging pass over every operation of an ML Program, run with the compute
619
+ units you intend to ship:
600
620
 
601
621
  ```swift
602
622
  import CoreML
603
-
604
- func profileModel(at url: URL) async throws {
605
- let config = MLModelConfiguration()
606
- config.computeUnits = .all
607
- let computePlan = try await MLComputePlan.load(contentsOf: url, configuration: config)
608
-
609
- guard case let .program(program) = computePlan.modelStructure,
610
- let mainFunction = program.functions["main"] else {
611
- print("Model is not an ML program or has no main function")
623
+ import OSLog
624
+
625
+ func auditDispatch(of packageURL: URL, units: MLComputeUnits) async throws {
626
+ let log = Logger(subsystem: "app.inference", category: "compute-plan")
627
+ let settings = MLModelConfiguration()
628
+ settings.computeUnits = units
629
+ let plan = try await MLComputePlan.load(contentsOf: packageURL, configuration: settings)
630
+ guard case .program(let program) = plan.modelStructure else {
631
+ log.info("Per-operation dispatch is reported only for ML Program models")
612
632
  return
613
633
  }
614
-
615
- for operation in mainFunction.block.operations {
616
- let opName = operation.operatorName
617
- if let deviceUsage = computePlan.deviceUsage(for: operation) {
618
- print(" \(opName): \(deviceUsage.preferred)")
619
- }
620
- if let cost = computePlan.estimatedCost(of: operation) {
621
- print(" Estimated weight: \(cost.weight)")
622
- }
634
+ guard let entry = program.functions["main"] else {
635
+ log.info("No main function in the program")
636
+ return
637
+ }
638
+ for op in entry.block.operations {
639
+ let device = plan.deviceUsage(for: op).map { "\($0.preferred)" } ?? "unknown"
640
+ let weight = plan.estimatedCost(of: op)?.weight ?? 0
641
+ log.debug("\(op.operatorName, privacy: .public) on \(device, privacy: .public) weight \(weight)")
623
642
  }
624
643
  }
625
644
  ```
626
645
 
627
- ### Interpreting MLComputePlan Results
646
+ Reading the result:
628
647
 
629
- | Device | Meaning | Action |
630
- |---|---|---|
631
- | Neural Engine | Best efficiency and speed for supported ops | Ideal -- no changes needed |
632
- | GPU | Runs on Metal GPU | Good for large matrix ops |
633
- | CPU | Fallback for unsupported operations | Investigate if many ops fall here |
634
-
635
- If many critical operations fall back to CPU, try `.cpuAndGPU` compute units,
636
- check for unsupported ANE operations, or re-convert with a different deployment target.
648
+ - **Neural Engine**: the fastest and most efficient place; nothing to do.
649
+ - **GPU**: runs through Metal; good for large matrix work.
650
+ - **CPU**: the fallback for operations the accelerators cannot run. A handful
651
+ is normal; many, especially heavy ones, deserve investigation.
637
652
 
638
- ## Background Model Loading and App Lifecycle
653
+ If critical operations land on the CPU: try `.cpuAndGPU`, look up which
654
+ operations the Neural Engine does not support, or convert again with a
655
+ different minimum deployment target.
639
656
 
640
- Manage model loading and memory across app lifecycle transitions.
657
+ ## Lifecycle: warm up and release
641
658
 
642
659
  ```swift
643
- @MainActor
660
+ import SwiftUI
661
+ import Observation
662
+ import CoreML
663
+
644
664
  @Observable
645
- final class AppModelState {
646
- var isModelReady = false
647
- private let modelManager = ModelManager()
665
+ @MainActor
666
+ final class InferenceLifecycle {
667
+ private(set) var ready = false
648
668
 
649
- func warmup() async {
650
- do {
651
- let config = MLModelConfiguration()
652
- config.computeUnits = .all
653
- _ = try await modelManager.model(named: "MainClassifier", configuration: config)
654
- isModelReady = true
655
- } catch {
656
- isModelReady = false
657
- }
669
+ func warmUp() async {
670
+ ready = (try? await ModelStore.shared.prepare("DishClassifier")) != nil
658
671
  }
659
672
 
660
- func handleBackground() async {
661
- await modelManager.unloadAll()
662
- isModelReady = false
673
+ func enteredBackground() async {
674
+ await ModelStore.shared.releaseEverything()
675
+ ready = false
663
676
  }
664
677
  }
665
678
 
666
- // SwiftUI integration
667
- @main
668
- struct MyApp: App {
669
- @State private var modelState = AppModelState()
670
- @Environment(\.scenePhase) private var scenePhase
679
+ struct PantryApp: App {
680
+ @State private var lifecycle = InferenceLifecycle()
681
+ @Environment(\.scenePhase) private var phase
671
682
 
672
683
  var body: some Scene {
673
684
  WindowGroup {
674
- ContentView()
675
- .environment(modelState)
676
- .task { await modelState.warmup() }
677
- .onChange(of: scenePhase) { _, newPhase in
678
- Task {
679
- if newPhase == .background {
680
- await modelState.handleBackground()
681
- } else if newPhase == .active {
682
- await modelState.warmup()
683
- }
684
- }
685
+ Text(lifecycle.ready ? "Ready" : "Loading")
686
+ .environment(lifecycle)
687
+ .task { await lifecycle.warmUp() }
688
+ }
689
+ .onChange(of: phase) { _, newPhase in
690
+ Task {
691
+ switch newPhase {
692
+ case .background: await lifecycle.enteredBackground()
693
+ case .active: await lifecycle.warmUp()
694
+ default: break
685
695
  }
696
+ }
686
697
  }
687
698
  }
688
699
  }
689
700
  ```
690
701
 
691
- ## Error Handling
702
+ In a real app this type carries `@main`; it is omitted here so the snippet
703
+ stays self-contained.
704
+
705
+ ## Errors
692
706
 
693
707
  ```swift
694
708
  import CoreML
695
709
 
696
- func loadAndPredict(modelName: String, input: any MLFeatureProvider) async -> (any MLFeatureProvider)? {
697
- let config = MLModelConfiguration()
698
- config.computeUnits = .all
699
-
710
+ func guardedPrediction(_ features: MLFeatureProvider, resource: String) async -> MLFeatureProvider? {
711
+ guard let location = Bundle.main.url(forResource: resource, withExtension: "mlmodelc") else {
712
+ return nil
713
+ }
700
714
  do {
701
- guard let url = Bundle.main.url(forResource: modelName, withExtension: "mlmodelc") else {
702
- print("Model \(modelName) not found in bundle")
703
- return nil
704
- }
705
- let model = try await MLModel.load(contentsOf: url, configuration: config)
706
- return try model.prediction(from: input)
715
+ let engine = try await MLModel.load(contentsOf: location)
716
+ return try await engine.prediction(from: features)
707
717
  } catch {
708
- print("Model error: \(error)")
709
718
  return nil
710
719
  }
711
720
  }
712
721
  ```
713
722
 
714
- ### Common Error Types
715
-
716
- | Error | Cause | Fix |
723
+ | Symptom | Usual cause | Fix |
717
724
  |---|---|---|
718
- | `MLModel` file not found | Wrong bundle path or missing target membership | Verify file is in correct target |
719
- | Compilation failure | Corrupted `.mlpackage` or unsupported ops | Re-export from coremltools |
720
- | Input shape mismatch | Wrong image dimensions or tensor shape | Match model's expected input shape |
721
- | Out of memory | Model too large for device | Use smaller model or `.cpuOnly` compute |
722
- | Compute unit fallback | Ops unsupported on requested device | Use `.all` or check `MLComputePlan` |
725
+ | Model file not found | Wrong path, or the file is not a member of the target | Check target membership and the resource name |
726
+ | Compilation fails | Corrupt `.mlpackage` or operations the OS does not support | Export again from coremltools |
727
+ | Input shape mismatch | Image size or tensor shape differs from the model's description | Match `modelDescription.inputDescriptionsByName` |
728
+ | Memory exhausted | The device cannot hold the model | Smaller or compressed model, or `.cpuOnly` as a last resort |
729
+ | Unexpected CPU fallback | Operations unsupported on the chosen device | Stay on `.all` and inspect `MLComputePlan` |
723
730
 
724
- For MLTensor preprocessing, keep examples to source-confirmed operations. Do not
725
- use `MLTensor(multiArray)`, `tensor.std()`, `tensor.standardDeviation()`, direct
726
- lazy-buffer access, or synchronous extraction unless Apple documents that exact
727
- API and availability.
731
+ MLTensor code should use only operations confirmed in Apple's documentation
732
+ (see the tensor section); invented helpers compile nowhere.
728
733
 
729
- ## Testing Patterns
734
+ ## Tests with Swift Testing
730
735
 
731
736
  ```swift
732
- import Testing
733
737
  import CoreML
738
+ import Testing
734
739
 
735
- struct ModelLoadingTests {
736
- @Test func loadModelSucceeds() async throws {
737
- let config = MLModelConfiguration()
738
- config.computeUnits = .cpuOnly // CPU for test stability
739
- let model = try MyImageClassifier(configuration: config)
740
- #expect(model.model.modelDescription.inputDescriptionsByName.count > 0)
740
+ struct DishClassifierTests {
741
+ private func model(units: MLComputeUnits) async throws -> MLModel {
742
+ let url = try #require(Bundle.main.url(forResource: "DishClassifier", withExtension: "mlmodelc"))
743
+ let settings = MLModelConfiguration()
744
+ settings.computeUnits = units
745
+ return try await MLModel.load(contentsOf: url, configuration: settings)
741
746
  }
742
747
 
743
- @Test func predictionReturnsValidOutput() async throws {
744
- let config = MLModelConfiguration()
745
- config.computeUnits = .cpuOnly
746
-
747
- let model = try MyImageClassifier(configuration: config)
748
- let input = try createTestInput(width: 224, height: 224)
749
- let output = try model.prediction(input: input)
750
-
751
- #expect(!output.classLabel.isEmpty)
752
- #expect(output.classLabelProbs.values.allSatisfy { $0 >= 0 && $0 <= 1 })
748
+ private func blankFrame() throws -> CVPixelBuffer {
749
+ var created: CVPixelBuffer?
750
+ CVPixelBufferCreate(kCFAllocatorDefault, 224, 224, kCVPixelFormatType_32ARGB, nil, &created)
751
+ return try #require(created)
753
752
  }
754
753
 
755
- @Test func predictionLatencyUnderThreshold() async throws {
756
- let config = MLModelConfiguration()
757
- config.computeUnits = .all
758
-
759
- let model = try MyImageClassifier(configuration: config)
760
- let input = try createTestInput(width: 224, height: 224)
761
-
762
- _ = try model.prediction(input: input) // Warm up
763
-
764
- let start = ContinuousClock.now
765
- for _ in 0..<10 {
766
- _ = try model.prediction(input: input)
767
- }
768
- let avgMs = (ContinuousClock.now - start) / 10
754
+ @Test func loadsWithInputs() async throws {
755
+ let model = try await model(units: .cpuOnly)
756
+ #expect(!model.modelDescription.inputDescriptionsByName.isEmpty)
757
+ }
769
758
 
770
- #expect(avgMs < .milliseconds(50), "Average prediction time \(avgMs) exceeds 50ms")
759
+ @Test func producesProbabilities() async throws {
760
+ let model = try await model(units: .cpuOnly)
761
+ let verdict = try await model.prediction(from: photoInput(try blankFrame()))
762
+ let winner = verdict.featureValue(for: "classLabel")?.stringValue ?? ""
763
+ let probabilities = verdict.featureValue(for: "classLabelProbs")?.dictionaryValue.values.map { $0.doubleValue } ?? []
764
+ #expect(!winner.isEmpty)
765
+ #expect(probabilities.allSatisfy { (0...1).contains($0) })
771
766
  }
772
- }
773
767
 
774
- private func createTestPixelBuffer(width: Int, height: Int) throws -> CVPixelBuffer {
775
- var pixelBuffer: CVPixelBuffer?
776
- let status = CVPixelBufferCreate(
777
- kCFAllocatorDefault, width, height,
778
- kCVPixelFormatType_32ARGB, nil, &pixelBuffer
779
- )
780
- guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
781
- throw ImageError.pixelBufferCreationFailed
768
+ @Test func staysUnderLatencyBudget() async throws {
769
+ let model = try await model(units: .all)
770
+ let probe = try photoInput(try blankFrame())
771
+ _ = try await model.prediction(from: probe)
772
+ let clock = ContinuousClock()
773
+ let rounds = 10
774
+ let elapsed = try await clock.measure {
775
+ for _ in 0..<rounds {
776
+ _ = try await model.prediction(from: probe)
777
+ }
778
+ }
779
+ #expect(elapsed / rounds < .milliseconds(50))
782
780
  }
783
- return buffer
784
781
  }
785
782
  ```
786
783
 
787
- ## Memory Management Best Practices
788
-
789
- 1. **Unload on background.** Unload models when `scenePhase == .background`
790
- and reload on return to foreground. iOS reclaims memory aggressively.
791
- 2. **Choose compute units by context.** Use `.all` by default. Consider
792
- `.cpuOnly` only when profiling or app policy shows accelerator contention,
793
- thermal state, energy budget, deterministic testing, or a legitimate
794
- background execution constraint makes CPU the right tradeoff.
795
- Do not claim GPU or Neural Engine are categorically unavailable for every
796
- background-adjacent task; background behavior depends on app mode, suspension,
797
- system policy, thermal state, energy, and contention.
798
- 3. **Prefer compiled models.** `.mlmodelc` loads faster and uses less transient
799
- memory than compiling `.mlpackage` at runtime. If a model is downloaded as
800
- `.mlmodel` or `.mlpackage`, compile once with `MLModel.compileModel(at:)`,
801
- move the `.mlmodelc` out of Core ML's temporary location, and cache it by
802
- model version. Do not call `compileModel(at:)` on every launch for the same
803
- model.
804
- 4. **Validate on physical devices.** Measure model load, first prediction,
805
- repeated predictions, background/foreground transitions, and low-memory
806
- behavior on the lowest-memory supported device.
807
- 5. **Share model instances.** Use an actor (like `ModelManager` above) to
808
- ensure only one instance of each model exists.
809
- 6. **Release batch providers promptly.** Large `MLArrayBatchProvider` instances
810
- hold references to all input data.
784
+ Load and output tests pin `.cpuOnly` for repeatable results. The latency test
785
+ uses `.all`, discards one warm-up call, then averages ten timed runs against a
786
+ 50 ms budget; set the budget from the product requirement and run it on
787
+ device, not in the simulator.
788
+
789
+ ## Memory practices
790
+
791
+ - Unload when `scenePhase` becomes `.background` and reload in the foreground;
792
+ the system reclaims memory from background apps aggressively.
793
+ - Choose compute units by context: `.all` normally; `.cpuOnly` only when
794
+ profiling or policy shows contention, thermal limits, energy pressure,
795
+ deterministic testing or a real background-execution need. Whether a
796
+ backgrounded app gets the accelerators at all is decided by the system: its
797
+ execution mode, whether it is suspended, heat, battery and competing work.
798
+ - Ship or cache compiled `.mlmodelc`: it loads faster and needs less transient
799
+ memory than compiling at run time. Compile downloads once, move them out of
800
+ the temporary directory, key the cache by version, never recompile the same
801
+ model on every launch.
802
+ - Measure on real hardware, beginning with the device that has the least
803
+ RAM: model load, the first call, sustained calls, trips to the background
804
+ and back, and behavior under memory pressure.
805
+ - One instance per model, shared through an actor.
806
+ - Release large `MLArrayBatchProvider` values promptly; they hold every input
807
+ in the batch.