@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,668 +1,550 @@
1
- # AVKit Patterns
1
+ # AVKit patterns
2
2
 
3
- Advanced patterns for AVKit media playback beyond the main skill coverage.
3
+ Advanced playback recipes for iOS 26 and Swift 6.3.
4
4
 
5
5
  ## Contents
6
6
 
7
- - [Custom Player UI with AVPlayerLayer](#custom-player-ui-with-avplayerlayer)
8
- - [Interstitial Content](#interstitial-content)
9
- - [Background Playback](#background-playback)
10
- - [Error Handling](#error-handling)
11
- - [Observing Playback State](#observing-playback-state)
12
- - [Video Frame Analysis](#video-frame-analysis)
13
- - [HDR Content](#hdr-content)
14
- - [SwiftUI Player Manager](#swiftui-player-manager)
15
- - [AVPlayerViewController in UIViewControllerRepresentable](#avplayerviewcontroller-in-uiviewcontrollerrepresentable)
7
+ - [Custom player with AVPlayerLayer](#custom-player-with-avplayerlayer)
8
+ - [Interstitials](#interstitials)
9
+ - [Background playback](#background-playback)
10
+ - [Errors](#errors)
11
+ - [Observing playback](#observing-playback)
12
+ - [Video frame analysis](#video-frame-analysis)
13
+ - [HDR](#hdr)
14
+ - [SwiftUI player model](#swiftui-player-model)
15
+ - [Wrapping AVPlayerViewController for SwiftUI](#wrapping-avplayerviewcontroller-for-swiftui)
16
16
 
17
- ## Custom Player UI with AVPlayerLayer
17
+ ## Custom player with AVPlayerLayer
18
18
 
19
- When `AVPlayerViewController` does not meet your design requirements, build a
20
- custom player UI using `AVPlayerLayer` directly. You lose system transport
21
- controls but gain full control over the interface.
19
+ Drawing video through `AVPlayerLayer` means giving up the system transport
20
+ controls in exchange for complete control over the UI.
22
21
 
23
22
  ```swift
24
23
  import AVFoundation
25
24
  import UIKit
26
25
 
27
- final class CustomPlayerView: UIView {
28
- override class var layerClass: AnyClass { AVPlayerLayer.self }
26
+ @MainActor
27
+ final class FilmSurface: UIView {
28
+ override static var layerClass: AnyClass { AVPlayerLayer.self }
29
29
 
30
- var playerLayer: AVPlayerLayer { layer as! AVPlayerLayer }
30
+ var filmLayer: AVPlayerLayer {
31
+ layer as! AVPlayerLayer
32
+ }
31
33
 
32
34
  var player: AVPlayer? {
33
- get { playerLayer.player }
34
- set { playerLayer.player = newValue }
35
+ get { filmLayer.player }
36
+ set { filmLayer.player = newValue }
35
37
  }
36
38
 
37
- override init(frame: CGRect) {
38
- super.init(frame: frame)
39
- playerLayer.videoGravity = .resizeAspect
39
+ override init(frame rect: CGRect) {
40
+ super.init(frame: rect)
41
+ filmLayer.videoGravity = .resizeAspect
40
42
  }
41
43
 
42
- required init?(coder: NSCoder) {
43
- super.init(coder: coder)
44
- playerLayer.videoGravity = .resizeAspect
44
+ required init?(coder decoder: NSCoder) {
45
+ super.init(coder: decoder)
46
+ filmLayer.videoGravity = .resizeAspect
45
47
  }
46
48
  }
47
49
  ```
48
50
 
49
- ### Adding PiP to a Custom Player
51
+ The force cast is safe because `layerClass` guarantees the layer type.
50
52
 
51
- Use `AVPictureInPictureController` with the custom player layer:
53
+ ### Adding PiP to it
52
54
 
53
55
  ```swift
56
+ import AVFoundation
54
57
  import AVKit
58
+ import UIKit
55
59
 
56
- final class CustomPlayerController: UIViewController, AVPictureInPictureControllerDelegate {
57
- private let playerView = CustomPlayerView()
58
- private var pipController: AVPictureInPictureController?
60
+ @MainActor
61
+ final class CustomCinemaController: UIViewController, @preconcurrency AVPictureInPictureControllerDelegate {
62
+ private let surface = FilmSurface()
59
63
  private let player = AVPlayer()
64
+ private var pip: AVPictureInPictureController?
65
+ private let overlayControls = UIStackView()
60
66
 
61
- override func viewDidLoad() {
62
- super.viewDidLoad()
63
- view.addSubview(playerView)
64
- playerView.player = player
67
+ override func loadView() {
68
+ view = surface
69
+ }
65
70
 
71
+ func load(_ url: URL) {
72
+ surface.player = player
66
73
  if AVPictureInPictureController.isPictureInPictureSupported() {
67
- pipController = AVPictureInPictureController(playerLayer: playerView.playerLayer)
68
- pipController?.delegate = self
69
- pipController?.canStartPictureInPictureAutomaticallyFromInline = true
74
+ let controller = AVPictureInPictureController(playerLayer: surface.filmLayer)
75
+ controller?.delegate = self
76
+ controller?.canStartPictureInPictureAutomaticallyFromInline = true
77
+ pip = controller
70
78
  }
71
- }
72
-
73
- func play(url: URL) {
74
- player.replaceCurrentItem(with: AVPlayerItem(url: url))
79
+ let episode = AVPlayerItem(url: url)
80
+ player.replaceCurrentItem(with: episode)
75
81
  player.play()
76
82
  }
77
83
 
78
- @IBAction func userTappedPictureInPictureButton(_ sender: UIButton) {
79
- guard pipController?.isPictureInPicturePossible == true else { return }
80
- pipController?.startPictureInPicture()
84
+ @objc func pipButtonTapped() {
85
+ guard let pip, pip.isPictureInPicturePossible else { return }
86
+ pip.startPictureInPicture()
81
87
  }
82
88
 
83
- // MARK: - AVPictureInPictureControllerDelegate
84
-
85
- func pictureInPictureControllerWillStartPictureInPicture(
86
- _ pictureInPictureController: AVPictureInPictureController
87
- ) {
88
- // Prepare UI for PiP (e.g., hide custom controls)
89
+ func pictureInPictureControllerWillStartPictureInPicture(_ controller: AVPictureInPictureController) {
90
+ overlayControls.isHidden = true
89
91
  }
90
92
 
91
- func pictureInPictureControllerDidStopPictureInPicture(
92
- _ pictureInPictureController: AVPictureInPictureController
93
- ) {
94
- // Restore custom controls
93
+ func pictureInPictureControllerDidStopPictureInPicture(_ controller: AVPictureInPictureController) {
94
+ overlayControls.isHidden = false
95
95
  }
96
96
 
97
- func pictureInPictureController(
98
- _ pictureInPictureController: AVPictureInPictureController,
99
- restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
100
- ) {
101
- // Re-present the player view controller
102
- completionHandler(true)
97
+ func pictureInPictureController(_ controller: AVPictureInPictureController,
98
+ restoreUserInterfaceForPictureInPictureStopWithCompletionHandler finish: @escaping (Bool) -> Void) {
99
+ finish(true)
103
100
  }
104
101
  }
105
102
  ```
106
103
 
107
- ### PiP with Sample Buffers
104
+ - Create the PiP controller only when the device supports PiP.
105
+ - Set its delegate and the automatic-inline flag, then load the item and play.
106
+ - The button action re-checks `isPictureInPicturePossible` before starting.
107
+ - Hide custom controls when PiP starts, show them again when it stops, and
108
+ always call `completionHandler(true)` in the restore callback.
108
109
 
109
- For apps rendering video frames manually (e.g., from a custom pipeline), use
110
- the sample buffer content source:
110
+ ### PiP from sample buffers
111
+
112
+ When the app renders frames itself into an `AVSampleBufferDisplayLayer`, build
113
+ the controller from a content source:
111
114
 
112
115
  ```swift
116
+ import AVFoundation
113
117
  import AVKit
118
+ import CoreMedia
114
119
 
115
- func setupSampleBufferPiP(
116
- displayLayer: AVSampleBufferDisplayLayer,
117
- delegate: AVPictureInPictureSampleBufferPlaybackDelegate
118
- ) -> AVPictureInPictureController? {
119
- guard AVPictureInPictureController.isPictureInPictureSupported() else { return nil }
120
-
121
- let contentSource = AVPictureInPictureController.ContentSource(
122
- sampleBufferDisplayLayer: displayLayer,
123
- playbackDelegate: delegate
124
- )
125
- let controller = AVPictureInPictureController(contentSource: contentSource)
126
- return controller
127
- }
128
- ```
129
-
130
- Only show a custom PiP start affordance after device support is known. When the
131
- user taps that affordance, check the controller's `isPictureInPicturePossible`
132
- because support can still be false for the current layer, item, media format,
133
- or playback state. Start custom PiP only in response to user interaction.
134
-
135
- The playback delegate must report timing and play/pause state:
120
+ @MainActor
121
+ final class RemoteDesktopPiP: NSObject, @preconcurrency AVPictureInPictureSampleBufferPlaybackDelegate {
122
+ let frameLayer = AVSampleBufferDisplayLayer()
123
+ private(set) var pip: AVPictureInPictureController?
124
+ private var paused = false
136
125
 
137
- ```swift
138
- final class SampleBufferPlaybackHandler: NSObject, AVPictureInPictureSampleBufferPlaybackDelegate {
139
- var isPlaying = true
140
- var currentTime: CMTime = .zero
141
- var duration: CMTime = CMTime(seconds: 300, preferredTimescale: 600)
126
+ func prepare() {
127
+ if !AVPictureInPictureController.isPictureInPictureSupported() { return }
128
+ let source = AVPictureInPictureController.ContentSource(sampleBufferDisplayLayer: frameLayer, playbackDelegate: self)
129
+ pip = AVPictureInPictureController(contentSource: source)
130
+ }
142
131
 
143
- func pictureInPictureController(
144
- _ pictureInPictureController: AVPictureInPictureController,
145
- setPlaying playing: Bool
146
- ) {
147
- isPlaying = playing
132
+ func userTappedPiP() {
133
+ guard let pip, pip.isPictureInPicturePossible else { return }
134
+ pip.startPictureInPicture()
148
135
  }
149
136
 
150
- func pictureInPictureControllerTimeRangeForPlayback(
151
- _ pictureInPictureController: AVPictureInPictureController
152
- ) -> CMTimeRange {
153
- CMTimeRange(start: .zero, duration: duration)
137
+ func pictureInPictureController(_ controller: AVPictureInPictureController, setPlaying playing: Bool) {
138
+ paused = !playing
154
139
  }
155
140
 
156
- func pictureInPictureControllerIsPlaybackPaused(
157
- _ pictureInPictureController: AVPictureInPictureController
158
- ) -> Bool {
159
- !isPlaying
141
+ func pictureInPictureControllerTimeRangeForPlayback(_ controller: AVPictureInPictureController) -> CMTimeRange {
142
+ CMTimeRange(start: .negativeInfinity, duration: .positiveInfinity)
160
143
  }
161
144
 
162
- func pictureInPictureController(
163
- _ pictureInPictureController: AVPictureInPictureController,
164
- didTransitionToRenderSize newRenderSize: CMVideoDimensions
165
- ) {
166
- // Adjust rendering resolution for PiP window size
145
+ func pictureInPictureControllerIsPlaybackPaused(_ controller: AVPictureInPictureController) -> Bool {
146
+ paused
167
147
  }
168
148
 
169
- func pictureInPictureController(
170
- _ pictureInPictureController: AVPictureInPictureController,
171
- skipByInterval skipInterval: CMTime,
172
- completion: @escaping () -> Void
173
- ) {
174
- currentTime = CMTimeAdd(currentTime, skipInterval)
175
- completion()
149
+ func pictureInPictureController(_ controller: AVPictureInPictureController,
150
+ didTransitionToRenderSize newRenderSize: CMVideoDimensions) {}
151
+
152
+ func pictureInPictureController(_ controller: AVPictureInPictureController,
153
+ skipByInterval skipInterval: CMTime,
154
+ completion completionHandler: @escaping () -> Void) {
155
+ completionHandler()
176
156
  }
177
157
 
178
- func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(
179
- _ pictureInPictureController: AVPictureInPictureController
180
- ) -> Bool {
158
+ func pictureInPictureControllerShouldProhibitBackgroundAudioPlayback(_ controller: AVPictureInPictureController) -> Bool {
181
159
  false
182
160
  }
183
161
  }
184
162
  ```
185
163
 
186
- ## Interstitial Content
164
+ - Offer a PiP button only once support is confirmed.
165
+ - On tap, check `isPictureInPicturePossible` again; the layer, item, format or
166
+ playback state can still rule PiP out. Start only from the user's tap.
167
+ - The playback delegate must answer the system's questions about timing and
168
+ play state: `setPlaying`, the playable time range (an infinite range here, for
169
+ live content), whether playback is paused, render-size changes, skip requests
170
+ (call the completion), and whether background audio should be blocked.
187
171
 
188
- Use interstitials for ads, legal notices, and other timeline ranges with
189
- special playback rules. `AVPlayerViewController` can mark these ranges in the
190
- timeline and call delegate methods when interstitial playback begins and ends.
172
+ ## Interstitials
191
173
 
192
- ### Stream-Defined Interstitial Ranges
174
+ Interstitials cover ads, legal notices and any stretch of the timeline with
175
+ special rules. `AVPlayerViewController` marks them on the scrubber and tells its
176
+ delegate when one starts and ends.
193
177
 
194
178
  ```swift
179
+ import AVFoundation
195
180
  import AVKit
196
181
 
197
- func inspectInterstitials(for playerItem: AVPlayerItem) {
198
- for interstitial in playerItem.interstitialTimeRanges {
199
- let range = interstitial.timeRange
200
- print("Interstitial starts at \(range.start.seconds)s")
201
- }
182
+ @MainActor
183
+ func adBreaks(in item: AVPlayerItem) -> [CMTimeRange] {
184
+ item.interstitialTimeRanges.map(\.timeRange)
202
185
  }
203
- ```
204
-
205
- On iOS, valid scheduling sources are either stream-defined interstitials or
206
- app-created schedules through `AVPlayerInterstitialEventController`. For
207
- stream-defined breaks, the manifest or source media owns the schedule, and
208
- AVFoundation exposes it through `playerItem.interstitialTimeRanges` for
209
- inspection and delegate coordination. Do not assign to
210
- `playerItem.interstitialTimeRanges` directly as the scheduling recipe.
211
-
212
- ### App-Scheduled Interstitials
213
-
214
- For app-scheduled interstitial content, use `AVPlayerInterstitialEventController`
215
- from AVFoundation. Creating a controller schedule causes playback to ignore
216
- interstitial events present in the source media, so use it only when the app
217
- owns the schedule.
218
-
219
- ```swift
220
- import AVFoundation
221
-
222
- func setupInterstitialEvents(primaryPlayer: AVPlayer) {
223
- guard let primaryItem = primaryPlayer.currentItem else { return }
224
-
225
- let controller = AVPlayerInterstitialEventController(primaryPlayer: primaryPlayer)
226
-
227
- let adAsset = AVURLAsset(url: URL(string: "https://example.com/ad.m3u8")!)
228
- let adItem = AVPlayerItem(asset: adAsset)
229
186
 
230
- let event = AVPlayerInterstitialEvent(
231
- primaryItem: primaryItem,
232
- time: CMTime(seconds: 60, preferredTimescale: 600)
187
+ @MainActor
188
+ func scheduleMidroll(on player: AVPlayer, adURL: URL, at seconds: Double) -> AVPlayerInterstitialEventController? {
189
+ guard let main = player.currentItem else { return nil }
190
+ let controller = AVPlayerInterstitialEventController(primaryPlayer: player)
191
+ let midroll = AVPlayerInterstitialEvent(
192
+ primaryItem: main,
193
+ time: CMTime(seconds: seconds, preferredTimescale: 600)
233
194
  )
234
- event.templateItems = [adItem]
235
- event.restrictions = [.requiresPlaybackAtPreferredRateForAdvancement]
236
-
237
- controller.events = [event]
195
+ midroll.templateItems = [AVPlayerItem(asset: AVURLAsset(url: adURL))]
196
+ midroll.restrictions = [.requiresPlaybackAtPreferredRateForAdvancement]
197
+ controller.events = [midroll]
198
+ return controller
238
199
  }
239
- ```
240
-
241
- ### Delegate Callbacks for Interstitials
242
200
 
243
- ```swift
244
- func playerViewController(
245
- _ playerViewController: AVPlayerViewController,
246
- willPresent interstitial: AVInterstitialTimeRange
247
- ) {
248
- // About to play interstitial content - disable skip controls
249
- playerViewController.requiresLinearPlayback = true
250
- }
201
+ @MainActor
202
+ final class AdGate: NSObject, @preconcurrency AVPlayerViewControllerDelegate {
203
+ func playerViewController(_ viewer: AVPlayerViewController, willPresent adBreak: AVInterstitialTimeRange) {
204
+ viewer.requiresLinearPlayback = true
205
+ }
251
206
 
252
- func playerViewController(
253
- _ playerViewController: AVPlayerViewController,
254
- didPresent interstitial: AVInterstitialTimeRange
255
- ) {
256
- // Interstitial finished - re-enable seeking
257
- playerViewController.requiresLinearPlayback = false
207
+ func playerViewController(_ viewer: AVPlayerViewController, didPresent adBreak: AVInterstitialTimeRange) {
208
+ viewer.requiresLinearPlayback = false
209
+ }
258
210
  }
259
211
  ```
260
212
 
261
- ## Background Playback
262
-
263
- ### Info.plist Configuration
213
+ - Read stream-defined breaks by iterating `interstitialTimeRanges` and taking
214
+ each `timeRange`.
215
+ - On iOS there are two valid sources: interstitials defined in the stream, or
216
+ an `AVPlayerInterstitialEventController` schedule. Assigning
217
+ `interstitialTimeRanges` is not a scheduling mechanism.
218
+ - Once `AVPlayerInterstitialEventController(primaryPlayer:)` sets a schedule,
219
+ playback ignores interstitial events embedded in the source media. Use it only
220
+ when the app owns the ad schedule.
221
+ - Lock seeking in `willPresent` and unlock it in `didPresent`.
264
222
 
265
- Add the `audio` background mode:
266
-
267
- ```xml
268
- <key>UIBackgroundModes</key>
269
- <array>
270
- <string>audio</string>
271
- </array>
272
- ```
223
+ ## Background playback
273
224
 
274
- ### Runtime Behavior
225
+ - Info.plist: `UIBackgroundModes` array containing `audio`.
226
+ - Set the `.playback` category, activate when playback starts, and hold the
227
+ `AVPlayer` strongly for as long as audio should continue.
228
+ - iOS normally pauses video when its scene moves to the background unless the
229
+ video goes into PiP. Use PiP for video that keeps playing, and the background
230
+ audio mode for sound that should keep going once the app is no longer in front.
275
231
 
276
- Set the audio session category to `.playback`, activate it when playback begins,
277
- and keep a strong reference to the `AVPlayer` for as long as playback should
278
- continue. iOS generally pauses video when a scene backgrounds unless playback
279
- moves to PiP. Use PiP for background video continuity; use background audio mode
280
- for audio that should continue after the user leaves the app.
232
+ ## Errors
281
233
 
282
- ## Error Handling
234
+ ```swift
235
+ import AVFoundation
283
236
 
284
- ### Observing Player Item Status
237
+ enum PlaybackProblem {
238
+ case offline, timedOut, media(Error), unknown
239
+ }
285
240
 
286
- ```swift
287
- func observePlayerItem(_ item: AVPlayerItem) {
288
- let observation = item.observe(\.status) { item, _ in
289
- switch item.status {
290
- case .readyToPlay:
291
- // Safe to begin playback
292
- break
293
- case .failed:
294
- if let error = item.error as? NSError {
295
- handlePlaybackError(error)
241
+ @MainActor
242
+ func watchStatus(of item: AVPlayerItem, report: @escaping @MainActor (PlaybackProblem?) -> Void) -> NSKeyValueObservation {
243
+ item.observe(\.status, options: [.new]) { observed, _ in
244
+ MainActor.assumeIsolated {
245
+ switch observed.status {
246
+ case .readyToPlay:
247
+ report(nil)
248
+ case .failed:
249
+ report(classify(observed.error))
250
+ case .unknown:
251
+ return
252
+ @unknown default:
253
+ report(.unknown)
296
254
  }
297
- case .unknown:
298
- break
299
- @unknown default:
300
- break
301
255
  }
302
256
  }
303
257
  }
304
258
 
305
- func handlePlaybackError(_ error: NSError) {
306
- switch error.domain {
307
- case NSURLErrorDomain:
308
- // Network-related errors
309
- if error.code == NSURLErrorNotConnectedToInternet {
310
- // Show offline message
311
- } else if error.code == NSURLErrorTimedOut {
312
- // Offer retry
313
- }
314
- case AVFoundationErrorDomain:
315
- // AVFoundation-specific errors
316
- break
317
- default:
318
- break
259
+ func classify(_ error: Error?) -> PlaybackProblem {
260
+ guard let error = error as NSError? else { return .unknown }
261
+ switch (error.domain, error.code) {
262
+ case (NSURLErrorDomain, NSURLErrorNotConnectedToInternet): return .offline
263
+ case (NSURLErrorDomain, NSURLErrorTimedOut): return .timedOut
264
+ case (AVFoundationErrorDomain, _): return .media(error)
265
+ default: return .media(error)
319
266
  }
320
267
  }
321
- ```
322
268
 
323
- ### Observing Player Time Control Status
324
-
325
- ```swift
326
- func observePlayer(_ player: AVPlayer) {
327
- let observation = player.observe(\.timeControlStatus) { player, _ in
328
- switch player.timeControlStatus {
329
- case .paused:
330
- break
331
- case .playing:
332
- break
333
- case .waitingToPlayAtSpecifiedRate:
334
- if let reason = player.reasonForWaitingToPlay {
335
- switch reason {
336
- case .toMinimizeStalls:
337
- // Buffering - show spinner
338
- break
339
- case .evaluatingBufferingRate:
340
- break
341
- case .noItemToPlay:
342
- break
343
- default:
344
- break
345
- }
269
+ @MainActor
270
+ func watchStalls(of player: AVPlayer, spinner: @escaping @MainActor (Bool) -> Void) -> NSKeyValueObservation {
271
+ player.observe(\.timeControlStatus, options: [.new]) { observed, _ in
272
+ MainActor.assumeIsolated {
273
+ switch observed.timeControlStatus {
274
+ case .waitingToPlayAtSpecifiedRate:
275
+ spinner(observed.reasonForWaitingToPlay == .toMinimizeStalls)
276
+ case .playing, .paused:
277
+ spinner(false)
278
+ @unknown default:
279
+ spinner(false)
346
280
  }
347
- @unknown default:
348
- break
349
281
  }
350
282
  }
351
283
  }
352
284
  ```
353
285
 
354
- ### Handling AVKitError
286
+ - `AVPlayerItem.status` moves from `.unknown` to `.readyToPlay` or `.failed`;
287
+ on failure read `item.error`.
288
+ - Split network errors (`NSURLErrorDomain`: show an offline message for
289
+ `NSURLErrorNotConnectedToInternet`, offer a retry for `NSURLErrorTimedOut`)
290
+ from media errors in `AVFoundationErrorDomain`.
291
+ - `AVPlayer.timeControlStatus` is `.paused`, `.playing` or
292
+ `.waitingToPlayAtSpecifiedRate`. While waiting, `reasonForWaitingToPlay` says
293
+ why: `.toMinimizeStalls` (show a buffering indicator),
294
+ `.evaluatingBufferingRate` or `.noItemToPlay`.
355
295
 
356
- ```swift
357
- import AVKit
296
+ `AVKitError` codes:
358
297
 
359
- func handleAVKitError(_ error: Error) {
360
- guard let avkitError = error as? AVKitError else { return }
361
-
362
- switch avkitError.code {
363
- case .pictureInPictureStartFailed:
364
- // Check audio session/background mode, support, current possibility,
365
- // and whether the player layer or content source is active.
366
- break
367
- case .contentRatingUnknown:
368
- // Content rating could not be determined
369
- break
370
- case .contentDisallowedByPasscode:
371
- // Parental controls restrict this content
372
- break
373
- case .contentDisallowedByProfile:
374
- // MDM profile restricts this content
375
- break
376
- case .unknown:
377
- break
378
- @unknown default:
379
- break
380
- }
381
- }
382
- ```
298
+ | Code | Meaning |
299
+ |---|---|
300
+ | `.pictureInPictureStartFailed` | PiP could not start; verify the playback category, the `audio` background mode, device support, `isPictureInPicturePossible`, and that the layer or content source feeding PiP is still active |
301
+ | `.contentRatingUnknown` | the content's rating could not be determined |
302
+ | `.contentDisallowedByPasscode` | blocked by parental restrictions |
303
+ | `.contentDisallowedByProfile` | blocked by a device management profile |
304
+ | `.unknown` | unspecified failure |
383
305
 
384
- ## Observing Playback State
385
-
386
- ### Periodic Time Observer
306
+ ## Observing playback
387
307
 
388
308
  ```swift
389
- func addPeriodicObserver(to player: AVPlayer) -> Any {
390
- let interval = CMTime(seconds: 0.5, preferredTimescale: 600)
391
- return player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { time in
392
- let seconds = CMTimeGetSeconds(time)
393
- // Update progress UI
394
- }
395
- }
396
-
397
- // Remove when done
398
- // player.removeTimeObserver(observer)
399
- ```
400
-
401
- ### Boundary Time Observer
402
-
403
- Trigger actions at specific points in the timeline:
309
+ import AVFoundation
404
310
 
405
- ```swift
406
- func addBoundaryObserver(to player: AVPlayer, at times: [CMTime]) -> Any {
407
- let timeValues = times.map { NSValue(time: $0) }
408
- return player.addBoundaryTimeObserver(forTimes: timeValues, queue: .main) {
409
- // Reached a boundary time
311
+ @MainActor
312
+ final class ChapterClock {
313
+ private let player: AVPlayer
314
+ private var ticker: Any?
315
+ private var markers: Any?
316
+ private var endObserver: NSObjectProtocol?
317
+
318
+ init(player: AVPlayer) {
319
+ self.player = player
320
+ }
321
+
322
+ func start(chapters: [CMTime], onTick: @escaping @Sendable (CMTime) -> Void, onChapter: @escaping @Sendable () -> Void) {
323
+ let halfSecond = CMTime(value: 1, timescale: 2)
324
+ ticker = player.addPeriodicTimeObserver(forInterval: halfSecond,
325
+ queue: .main, using: onTick)
326
+ markers = player.addBoundaryTimeObserver(forTimes: chapters.map { NSValue(time: $0) },
327
+ queue: .main, using: onChapter)
328
+ let center = NotificationCenter.default
329
+ endObserver = center.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem,
330
+ queue: .main) { _ in onChapter() }
331
+ }
332
+
333
+ func stop() {
334
+ if let ticker { player.removeTimeObserver(ticker) }
335
+ if let markers { player.removeTimeObserver(markers) }
336
+ if let endObserver { NotificationCenter.default.removeObserver(endObserver) }
337
+ ticker = nil
338
+ markers = nil
339
+ endObserver = nil
410
340
  }
411
341
  }
412
342
  ```
413
343
 
414
- ### Observing Playback End
415
-
416
- ```swift
417
- func observePlaybackEnd(for item: AVPlayerItem) {
418
- NotificationCenter.default.addObserver(
419
- forName: .AVPlayerItemDidPlayToEndTime,
420
- object: item,
421
- queue: .main
422
- ) { _ in
423
- // Playback finished - show replay button or load next item
424
- }
425
- }
426
- ```
344
+ - `addPeriodicTimeObserver(forInterval:queue:using:)` returns a token; pass it
345
+ to `removeTimeObserver(_:)` when done.
346
+ - `addBoundaryTimeObserver(forTimes:queue:using:)` fires at the listed times,
347
+ wrapped with `NSValue(time:)`.
348
+ - The end of an item is announced with `.AVPlayerItemDidPlayToEndTime`, scoped
349
+ to that item.
427
350
 
428
- ## Video Frame Analysis
351
+ ## Video frame analysis
429
352
 
430
- `AVPlayerViewController` supports automatic analysis of paused video frames
431
- for text recognition, visual search, and subject lifting.
353
+ `allowsVideoFrameAnalysis` (default `true`, iOS 16+) lets the user interact
354
+ with a paused frame: select text, look things up, lift a subject. Narrow it with
355
+ `videoFrameAnalysisTypes` (iOS 17+), whose options include `.text`,
356
+ `.visualSearch`, `.subject` and `.machineReadableCode`, plus `.default`.
432
357
 
433
358
  ```swift
434
- let playerVC = AVPlayerViewController()
435
-
436
- // Enable frame analysis (default is true)
437
- playerVC.allowsVideoFrameAnalysis = true
359
+ import AVKit
438
360
 
439
- // Configure which analysis types to perform
440
- playerVC.videoFrameAnalysisTypes = [.text, .visualSearch, .subject, .machineReadableCode]
361
+ @MainActor
362
+ func onlyTextAndCodes(_ viewer: AVPlayerViewController) {
363
+ viewer.allowsVideoFrameAnalysis = true
364
+ viewer.videoFrameAnalysisTypes = [.text, .machineReadableCode]
365
+ }
441
366
  ```
442
367
 
443
- ## HDR Content
368
+ ## HDR
444
369
 
445
- Control how HDR video content renders:
370
+ `AVPlayerViewController.preferredDisplayDynamicRange` (iOS 26+) picks how HDR
371
+ content is shown:
446
372
 
447
- ```swift
448
- // Let the system decide the best rendering
449
- playerVC.preferredDisplayDynamicRange = .automatic
373
+ | Value | Behavior |
374
+ |---|---|
375
+ | `.automatic` | the system decides (default) |
376
+ | `.standard` | force SDR |
377
+ | `.high` | full HDR |
378
+ | `.constrainedHigh` | limited HDR, useful when HDR sits next to SDR content |
450
379
 
451
- // Force SDR rendering
452
- playerVC.preferredDisplayDynamicRange = .standard
453
-
454
- // Full HDR
455
- playerVC.preferredDisplayDynamicRange = .high
380
+ ```swift
381
+ import AVKit
456
382
 
457
- // Constrained HDR (tone-mapped HDR)
458
- playerVC.preferredDisplayDynamicRange = .constrainedHigh
383
+ @MainActor
384
+ func calmHDR(_ viewer: AVPlayerViewController) {
385
+ viewer.preferredDisplayDynamicRange = .constrainedHigh
386
+ }
459
387
  ```
460
388
 
461
- ## SwiftUI Player Manager
462
-
463
- An `@Observable` manager for coordinating playback state across a SwiftUI app:
389
+ ## SwiftUI player model
464
390
 
465
391
  ```swift
466
- import AVKit
467
392
  import AVFoundation
393
+ import AVKit
394
+ import Observation
395
+ import SwiftUI
468
396
 
469
397
  @Observable
470
398
  @MainActor
471
- final class VideoPlayerManager {
399
+ final class EpisodePlayer {
472
400
  private(set) var player: AVPlayer?
401
+ private(set) var position: Double = 0
473
402
  private(set) var isPlaying = false
474
- private(set) var currentTime: TimeInterval = 0
475
- private(set) var duration: TimeInterval = 0
476
403
  private(set) var isBuffering = false
477
- private(set) var error: Error?
478
-
479
- private var timeObserver: Any?
480
- private var statusObservation: NSKeyValueObservation?
481
- private var timeControlObservation: NSKeyValueObservation?
482
-
483
- func loadMedia(url: URL) {
484
- cleanup()
485
-
486
- let item = AVPlayerItem(url: url)
487
- let newPlayer = AVPlayer(playerItem: item)
488
- player = newPlayer
489
-
490
- observeStatus(of: item)
491
- observeTimeControl(of: newPlayer)
492
- addPeriodicTimeObserver(to: newPlayer)
493
- }
494
-
495
- func play() {
496
- player?.play()
404
+ private(set) var length: Double = 0
405
+ private(set) var failure: String?
406
+
407
+ @ObservationIgnored private var tick: Any?
408
+ @ObservationIgnored private var observations: [NSKeyValueObservation] = []
409
+
410
+ func load(_ url: URL) {
411
+ tearDown()
412
+ let item = AVPlayerItem(asset: AVURLAsset(url: url))
413
+ let fresh = AVPlayer(playerItem: item)
414
+ player = fresh
415
+
416
+ observations.append(item.observe(\.status) { [weak self] observed, _ in
417
+ let status = observed.status
418
+ let seconds = CMTimeGetSeconds(observed.duration)
419
+ let message = observed.error?.localizedDescription
420
+ Task { @MainActor [weak self] in
421
+ guard let model = self else { return }
422
+ if status == .readyToPlay { model.length = seconds.isFinite ? seconds : 0 }
423
+ if status == .failed { model.failure = message }
424
+ }
425
+ })
426
+ observations.append(fresh.observe(\.timeControlStatus) { [weak self] observed, _ in
427
+ let state = observed.timeControlStatus
428
+ Task { @MainActor in
429
+ self?.isPlaying = state == .playing
430
+ self?.isBuffering = state == .waitingToPlayAtSpecifiedRate
431
+ }
432
+ })
433
+ let every = CMTime(value: 1, timescale: 2)
434
+ tick = fresh.addPeriodicTimeObserver(forInterval: every, queue: .main) { [weak self] now in
435
+ let seconds = now.seconds
436
+ Task { @MainActor in self?.position = seconds }
437
+ }
497
438
  }
498
439
 
499
- func pause() {
500
- player?.pause()
440
+ func toggle() {
441
+ guard let player else { return }
442
+ isPlaying ? player.pause() : player.play()
501
443
  }
502
444
 
503
- func seek(to time: TimeInterval) {
504
- let cmTime = CMTime(seconds: time, preferredTimescale: 600)
505
- player?.seek(to: cmTime, toleranceBefore: .zero, toleranceAfter: .zero)
445
+ func jump(to seconds: Double) {
446
+ player?.seek(to: CMTime(seconds: seconds, preferredTimescale: 600), toleranceBefore: .zero, toleranceAfter: .zero)
506
447
  }
507
448
 
508
- func cleanup() {
509
- if let observer = timeObserver, let player {
510
- player.removeTimeObserver(observer)
511
- }
512
- timeObserver = nil
513
- statusObservation?.invalidate()
514
- timeControlObservation?.invalidate()
449
+ func tearDown() {
450
+ if let tick, let player { player.removeTimeObserver(tick) }
451
+ tick = nil
452
+ observations.forEach { $0.invalidate() }
453
+ observations.removeAll()
515
454
  player = nil
516
- error = nil
517
455
  isPlaying = false
518
- currentTime = 0
519
- duration = 0
456
+ isBuffering = false
457
+ position = 0
458
+ length = 0
459
+ failure = nil
520
460
  }
521
461
 
522
- private func observeStatus(of item: AVPlayerItem) {
523
- statusObservation = item.observe(\.status) { [weak self] item, _ in
524
- Task { @MainActor in
525
- guard let self else { return }
526
- switch item.status {
527
- case .readyToPlay:
528
- self.duration = CMTimeGetSeconds(item.duration)
529
- case .failed:
530
- self.error = item.error
531
- case .unknown:
532
- break
533
- @unknown default:
534
- break
535
- }
536
- }
537
- }
538
- }
539
-
540
- private func observeTimeControl(of player: AVPlayer) {
541
- timeControlObservation = player.observe(\.timeControlStatus) { [weak self] player, _ in
542
- Task { @MainActor in
543
- guard let self else { return }
544
- self.isPlaying = player.timeControlStatus == .playing
545
- self.isBuffering = player.timeControlStatus == .waitingToPlayAtSpecifiedRate
546
- }
547
- }
548
- }
549
-
550
- private func addPeriodicTimeObserver(to player: AVPlayer) {
551
- let interval = CMTime(seconds: 0.5, preferredTimescale: 600)
552
- timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
553
- Task { @MainActor in
554
- self?.currentTime = CMTimeGetSeconds(time)
555
- }
556
- }
557
- }
558
-
559
- deinit {
560
- if let observer = timeObserver, let player {
561
- player.removeTimeObserver(observer)
562
- }
462
+ isolated deinit {
463
+ if let tick, let player { player.removeTimeObserver(tick) }
563
464
  }
564
465
  }
565
- ```
566
-
567
- ### Using the Manager in SwiftUI
568
466
 
569
- ```swift
570
- import SwiftUI
571
- import AVKit
572
-
573
- struct VideoScreen: View {
574
- @State private var manager = VideoPlayerManager()
467
+ struct EpisodeScreen: View {
468
+ let episodeURL: URL
469
+ @State private var model = EpisodePlayer()
575
470
 
576
471
  var body: some View {
577
- VStack {
578
- if let player = manager.player {
579
- VideoPlayer(player: player)
580
- .frame(height: 300)
581
-
582
- HStack {
583
- Button(manager.isPlaying ? "Pause" : "Play") {
584
- manager.isPlaying ? manager.pause() : manager.play()
585
- }
586
-
587
- Text("\(Int(manager.currentTime))s / \(Int(manager.duration))s")
588
- }
472
+ VStack(spacing: 12) {
473
+ if let current = model.player {
474
+ VideoPlayer(player: current)
475
+ .aspectRatio(16 / 9, contentMode: .fit)
589
476
  }
590
-
591
- if manager.isBuffering {
592
- ProgressView("Buffering...")
477
+ HStack {
478
+ Button(model.isPlaying ? "Pause" : "Play") { model.toggle() }
479
+ Text("\(Int(model.position))s / \(Int(model.length))s").monospacedDigit()
480
+ if model.isBuffering { ProgressView() }
593
481
  }
594
-
595
- if let error = manager.error {
596
- Text("Error: \(error.localizedDescription)")
597
- .foregroundStyle(.red)
482
+ if let failure = model.failure {
483
+ Text(failure).foregroundStyle(.red)
598
484
  }
599
485
  }
600
- .task {
601
- manager.loadMedia(url: URL(string: "https://example.com/video.m3u8")!)
602
- }
486
+ .task { model.load(episodeURL) }
603
487
  }
604
488
  }
605
489
  ```
606
490
 
607
- ## AVPlayerViewController in UIViewControllerRepresentable
491
+ - `load(_:)` clears previous state, creates the item and player, and installs
492
+ the status observation, the `timeControlStatus` observation and a periodic
493
+ time observer.
494
+ - Seeking uses zero tolerance on both sides for frame-accurate jumps, with a
495
+ timescale of 600.
496
+ - `tearDown()` removes the time observer, invalidates each
497
+ `NSKeyValueObservation` and resets the published state; the isolated
498
+ `deinit` removes the time observer as well.
499
+ - Observer closures capture `self` weakly and hop to the main actor with
500
+ `Task { @MainActor in ... }`. Duration is read with `CMTimeGetSeconds` once
501
+ the item is ready; buffering is `.waitingToPlayAtSpecifiedRate`.
608
502
 
609
- Full-featured wrapper exposing delegate callbacks, PiP, and playback speed to
610
- SwiftUI. Prefer this over `VideoPlayer` when you need fine-grained control.
503
+ ## Wrapping AVPlayerViewController for SwiftUI
504
+
505
+ When the app needs the delegate, PiP settings or speed options, a representable
506
+ around `AVPlayerViewController` gives more control than `VideoPlayer`.
611
507
 
612
508
  ```swift
613
- import SwiftUI
614
509
  import AVKit
510
+ import SwiftUI
615
511
 
616
- struct SystemPlayerView: UIViewControllerRepresentable {
512
+ struct FullPlayer: UIViewControllerRepresentable {
617
513
  let player: AVPlayer
618
- var allowsPiP: Bool = true
619
- var autoStartPiPFromInline: Bool = false
620
- var speeds: [AVPlaybackSpeed] = AVPlaybackSpeed.systemDefaultSpeeds
514
+ var speeds = AVPlaybackSpeed.systemDefaultSpeeds
515
+ var allowsPiP = true
621
516
 
622
- func makeUIViewController(context: Context) -> AVPlayerViewController {
623
- let controller = AVPlayerViewController()
624
- controller.player = player
625
- controller.allowsPictureInPicturePlayback = allowsPiP
626
- controller.canStartPictureInPictureAutomaticallyFromInline = autoStartPiPFromInline
627
- controller.speeds = speeds
628
- controller.delegate = context.coordinator
629
- return controller
630
- }
517
+ func makeCoordinator() -> Coordinator { Coordinator() }
631
518
 
632
- func updateUIViewController(_ controller: AVPlayerViewController, context: Context) {
633
- controller.player = player
519
+ func makeUIViewController(context ctx: Context) -> AVPlayerViewController {
520
+ let viewer = AVPlayerViewController()
521
+ viewer.player = player
522
+ viewer.allowsPictureInPicturePlayback = allowsPiP
523
+ viewer.canStartPictureInPictureAutomaticallyFromInline = allowsPiP
524
+ viewer.speeds = speeds
525
+ viewer.delegate = ctx.coordinator
526
+ return viewer
634
527
  }
635
528
 
636
- func makeCoordinator() -> Coordinator { Coordinator() }
529
+ func updateUIViewController(_ viewer: AVPlayerViewController, context: Context) {
530
+ viewer.player = player
531
+ }
637
532
 
638
- final class Coordinator: NSObject, AVPlayerViewControllerDelegate {
639
- func playerViewController(
640
- _ playerViewController: AVPlayerViewController,
641
- willBeginFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator
642
- ) {
643
- // Handle full-screen entry
644
- }
533
+ @MainActor
534
+ final class Coordinator: NSObject, @preconcurrency AVPlayerViewControllerDelegate {
535
+ func playerViewController(_ viewer: AVPlayerViewController,
536
+ willBeginFullScreenPresentationWithAnimationCoordinator transition: any UIViewControllerTransitionCoordinator) {}
645
537
 
646
- func playerViewController(
647
- _ playerViewController: AVPlayerViewController,
648
- willEndFullScreenPresentationWithAnimationCoordinator coordinator: any UIViewControllerTransitionCoordinator
649
- ) {
650
- // Handle full-screen exit
651
- }
538
+ func playerViewController(_ viewer: AVPlayerViewController,
539
+ willEndFullScreenPresentationWithAnimationCoordinator transition: any UIViewControllerTransitionCoordinator) {}
652
540
 
653
- func playerViewController(
654
- _ playerViewController: AVPlayerViewController,
655
- restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
656
- ) {
657
- completionHandler(true)
541
+ func playerViewController(_ viewer: AVPlayerViewController,
542
+ restoreUserInterfaceForPictureInPictureStopWithCompletionHandler done: @escaping (Bool) -> Void) {
543
+ done(true)
658
544
  }
659
545
 
660
- func playerViewController(
661
- _ playerViewController: AVPlayerViewController,
662
- failedToStartPictureInPictureWithError error: any Error
663
- ) {
664
- print("PiP failed: \(error)")
665
- }
546
+ func playerViewController(_ viewer: AVPlayerViewController,
547
+ failedToStartPictureInPictureWithError reason: any Error) {}
666
548
  }
667
549
  }
668
550
  ```