@mmerterden/multi-agent-pipeline 20.6.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 (326) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +328 -329
  5. package/package.json +2 -2
  6. package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
  7. package/pipeline/scripts/_notices.mjs +12 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  9. package/pipeline/skills/.skill-manifest.json +106 -106
  10. package/pipeline/skills/shared/README.md +71 -71
  11. package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
  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/android-architecture/SKILL.md +2 -0
  16. package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
  17. package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
  18. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  19. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  20. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  21. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  22. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  23. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  24. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  25. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  26. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  27. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  28. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  29. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  30. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  31. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  32. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  33. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  34. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  35. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  37. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  38. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  39. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  40. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  41. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  42. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  43. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  44. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  45. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  46. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  47. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  48. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  49. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  50. package/pipeline/skills/shared/external/backlog/BACKLOG.md +1 -1
  51. package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
  52. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  53. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  54. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  55. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +1 -0
  56. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  57. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  58. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  59. package/pipeline/skills/shared/external/compose-components/SKILL.md +2 -0
  60. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
  61. package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
  62. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  63. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  64. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  65. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  66. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  67. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  68. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  69. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  70. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  71. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  72. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  73. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  74. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  75. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  76. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  77. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  78. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  79. package/pipeline/skills/shared/external/council/SKILL.md +1 -0
  80. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  81. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  82. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  83. package/pipeline/skills/shared/external/css-modern/SKILL.md +1 -0
  84. package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
  85. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  86. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  87. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  88. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  89. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  90. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  91. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  92. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  93. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  94. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  95. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  96. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  97. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  98. package/pipeline/skills/shared/external/evidence-github/SKILL.md +2 -0
  99. package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
  100. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
  101. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  102. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  103. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  104. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  105. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  106. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  107. package/pipeline/skills/shared/external/html-semantic/SKILL.md +1 -0
  108. package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
  109. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  110. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  111. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  112. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  113. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  114. package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +1 -0
  115. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  116. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  117. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  118. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  119. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +1 -0
  120. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  121. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  122. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  123. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  124. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  125. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  126. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  127. package/pipeline/skills/shared/external/ios-security/SKILL.md +2 -0
  128. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  129. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  130. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  131. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  132. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  133. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  134. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +91 -283
  135. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
  136. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
  137. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
  138. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
  139. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
  140. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
  141. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
  142. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
  143. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
  144. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
  145. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
  146. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
  147. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
  148. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
  149. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
  150. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  151. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  152. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  153. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  154. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  155. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  156. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  157. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  158. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  159. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  160. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  161. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  162. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  163. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +1 -0
  164. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  165. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  166. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  167. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  168. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  169. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  170. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  171. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  172. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  173. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  174. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  175. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  176. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  177. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  178. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  179. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  180. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  181. package/pipeline/skills/shared/external/play-store-review/SKILL.md +2 -0
  182. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  183. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  184. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  185. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  186. package/pipeline/skills/shared/external/python-patterns/SKILL.md +1 -0
  187. package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
  188. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  189. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  190. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  191. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +1 -0
  192. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
  193. package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
  194. package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
  195. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  196. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  197. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  198. package/pipeline/skills/shared/external/signal-community/SKILL.md +2 -0
  199. package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
  200. package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
  201. package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
  202. package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
  203. package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
  204. package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
  205. package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
  206. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  207. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  208. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  209. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  210. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  211. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  212. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  213. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  214. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  215. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  216. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  217. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  218. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  219. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  220. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  221. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  222. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  223. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  224. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  225. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  226. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  227. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  228. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  229. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  230. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  231. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  232. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  233. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  234. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  235. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  236. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  237. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  238. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  239. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  240. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  241. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  242. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  243. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  244. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  245. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  246. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  247. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  248. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  249. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  250. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  251. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  252. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  253. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  254. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  255. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  256. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  257. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  258. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  259. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  260. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  261. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  262. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  263. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  264. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  265. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  266. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  267. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  268. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  269. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  270. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  271. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  272. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  273. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  274. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  275. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  276. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  277. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  278. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  279. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  280. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  281. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  282. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  283. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  284. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  285. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  286. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  287. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  288. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  289. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  290. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  291. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  292. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  293. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  294. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  295. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  296. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  297. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  298. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  299. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  300. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  301. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  302. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  303. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  304. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  305. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  306. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  307. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  308. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  309. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  310. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +1 -0
  311. package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
  312. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  313. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  314. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +1 -0
  315. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  316. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  317. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  318. package/pipeline/skills/shared/external/vue-composition/SKILL.md +1 -0
  319. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  320. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  321. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +1 -0
  322. package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
  323. package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
  324. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  325. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  326. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,874 +1,647 @@
1
- # Background Transfers and WebSocket
1
+ # Background Transfers and WebSockets
2
2
 
3
- Patterns for background URLSession downloads/uploads and
4
- URLSessionWebSocketTask with structured concurrency.
5
-
6
- ---
3
+ Two things that are often confused. A background URLSession moves HTTP and
4
+ HTTPS uploads and downloads while the app is not running. A WebSocket is a
5
+ live foreground connection. Neither does the other's job.
7
6
 
8
7
  ## Contents
9
8
 
10
- - [Background URLSession Configuration](#background-urlsession-configuration)
11
- - [Background Download Tasks](#background-download-tasks)
12
- - [Handling Background Session Events](#handling-background-session-events)
13
- - [Background Upload Tasks](#background-upload-tasks)
9
+ - [Setting up a background session](#setting-up-a-background-session)
10
+ - [Downloads that outlive the app](#downloads-that-outlive-the-app)
11
+ - [Relaunch and session event delivery](#relaunch-and-session-event-delivery)
12
+ - [Uploads from a file on disk](#uploads-from-a-file-on-disk)
14
13
  - [URLSessionWebSocketTask](#urlsessionwebsockettask)
15
- - [WebSocket Reconnection Strategy](#websocket-reconnection-strategy)
16
- - [WebSocket with Codable Messages](#websocket-with-codable-messages)
17
- - [Background Session Gotchas](#background-session-gotchas)
18
- - [Combining Background Downloads with SwiftUI Progress](#combining-background-downloads-with-swiftui-progress)
14
+ - [Reconnecting a dropped socket](#reconnecting-a-dropped-socket)
15
+ - [Typed messages over a socket](#typed-messages-over-a-socket)
16
+ - [Pitfalls of background sessions](#pitfalls-of-background-sessions)
17
+ - [Showing download progress in SwiftUI](#showing-download-progress-in-swiftui)
19
18
  - [WebSocket Authentication](#websocket-authentication)
20
- - [WebSocket Subprotocol Negotiation](#websocket-subprotocol-negotiation)
21
-
22
- ## Background URLSession Configuration
23
-
24
- Background sessions allow HTTP/HTTPS upload and download transfers to continue
25
- when the app is suspended or terminated by the system. The system manages the
26
- transfer in a separate process and wakes the app on completion.
19
+ - [Choosing a subprotocol](#choosing-a-subprotocol)
27
20
 
28
- ### Why Background Sessions
21
+ ## Setting up a background session
29
22
 
30
- - Downloads/uploads survive app suspension, system termination, and device restarts.
31
- - The system handles retries for network failures automatically.
32
- - Required for any transfer the user expects to complete even if they
33
- switch away from the app (e.g., file sync, media downloads).
23
+ A background session hands its transfers to a system process. They carry on
24
+ while the app is suspended, after the system terminates it, and across a
25
+ device restart; the system also retries network failures by itself. When a
26
+ transfer finishes, the app is woken or relaunched to hear about it. Use one
27
+ for anything the user expects to complete after leaving the app: syncing a
28
+ folder, downloading an episode, uploading a video.
34
29
 
35
- If the user force-quits the app from the multitasking screen, iOS cancels the
36
- background transfers and does not relaunch the app until the user opens it
37
- again.
30
+ There is one hard limit: a swipe-away in the app switcher. That user
31
+ force-quit cancels every pending background transfer, and the system keeps
32
+ the app closed, with no wake-ups for those transfers, until the person
33
+ launches it by hand.
38
34
 
39
35
  ### Configuration
40
36
 
41
37
  ```swift
42
- @available(iOS 15.0, *)
43
- final class BackgroundDownloadManager: NSObject, Sendable {
44
- static let shared = BackgroundDownloadManager()
38
+ final class BackgroundTransferService: NSObject, @unchecked Sendable {
39
+ static let shared = BackgroundTransferService()
40
+ static let identifier = "com.example.reader.transfers"
45
41
 
46
- /// Use a unique identifier tied to your app's bundle ID.
47
- /// The system uses this to reconnect to the session after relaunch.
48
- private let sessionID = "com.example.app.background-downloads"
42
+ @MainActor var systemCompletionHandler: (() -> Void)?
49
43
 
50
- /// Lazy-initialized background session. Must use a delegate, not async/await,
51
- /// because the system delivers events through the delegate after app relaunch.
52
44
  lazy var session: URLSession = {
53
- let config = URLSessionConfiguration.background(
54
- withIdentifier: sessionID
55
- )
56
- config.isDiscretionary = false // Start immediately (true = system-scheduled)
57
- config.sessionSendsLaunchEvents = true // Wake app on completion
45
+ let config = URLSessionConfiguration.background(withIdentifier: Self.identifier)
46
+ config.isDiscretionary = false
47
+ config.sessionSendsLaunchEvents = true
58
48
  config.allowsExpensiveNetworkAccess = true
59
- config.allowsConstrainedNetworkAccess = false // Respect Low Data Mode
60
- config.timeoutIntervalForResource = 24 * 60 * 60 // 24 hours
61
-
62
- return URLSession(
63
- configuration: config,
64
- delegate: self,
65
- delegateQueue: nil // Use a system-managed serial queue
66
- )
49
+ config.allowsConstrainedNetworkAccess = false
50
+ config.timeoutIntervalForResource = 24 * 60 * 60
51
+ let callbackQueue: OperationQueue? = nil
52
+ return URLSession(configuration: config, delegate: self, delegateQueue: callbackQueue)
67
53
  }()
68
-
69
- /// Store completionHandler from AppDelegate for system callback
70
- nonisolated(unsafe) var backgroundCompletionHandler: (() -> Void)?
71
54
  }
72
55
  ```
73
56
 
74
- ### Key Configuration Options
57
+ - The identifier is unique per app and prefixed with the bundle ID. After a
58
+ relaunch, a new session built from that identifier picks up the transfers
59
+ that were already running.
60
+ - The session needs a delegate. Its events arrive through delegate callbacks,
61
+ possibly in a process that was just relaunched, so async/await calls on it
62
+ are not an option.
63
+ - `delegateQueue: nil` gives the session its own serial operation queue.
64
+ - `@unchecked Sendable` is acceptable here because `URLSession` is
65
+ thread-safe, the lazy session is first touched on the main thread, and the
66
+ completion handler is main-actor isolated.
67
+ - Background sessions are available since iOS 7; the async APIs used around
68
+ them need iOS 15.
75
69
 
76
- | Property | Effect |
77
- |---|---|
78
- | `isDiscretionary` | `true` = system schedules for optimal battery/network. Use for non-urgent sync. `false` = start immediately. |
79
- | `sessionSendsLaunchEvents` | Relaunches the app when transfers complete. Required for completion handling. |
80
- | `allowsConstrainedNetworkAccess` | `false` = honor Low Data Mode. Good for optional downloads. |
81
- | `allowsExpensiveNetworkAccess` | `false` = Wi-Fi only. Use for large transfers. |
82
- | `timeoutIntervalForResource` | Maximum time for the entire transfer. Default is 7 days. |
70
+ ### Key Configuration Options
83
71
 
84
- ---
72
+ | Property | Effect | Typical choice |
73
+ |---|---|---|
74
+ | `isDiscretionary` | `true` lets the system wait for power and good network | `true` for optional sync, `false` to start now |
75
+ | `sessionSendsLaunchEvents` | Relaunch the app when transfers finish | `true`; completion handling needs it |
76
+ | `allowsConstrainedNetworkAccess` | `false` respects Low Data Mode | `false` for optional downloads |
77
+ | `allowsExpensiveNetworkAccess` | `false` keeps transfers off cellular and hotspots | `false` for very large files |
78
+ | `timeoutIntervalForResource` | Maximum lifetime of a whole transfer | Default is 7 days |
85
79
 
86
- ## Background Download Tasks
80
+ ## Downloads that outlive the app
87
81
 
88
- Background downloads must use `downloadTask(with:)`, not `data(for:)`.
89
- The async/await overloads are not supported for background sessions --
90
- you must use the delegate pattern.
82
+ Background downloads are `downloadTask(with:)` tasks. The async overloads
83
+ such as `data(for:)` and `download(for:)` are not supported on a background
84
+ session.
91
85
 
92
86
  ```swift
93
- extension BackgroundDownloadManager {
94
- func startDownload(from url: URL) -> URLSessionDownloadTask {
95
- let task = session.downloadTask(with: url)
96
- task.earliestBeginDate = Date() // Start now
97
- task.countOfBytesClientExpectsToSend = 0
98
- task.countOfBytesClientExpectsToReceive = 50 * 1024 * 1024 // Estimated size
99
- task.resume()
100
- return task
87
+ extension BackgroundTransferService {
88
+ func enqueueDownload(of source: URL) {
89
+ let job = session.downloadTask(with: source)
90
+ job.earliestBeginDate = .now
91
+ job.countOfBytesClientExpectsToSend = 0
92
+ job.countOfBytesClientExpectsToReceive = 80 * 1_048_576
93
+ job.resume()
101
94
  }
102
95
 
103
- func startDownload(from url: URL, resumeData: Data) -> URLSessionDownloadTask {
104
- let task = session.downloadTask(withResumeData: resumeData)
105
- task.resume()
106
- return task
96
+ func resumeDownload(with resumeData: Data) {
97
+ session.downloadTask(withResumeData: resumeData).resume()
107
98
  }
108
99
  }
109
100
  ```
110
101
 
102
+ The byte estimates help the scheduler decide when to run the task.
103
+
111
104
  ### Download Delegate
112
105
 
113
106
  ```swift
114
- extension BackgroundDownloadManager: URLSessionDownloadDelegate {
115
- nonisolated func urlSession(
116
- _ session: URLSession,
117
- downloadTask: URLSessionDownloadTask,
118
- didFinishDownloadingTo location: URL
119
- ) {
120
- // CRITICAL: Move or open the file before this method returns.
121
- // The temporary file is only available until the delegate returns.
122
- let destinationDir = FileManager.default.urls(
123
- for: .documentDirectory,
124
- in: .userDomainMask
125
- ).first!
126
-
127
- let filename = downloadTask.originalRequest?.url?.lastPathComponent ?? UUID().uuidString
128
- let destination = destinationDir.appendingPathComponent(filename)
129
-
107
+ extension BackgroundTransferService: URLSessionDownloadDelegate {
108
+ func urlSession(_: URLSession, downloadTask job: URLSessionDownloadTask,
109
+ didFinishDownloadingTo location: URL) {
110
+ let fileName = job.originalRequest?.url?.lastPathComponent
111
+ let destination = URL.documentsDirectory.appending(path: fileName ?? UUID().uuidString)
112
+ let files = FileManager.default
130
113
  do {
131
- // Remove existing file if present
132
- if FileManager.default.fileExists(atPath: destination.path) {
133
- try FileManager.default.removeItem(at: destination)
134
- }
135
- try FileManager.default.moveItem(at: location, to: destination)
136
- // Notify the app (post notification, update state, etc.)
114
+ if files.fileExists(atPath: destination.path()) { try files.removeItem(at: destination) }
115
+ try files.moveItem(at: location, to: destination)
137
116
  } catch {
138
- // Handle file move failure
117
+ Logger.transfers.error("Could not keep download: \(error.localizedDescription)")
139
118
  }
140
119
  }
141
120
 
142
- nonisolated func urlSession(
143
- _ session: URLSession,
144
- downloadTask: URLSessionDownloadTask,
145
- didWriteData bytesWritten: Int64,
146
- totalBytesWritten: Int64,
147
- totalBytesExpectedToWrite: Int64
148
- ) {
149
- guard totalBytesExpectedToWrite > 0 else { return }
150
- let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
151
- // Update progress UI (dispatch to main if needed)
152
- }
153
-
154
- nonisolated func urlSession(
155
- _ session: URLSession,
156
- task: URLSessionTask,
157
- didCompleteWithError error: (any Error)?
158
- ) {
159
- guard let error else { return } // Success handled in didFinishDownloadingTo
160
-
161
- // Check for resume data on failure
162
- let nsError = error as NSError
163
- if let resumeData = nsError.userInfo[NSURLSessionDownloadTaskResumeData] as? Data {
164
- // Store resumeData for retry
165
- saveResumeData(resumeData, for: task)
166
- }
121
+ func urlSession(_: URLSession, downloadTask job: URLSessionDownloadTask,
122
+ didWriteData bytesWritten: Int64, totalBytesWritten: Int64,
123
+ totalBytesExpectedToWrite: Int64) {
124
+ guard totalBytesExpectedToWrite > 0,
125
+ let url = job.originalRequest?.url else { return }
126
+ reportProgress(Double(totalBytesWritten) / Double(totalBytesExpectedToWrite), for: url)
167
127
  }
168
128
 
169
- private func saveResumeData(_ data: Data, for task: URLSessionTask) {
170
- // Persist resume data to disk for later retry
171
- let key = task.originalRequest?.url?.absoluteString ?? ""
172
- let path = FileManager.default.temporaryDirectory
173
- .appendingPathComponent("resume-\(key.hashValue)")
174
- try? data.write(to: path)
129
+ func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError failure: (any Error)?) {
130
+ guard let error = failure else { return }
131
+ let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
132
+ if let resumeData, let url = task.originalRequest?.url {
133
+ let file = URL.temporaryDirectory.appending(path: "resume-\(url.absoluteString.hashValue)")
134
+ try? resumeData.write(to: file)
135
+ }
175
136
  }
176
137
  }
177
138
  ```
178
139
 
179
- ---
140
+ - `location` exists only until `didFinishDownloadingTo` returns, so the move
141
+ happens inside it.
142
+ - Progress arrives on the session queue; UI updates must hop to the main
143
+ actor (see the tracker below).
144
+ - `didCompleteWithError` with a `nil` error is the success case, already
145
+ handled when the file was moved.
146
+ - On failure, the resume data is in the error's `userInfo` under
147
+ `NSURLSessionDownloadTaskResumeData`. Persist it and pass it to
148
+ `downloadTask(withResumeData:)` later. `hashValue` is seeded per launch, so
149
+ a real app should key the file by a stable ID it stores itself.
180
150
 
181
- ## Handling Background Session Events
151
+ ## Relaunch and session event delivery
182
152
 
183
- When the system completes a background transfer and the app is not
184
- running, it relaunches the app and calls the `AppDelegate` method. If you use
185
- the completion-handler overload, call the system's completion handler after
186
- processing all events.
187
-
188
- ### UIKit App Delegate
153
+ When a transfer finishes while the app is not running, the system relaunches
154
+ it in the background and calls the app delegate with a completion handler.
155
+ Call that handler once every pending event has been delivered.
189
156
 
190
157
  ```swift
191
- class AppDelegate: UIResponder, UIApplicationDelegate {
192
- func application(
193
- _ application: UIApplication,
194
- handleEventsForBackgroundURLSession identifier: String,
195
- completionHandler: @escaping () -> Void
196
- ) {
197
- // Store the completion handler. The BackgroundDownloadManager will
198
- // call it after processing all pending events.
199
- BackgroundDownloadManager.shared.backgroundCompletionHandler = completionHandler
200
-
201
- // Accessing .session triggers lazy initialization, which reconnects
202
- // to the background session and starts delivering delegate events.
203
- _ = BackgroundDownloadManager.shared.session
158
+ final class AppDelegate: NSObject, UIApplicationDelegate {
159
+ func application(_ application: UIApplication,
160
+ handleEventsForBackgroundURLSession identifier: String,
161
+ completionHandler: @escaping () -> Void) {
162
+ guard identifier == BackgroundTransferService.identifier else { return completionHandler() }
163
+ BackgroundTransferService.shared.systemCompletionHandler = completionHandler
164
+ _ = BackgroundTransferService.shared.session
204
165
  }
205
166
  }
206
- ```
207
167
 
208
- ### Session-Level Delegate
209
-
210
- ```swift
211
- extension BackgroundDownloadManager: URLSessionDelegate {
212
- nonisolated func urlSessionDidFinishEvents(
213
- forBackgroundURLSession session: URLSession
214
- ) {
215
- // Called after ALL pending delegate events have been delivered.
216
- // Call the stored completion handler on the main thread.
168
+ extension BackgroundTransferService {
169
+ func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) {
217
170
  Task { @MainActor in
218
- backgroundCompletionHandler?()
219
- backgroundCompletionHandler = nil
171
+ self.systemCompletionHandler?()
172
+ self.systemCompletionHandler = nil
220
173
  }
221
174
  }
222
175
  }
223
- ```
224
-
225
- ### SwiftUI App with AppDelegate Adapter
226
176
 
227
- ```swift
228
177
  @main
229
- struct MyApp: App {
230
- @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
231
-
178
+ struct ReaderApp: App {
179
+ @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
232
180
  var body: some Scene {
233
- WindowGroup {
234
- ContentView()
235
- }
181
+ WindowGroup { ShelfView() }
236
182
  }
237
183
  }
238
184
  ```
239
185
 
240
- **Important:** For the handler-based `UIApplicationDelegate` overload, the
241
- completion handler must be called exactly once and on the main thread. Failing
242
- to call it causes the system to take a snapshot of the app in the wrong state
243
- and may waste background runtime.
186
+ - Touching `session` recreates the background session, which is what makes
187
+ the queued delegate events flow.
188
+ - `urlSessionDidFinishEvents(forBackgroundURLSession:)` fires after the last
189
+ pending event.
190
+ - Call the stored handler exactly once, on the main thread. It tells the
191
+ system to take the app-switcher snapshot and suspend the app; skipping it
192
+ leaves a stale snapshot and wastes background time.
193
+ - SwiftUI apps reach the app delegate through `@UIApplicationDelegateAdaptor`.
244
194
 
245
- ---
195
+ ## Uploads from a file on disk
246
196
 
247
- ## Background Upload Tasks
248
-
249
- Background uploads require data from a file, not from memory.
197
+ A background upload must come from a file. Uploading `Data` or a stream is
198
+ not supported on a background session.
250
199
 
251
200
  ```swift
252
- extension BackgroundDownloadManager {
253
- func startUpload(
254
- to url: URL,
255
- fileURL: URL,
256
- method: String = "POST",
257
- headers: [String: String] = [:]
258
- ) -> URLSessionUploadTask {
259
- var request = URLRequest(url: url)
260
- request.httpMethod = method
261
- for (key, value) in headers {
262
- request.setValue(value, forHTTPHeaderField: key)
263
- }
264
-
265
- let task = session.uploadTask(with: request, fromFile: fileURL)
266
- task.resume()
267
- return task
201
+ extension BackgroundTransferService {
202
+ func enqueueUpload(of stagedFile: URL, to endpoint: URL,
203
+ verb: String = "POST", extraHeaders: [String: String] = [:]) {
204
+ var put = URLRequest(url: endpoint)
205
+ put.httpMethod = verb
206
+ put.allHTTPHeaderFields = extraHeaders
207
+ session.uploadTask(with: put, fromFile: stagedFile).resume()
268
208
  }
269
- }
270
- ```
271
-
272
- ### Upload Delegate Methods
273
209
 
274
- ```swift
275
- extension BackgroundDownloadManager {
276
- nonisolated func urlSession(
277
- _ session: URLSession,
278
- task: URLSessionTask,
279
- didSendBodyData bytesSent: Int64,
280
- totalBytesSent: Int64,
281
- totalBytesExpectedToSend: Int64
282
- ) {
283
- guard totalBytesExpectedToSend > 0 else { return }
284
- let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
285
- // Update progress UI
210
+ func urlSession(_: URLSession, task: URLSessionTask, didSendBodyData _: Int64,
211
+ totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
212
+ guard totalBytesExpectedToSend > 0, let url = task.originalRequest?.url else { return }
213
+ reportProgress(Double(totalBytesSent) / Double(totalBytesExpectedToSend), for: url)
286
214
  }
287
215
  }
288
216
  ```
289
217
 
290
- **Constraints of background uploads:**
291
- - Data must come from a file (`uploadTask(with:fromFile:)`).
292
- - `uploadTask(with:from: Data)` is not supported in background sessions.
293
- - Write multipart form data to a temporary file first, then upload.
294
-
295
- ---
218
+ `uploadTask(with:from:)` with in-memory data fails on a background session.
219
+ For a multipart upload, write the encoded body to a temporary file first and
220
+ upload that file.
296
221
 
297
222
  ## URLSessionWebSocketTask
298
223
 
299
- `URLSessionWebSocketTask` provides native WebSocket support without
300
- third-party libraries. Available since iOS 13. WebSockets use `ws:` or `wss:`
301
- URLs and are foreground/default-session realtime networking; background
302
- URLSession configuration does not make a WebSocket connection durable after
303
- suspension.
224
+ `URLSessionWebSocketTask` (iOS 13 and later) is the built-in WebSocket
225
+ client. It connects to `ws:` or `wss:` URLs. It is a foreground, default
226
+ session tool: a background configuration does not keep a socket alive once
227
+ the app is suspended.
304
228
 
305
229
  ### Basic Connection
306
230
 
307
231
  ```swift
308
- @available(iOS 15.0, *)
309
- final class WebSocketConnection: Sendable {
310
- private let task: URLSessionWebSocketTask
311
-
312
- init(url: URL, session: URLSession = .shared) {
313
- self.task = session.webSocketTask(with: url)
314
- }
232
+ final class SocketLink: Sendable {
233
+ private let socket: URLSessionWebSocketTask
315
234
 
316
- func connect() {
317
- task.resume()
235
+ init(address: URL, using session: URLSession) {
236
+ socket = session.webSocketTask(with: address)
318
237
  }
319
238
 
320
- func disconnect(reason: String? = nil) {
321
- task.cancel(with: .normalClosure, reason: reason?.data(using: .utf8))
322
- }
239
+ func open() { socket.resume() }
323
240
 
324
- func send(_ message: URLSessionWebSocketTask.Message) async throws {
325
- try await task.send(message)
241
+ func close(why: String? = nil) {
242
+ let note = why.map { Data($0.utf8) }
243
+ socket.cancel(with: .normalClosure, reason: note)
326
244
  }
327
245
 
328
- func send(text: String) async throws {
329
- try await task.send(.string(text))
246
+ func write(_ line: String) async throws {
247
+ try await socket.send(.string(line))
330
248
  }
331
-
332
- func send(data: Data) async throws {
333
- try await task.send(.data(data))
249
+ func write(_ blob: Data) async throws {
250
+ try await socket.send(.data(blob))
334
251
  }
335
-
336
- func receive() async throws -> URLSessionWebSocketTask.Message {
337
- try await task.receive()
252
+ func read() async throws -> URLSessionWebSocketTask.Message {
253
+ let incoming = try await socket.receive()
254
+ return incoming
338
255
  }
339
256
  }
340
257
  ```
341
258
 
342
259
  ### WebSocket with Structured Concurrency
343
260
 
344
- The key pattern: run a receive loop as an async task that yields
345
- messages through an `AsyncStream`. This integrates naturally with
346
- structured concurrency.
261
+ An actor owns the socket and a receive loop that feeds an `AsyncStream`.
347
262
 
348
263
  ```swift
349
- @available(iOS 15.0, *)
350
- actor WebSocketManager {
351
- private var task: URLSessionWebSocketTask?
352
- private var receiveTask: Task<Void, Never>?
353
- private let session: URLSession
354
- private let url: URL
355
-
264
+ actor SocketChannel {
356
265
  enum Event: Sendable {
357
- case connected
266
+ case opened
358
267
  case text(String)
359
- case data(Data)
360
- case disconnected(URLSessionWebSocketTask.CloseCode, Data?)
361
- case error(Error)
268
+ case binary(Data)
269
+ case closed(URLSessionWebSocketTask.CloseCode, Data?)
270
+ case failed(any Error)
362
271
  }
363
272
 
364
- init(url: URL, session: URLSession = .shared) {
365
- self.url = url
273
+ private let url: URL
274
+ private let session: URLSession
275
+ private var socket: URLSessionWebSocketTask?
276
+ private var receiver: Task<Void, Never>?
277
+
278
+ init(address: URL, using session: URLSession = .shared) {
279
+ url = address
366
280
  self.session = session
367
281
  }
368
282
 
369
- /// Returns a stream of WebSocket events. Call `connect()` to start.
370
283
  func events() -> AsyncStream<Event> {
371
- AsyncStream { continuation in
372
- let wsTask = session.webSocketTask(with: url)
373
- self.task = wsTask
374
-
375
- wsTask.resume()
376
- continuation.yield(.connected)
377
-
378
- // Start the receive loop
379
- self.receiveTask = Task { [weak self] in
380
- await self?.receiveLoop(continuation: continuation)
381
- }
382
-
383
- continuation.onTermination = { _ in
384
- Task { [weak self] in
385
- await self?.disconnect()
386
- }
387
- }
388
- }
389
- }
390
-
391
- private func receiveLoop(continuation: AsyncStream<Event>.Continuation) async {
392
- guard let task else { return }
393
-
394
- while !Task.isCancelled {
395
- do {
396
- let message = try await task.receive()
397
- switch message {
398
- case .string(let text):
399
- continuation.yield(.text(text))
400
- case .data(let data):
401
- continuation.yield(.data(data))
402
- @unknown default:
284
+ let (stream, continuation) = AsyncStream.makeStream(of: Event.self)
285
+ let socket = session.webSocketTask(with: url)
286
+ self.socket = socket
287
+ socket.resume()
288
+ continuation.yield(.opened)
289
+ receiver = Task {
290
+ while !Task.isCancelled {
291
+ do {
292
+ switch try await socket.receive() {
293
+ case .string(let words): continuation.yield(.text(words))
294
+ case .data(let blob): continuation.yield(.binary(blob))
295
+ @unknown default: break
296
+ }
297
+ } catch {
298
+ if socket.closeCode == .invalid {
299
+ continuation.yield(.failed(error))
300
+ } else {
301
+ continuation.yield(.closed(socket.closeCode, socket.closeReason))
302
+ }
403
303
  break
404
304
  }
405
- } catch {
406
- // The receive threw -- connection closed or failed
407
- let closeCode = task.closeCode
408
- let closeReason = task.closeReason
409
- if closeCode == .invalid {
410
- // Unexpected disconnection
411
- continuation.yield(.error(error))
412
- } else {
413
- continuation.yield(.disconnected(closeCode, closeReason))
414
- }
415
- continuation.finish()
416
- return
417
305
  }
306
+ continuation.finish()
418
307
  }
308
+ continuation.onTermination = { [weak self] _ in
309
+ Task { await self?.close() }
310
+ }
311
+ return stream
419
312
  }
420
313
 
421
- func send(text: String) async throws {
422
- try await task?.send(.string(text))
423
- }
424
-
425
- func send(data: Data) async throws {
426
- try await task?.send(.data(data))
427
- }
428
-
429
- func disconnect() {
430
- receiveTask?.cancel()
431
- receiveTask = nil
432
- task?.cancel(with: .normalClosure, reason: nil)
433
- task = nil
314
+ func send(_ text: String) async throws {
315
+ try await socket?.send(.string(text))
434
316
  }
435
317
 
436
- /// Send periodic pings to keep the connection alive
437
- func startPinging(interval: Duration = .seconds(30)) {
438
- Task { [weak self] in
439
- while !Task.isCancelled {
440
- try? await Task.sleep(for: interval)
441
- guard let self else { return }
442
- await self.ping()
443
- }
444
- }
318
+ func close() {
319
+ receiver?.cancel()
320
+ socket?.cancel(with: .normalClosure, reason: nil)
321
+ receiver = nil
322
+ socket = nil
445
323
  }
446
324
 
447
- private func ping() {
448
- task?.sendPing { error in
449
- if let error {
450
- // Connection may be dead
451
- print("Ping failed: \(error)")
325
+ func keepAlive(every interval: Duration = .seconds(30)) async {
326
+ while !Task.isCancelled {
327
+ do { try await Task.sleep(for: interval) } catch { return }
328
+ socket?.sendPing { error in
329
+ if error != nil {
330
+ Logger.transfers.notice("Ping failed; the connection is probably gone")
331
+ }
452
332
  }
453
333
  }
454
334
  }
455
335
  }
456
336
  ```
457
337
 
338
+ - A receive error with `closeCode == .invalid` means the peer never sent a
339
+ close frame: treat it as an unexpected failure. Any other close code is an
340
+ orderly close.
341
+ - The receive loop captures only the socket and the continuation, never the
342
+ actor, and the termination handler holds the actor weakly, so neither keeps
343
+ the channel alive.
344
+ - `keepAlive` sends a ping on an interval; a failed ping usually means the
345
+ connection is dead and a reconnect is due.
346
+
458
347
  ### Usage in SwiftUI
459
348
 
460
349
  ```swift
461
- @MainActor
462
- @Observable final class ChatStore {
463
- var messages: [ChatMessage] = []
464
- var connectionState: ConnectionState = .disconnected
350
+ @MainActor @Observable
351
+ final class RoomModel {
352
+ enum LinkState { case offline, connecting, online }
465
353
 
466
- enum ConnectionState { case disconnected, connecting, connected }
354
+ struct Line: Identifiable { let id = UUID(); let text: String; let isMine: Bool }
467
355
 
468
- private let wsManager: WebSocketManager
469
- private var eventTask: Task<Void, Never>?
356
+ private(set) var lines: [Line] = []
357
+ private(set) var state: LinkState = .offline
358
+ private let channel: SocketChannel
359
+ private var listener: Task<Void, Never>?
470
360
 
471
- init(url: URL) {
472
- self.wsManager = WebSocketManager(url: url)
473
- }
361
+ init(channel: SocketChannel) { self.channel = channel }
474
362
 
475
363
  func connect() async {
476
- connectionState = .connecting
477
- let stream = await wsManager.events()
478
-
479
- eventTask = Task {
480
- for await event in stream {
481
- await handleEvent(event)
482
- }
364
+ state = .connecting
365
+ let stream = await channel.events()
366
+ listener = Task {
367
+ for await event in stream { handle(event) }
483
368
  }
484
369
  }
485
370
 
486
- func sendMessage(_ text: String) async {
371
+ func post(_ text: String) async {
487
372
  do {
488
- try await wsManager.send(text: text)
489
- messages.append(ChatMessage(text: text, isOutgoing: true))
373
+ try await channel.send(text)
374
+ lines.append(Line(text: text, isMine: true))
490
375
  } catch {
491
- // Handle send failure
376
+ state = .offline
492
377
  }
493
378
  }
494
379
 
495
380
  func disconnect() async {
496
- eventTask?.cancel()
497
- eventTask = nil
498
- await wsManager.disconnect()
499
- connectionState = .disconnected
381
+ listener?.cancel()
382
+ await channel.close()
383
+ state = .offline
500
384
  }
501
385
 
502
- private func handleEvent(_ event: WebSocketManager.Event) async {
386
+ private func handle(_ event: SocketChannel.Event) {
503
387
  switch event {
504
- case .connected:
505
- connectionState = .connected
506
- case .text(let text):
507
- messages.append(ChatMessage(text: text, isOutgoing: false))
508
- case .data(let data):
509
- if let text = String(data: data, encoding: .utf8) {
510
- messages.append(ChatMessage(text: text, isOutgoing: false))
511
- }
512
- case .disconnected:
513
- connectionState = .disconnected
514
- case .error:
515
- connectionState = .disconnected
516
- // Optionally trigger reconnection
388
+ case .opened: state = .online
389
+ case .text(let text): lines.append(Line(text: text, isMine: false))
390
+ case .binary(let blob):
391
+ lines.append(Line(text: String(decoding: blob, as: UTF8.self), isMine: false))
392
+ case .closed, .failed: state = .offline
517
393
  }
518
394
  }
519
395
  }
520
- ```
521
-
522
- ```swift
523
- struct ChatView: View {
524
- @State var store: ChatStore
525
396
 
397
+ struct RoomView: View {
398
+ let model: RoomModel
526
399
  var body: some View {
527
- List(store.messages) { message in
528
- ChatBubble(message: message)
529
- }
530
- .task { await store.connect() }
531
- .onDisappear { Task { await store.disconnect() } }
400
+ List(model.lines) { Text($0.text) }
401
+ .task { await model.connect() }
402
+ .onDisappear { Task { await model.disconnect() } }
532
403
  }
533
404
  }
534
405
  ```
535
406
 
536
- ---
407
+ A sent message is appended only after `send` succeeds. On `.closed` or
408
+ `.failed` the model goes offline; reconnecting is the next section.
537
409
 
538
- ## WebSocket Reconnection Strategy
410
+ ## Reconnecting a dropped socket
539
411
 
540
- Network drops happen. A robust WebSocket client must reconnect
541
- automatically with exponential backoff.
412
+ Sockets drop. A durable client reconnects on its own with exponential
413
+ backoff.
542
414
 
543
415
  ```swift
544
- @available(iOS 15.0, *)
545
- actor ReconnectingWebSocket {
416
+ actor ResilientChannel {
546
417
  private let url: URL
547
- private let session: URLSession
548
- private let maxReconnectAttempts: Int
549
- private let initialDelay: Duration
550
- private let maxDelay: Duration
551
-
552
- private var currentManager: WebSocketManager?
553
- private var reconnectAttempts = 0
554
- private var isIntentionalDisconnect = false
555
-
556
- init(
557
- url: URL,
558
- session: URLSession = .shared,
559
- maxReconnectAttempts: Int = 10,
560
- initialDelay: Duration = .seconds(1),
561
- maxDelay: Duration = .seconds(60)
562
- ) {
563
- self.url = url
564
- self.session = session
565
- self.maxReconnectAttempts = maxReconnectAttempts
566
- self.initialDelay = initialDelay
567
- self.maxDelay = maxDelay
568
- }
418
+ private let maxAttempts = 10
419
+ private let firstDelay: Duration = .seconds(1)
420
+ private let ceiling: Duration = .seconds(60)
421
+ private var attempts = 0
422
+ private var stoppedOnPurpose = false
423
+ private var current: SocketChannel?
569
424
 
570
- /// Returns a stream that automatically reconnects on disconnection.
571
- func events() -> AsyncStream<WebSocketManager.Event> {
572
- AsyncStream { continuation in
573
- Task {
574
- await connectWithReconnection(continuation: continuation)
575
- }
576
- continuation.onTermination = { _ in
577
- Task { [weak self] in
578
- await self?.intentionalDisconnect()
579
- }
580
- }
581
- }
582
- }
425
+ init(url: URL) { self.url = url }
583
426
 
584
- private func connectWithReconnection(
585
- continuation: AsyncStream<WebSocketManager.Event>.Continuation
586
- ) async {
587
- while !isIntentionalDisconnect && reconnectAttempts < maxReconnectAttempts {
588
- guard !Task.isCancelled else { break }
589
-
590
- let manager = WebSocketManager(url: url, session: session)
591
- currentManager = manager
592
- let stream = await manager.events()
593
-
594
- for await event in stream {
595
- switch event {
596
- case .connected:
597
- reconnectAttempts = 0 // Reset on successful connection
598
- continuation.yield(event)
599
- case .error, .disconnected:
600
- continuation.yield(event)
601
- default:
602
- continuation.yield(event)
427
+ func events() -> AsyncStream<SocketChannel.Event> {
428
+ AsyncStream { continuation in
429
+ let loop = Task {
430
+ while shouldContinue() {
431
+ let channel = fresh()
432
+ for await event in await channel.events() {
433
+ if case .opened = event { resetAttempts() }
434
+ continuation.yield(event)
435
+ }
436
+ if Task.isCancelled || isStopped() { break }
437
+ let wait = nextDelay()
438
+ if (try? await Task.sleep(for: wait)) == nil { break }
603
439
  }
440
+ continuation.finish()
604
441
  }
605
-
606
- // Stream ended -- attempt reconnection unless intentional
607
- guard !isIntentionalDisconnect, !Task.isCancelled else { break }
608
-
609
- reconnectAttempts += 1
610
- let delay = calculateBackoff()
611
- do {
612
- try await Task.sleep(for: delay)
613
- } catch {
614
- break // Cancelled during sleep
442
+ continuation.onTermination = { _ in
443
+ loop.cancel()
444
+ Task { await self.stop() }
615
445
  }
616
446
  }
617
-
618
- continuation.finish()
619
447
  }
620
448
 
621
- private func calculateBackoff() -> Duration {
622
- let base = Double(initialDelay.components.seconds) * pow(2.0, Double(reconnectAttempts - 1))
623
- let capped = min(base, Double(maxDelay.components.seconds))
624
- let jitter = Double.random(in: 0...(capped * 0.25))
625
- return .seconds(capped + jitter)
449
+ func stop() async {
450
+ stoppedOnPurpose = true
451
+ await current?.close()
626
452
  }
627
453
 
628
- func send(text: String) async throws {
629
- try await currentManager?.send(text: text)
630
- }
454
+ private func shouldContinue() -> Bool { !stoppedOnPurpose && attempts < maxAttempts }
455
+ private func isStopped() -> Bool { stoppedOnPurpose }
456
+ private func resetAttempts() { attempts = 0 }
631
457
 
632
- func send(data: Data) async throws {
633
- try await currentManager?.send(data: data)
458
+ private func fresh() -> SocketChannel {
459
+ let channel = SocketChannel(address: url)
460
+ current = channel
461
+ return channel
634
462
  }
635
463
 
636
- private func intentionalDisconnect() {
637
- isIntentionalDisconnect = true
638
- Task {
639
- await currentManager?.disconnect()
640
- }
464
+ private func nextDelay() -> Duration {
465
+ attempts += 1
466
+ let grown = firstDelay * Int(pow(2.0, Double(attempts - 1)))
467
+ let capped = min(grown, ceiling)
468
+ return capped + capped * Double.random(in: 0...0.25)
641
469
  }
642
470
  }
643
471
  ```
644
472
 
645
- ---
473
+ - Each attempt uses a new channel, and every event is passed through.
474
+ - The `Task` made inside `events()` inherits the actor's isolation, so the
475
+ actor's own helpers are called without `await`; only the other actor
476
+ (`channel.events()`) needs one.
477
+ - A successful open resets the attempt counter.
478
+ - After a stream ends: stop if the disconnect was intentional or the task
479
+ was cancelled; otherwise count the attempt and sleep. A cancelled sleep ends
480
+ the loop.
481
+ - Delay is `firstDelay * 2^(attempt - 1)`, capped at 60 seconds, plus up to
482
+ 25% jitter; at most 10 attempts.
483
+ - Ending the outer stream marks the stop as intentional and closes the live
484
+ channel.
646
485
 
647
- ## WebSocket with Codable Messages
486
+ ## Typed messages over a socket
648
487
 
649
- For typed message protocols (common in chat, gaming, real-time apps),
650
- decode/encode messages automatically.
488
+ Wrap typed messages in an envelope that names the type, so one socket can
489
+ carry many message kinds.
651
490
 
652
491
  ```swift
653
- protocol WebSocketMessage: Codable, Sendable {
654
- static var messageType: String { get }
492
+ protocol SocketMessage: Codable, Sendable {
493
+ static var kind: String { get }
655
494
  }
656
495
 
657
- struct TypedWebSocketTransport {
658
- private let manager: WebSocketManager
659
- private let encoder = JSONEncoder()
660
- private let decoder = JSONDecoder()
496
+ struct Envelope: Codable, Sendable {
497
+ let kind: String
498
+ let payload: Data
499
+ }
661
500
 
662
- init(manager: WebSocketManager) {
663
- self.manager = manager
501
+ struct TypedChannel: Sendable {
502
+ enum Incoming: Sendable {
503
+ case opened
504
+ case message(kind: String, payload: Data)
505
+ case closed(URLSessionWebSocketTask.CloseCode)
506
+ case failed(any Error)
664
507
  }
665
508
 
666
- func send<T: WebSocketMessage>(_ message: T) async throws {
667
- let envelope = MessageEnvelope(
668
- type: T.messageType,
669
- payload: try encoder.encode(message)
670
- )
671
- let data = try encoder.encode(envelope)
672
- try await manager.send(data: data)
509
+ let channel: SocketChannel
510
+
511
+ func send<M: SocketMessage>(_ message: M) async throws {
512
+ let envelope = Envelope(kind: M.kind, payload: try JSONEncoder().encode(message))
513
+ let frame = try JSONEncoder().encode(envelope)
514
+ try await channel.send(String(decoding: frame, as: UTF8.self))
673
515
  }
674
516
 
675
- /// Typed event stream that decodes known message types
676
- func typedEvents() async -> AsyncStream<DecodedEvent> {
677
- let rawEvents = await manager.events()
517
+ func incoming() async -> AsyncStream<Incoming> {
518
+ let raw = await channel.events()
678
519
  return AsyncStream { continuation in
679
- Task {
680
- for await event in rawEvents {
520
+ let pump = Task {
521
+ for await event in raw {
681
522
  switch event {
682
- case .data(let data):
683
- if let envelope = try? decoder.decode(MessageEnvelope.self, from: data) {
684
- continuation.yield(.message(type: envelope.type, payload: envelope.payload))
685
- }
686
- case .text(let text):
687
- if let data = text.data(using: .utf8),
688
- let envelope = try? decoder.decode(MessageEnvelope.self, from: data) {
689
- continuation.yield(.message(type: envelope.type, payload: envelope.payload))
690
- }
691
- case .connected:
692
- continuation.yield(.connected)
693
- case .disconnected(let code, _):
694
- continuation.yield(.disconnected(code))
695
- case .error(let error):
696
- continuation.yield(.error(error))
523
+ case .opened: continuation.yield(.opened)
524
+ case .text(let text): yieldEnvelope(Data(text.utf8), to: continuation)
525
+ case .binary(let data): yieldEnvelope(data, to: continuation)
526
+ case .closed(let code, _): continuation.yield(.closed(code))
527
+ case .failed(let problem): continuation.yield(.failed(problem))
697
528
  }
698
529
  }
699
530
  continuation.finish()
700
531
  }
532
+ continuation.onTermination = { _ in pump.cancel() }
701
533
  }
702
534
  }
703
535
 
704
- enum DecodedEvent: Sendable {
705
- case connected
706
- case message(type: String, payload: Data)
707
- case disconnected(URLSessionWebSocketTask.CloseCode)
708
- case error(Error)
709
- }
710
-
711
- private struct MessageEnvelope: Codable, Sendable {
712
- let type: String
713
- let payload: Data
536
+ private func yieldEnvelope(_ data: Data, to continuation: AsyncStream<Incoming>.Continuation) {
537
+ guard let envelope = try? JSONDecoder().decode(Envelope.self, from: data) else { return }
538
+ continuation.yield(.message(kind: envelope.kind, payload: envelope.payload))
714
539
  }
715
540
  }
716
541
  ```
717
542
 
718
- ---
719
-
720
- ## Background Session Gotchas
721
-
722
- ### The session identifier must be unique per app
723
- If two sessions share the same identifier, events may be delivered to
724
- the wrong delegate. Use your bundle identifier as a prefix.
725
-
726
- ### Background sessions do not support async/await overloads
727
- The `data(for:)` and `download(for:)` async methods are not available
728
- on background sessions. Use `downloadTask(with:)` and the delegate.
729
-
730
- ### Only download and upload tasks are supported
731
- Data tasks (`dataTask`) are not supported in background sessions. Convert
732
- data requests to download tasks if needed for background execution.
733
- WebSocket tasks are not background transfer tasks; reconnect them when the app
734
- is active again.
735
-
736
- ### The app may be terminated and relaunched
737
- Store any state you need (task identifiers, file destinations) to disk.
738
- Do not rely on in-memory state surviving a background relaunch.
739
- User force-quit is different from system termination: iOS cancels outstanding
740
- background transfers and will not relaunch the app automatically.
741
-
742
- ### File must be moved or opened in didFinishDownloadingTo
743
- The temporary file at `location` is available until the delegate method
744
- returns. Move it to preserve it, or open it for reading before returning.
745
-
746
- ### Call the system completion handler exactly once
747
- Store the completion handler from
748
- `application(_:handleEventsForBackgroundURLSession:completionHandler:)`
749
- and invoke it in `urlSessionDidFinishEvents(forBackgroundURLSession:)`
750
- on the main thread.
751
-
752
- ### Test on a real device
753
- Background session behavior differs significantly between the Simulator
754
- and real devices. Always test background transfers on hardware.
755
-
756
- ---
757
-
758
- ## Combining Background Downloads with SwiftUI Progress
759
-
760
- Bridge the delegate-based background download to an `@Observable` model
761
- for live UI updates.
543
+ Frames that are not valid envelopes are dropped. The receiver switches on
544
+ `kind` and decodes `payload` into the matching `SocketMessage` type. The
545
+ channel above sends text frames; add a `send(data:)` to it if the server
546
+ expects binary envelopes.
547
+
548
+ ## Pitfalls of background sessions
549
+
550
+ - Session identifiers must be unique within the app. Two sessions sharing one
551
+ can deliver events to the wrong delegate. Prefix with the bundle ID.
552
+ - `data(for:)` and `download(for:)` are not available on a background
553
+ session.
554
+ - Only download and upload tasks run there. Data tasks do not; turn a data
555
+ request into a download if it must survive.
556
+ - WebSocket tasks are not background transfers. Reconnect when the app
557
+ becomes active again.
558
+ - Write task identifiers and planned destinations to disk. Memory does not
559
+ survive a background relaunch.
560
+ - A user force-quit is not a system termination: transfers are cancelled and
561
+ the app is not relaunched for them.
562
+ - Move or open the `location` file inside `didFinishDownloadingTo`.
563
+ - The app delegate's background-session callback hands over a completion
564
+ closure. Hold on to it, then invoke it a single time on the main thread
565
+ when `urlSessionDidFinishEvents(forBackgroundURLSession:)` arrives.
566
+ - Background scheduling in Simulator does not match a device. Test on
567
+ hardware.
568
+
569
+ ## Showing download progress in SwiftUI
570
+
571
+ Delegate callbacks run on the session queue. Bridge them into an observable
572
+ model on the main actor:
762
573
 
763
574
  ```swift
764
- @MainActor
765
- @Observable final class DownloadTracker {
766
- var downloads: [URL: DownloadProgress] = [:]
767
-
768
- struct DownloadProgress: Sendable {
769
- var fractionCompleted: Double = 0
770
- var state: State = .downloading
771
-
772
- enum State: Sendable { case downloading, completed, failed }
773
- }
774
-
775
- func updateProgress(for url: URL, fraction: Double) {
776
- downloads[url, default: DownloadProgress()].fractionCompleted = fraction
575
+ @MainActor @Observable
576
+ final class TransferBoard {
577
+ struct Entry {
578
+ enum Phase { case running, done, failed }
579
+ var fraction: Double = 0
580
+ var phase: Phase = .running
777
581
  }
778
582
 
779
- func markCompleted(for url: URL) {
780
- downloads[url]?.state = .completed
781
- downloads[url]?.fractionCompleted = 1.0
782
- }
583
+ static let shared = TransferBoard()
584
+ private(set) var entries: [URL: Entry] = [:]
783
585
 
784
- func markFailed(for url: URL) {
785
- downloads[url]?.state = .failed
786
- }
586
+ func update(_ url: URL, fraction: Double) { entries[url, default: Entry()].fraction = fraction }
587
+ func finish(_ url: URL) { entries[url] = Entry(fraction: 1, phase: .done) }
588
+ func fail(_ url: URL) { entries[url, default: Entry()].phase = .failed }
787
589
  }
788
- ```
789
-
790
- Wire the delegate to the tracker:
791
590
 
792
- ```swift
793
- extension BackgroundDownloadManager {
794
- // Called from delegate methods; dispatches to MainActor
795
- func reportProgress(for url: URL, fraction: Double) {
796
- Task { @MainActor in
797
- downloadTracker.updateProgress(for: url, fraction: fraction)
798
- }
591
+ extension BackgroundTransferService {
592
+ func reportProgress(_ fraction: Double, for url: URL) {
593
+ Task { @MainActor in TransferBoard.shared.update(url, fraction: fraction) }
799
594
  }
800
595
  }
801
596
  ```
802
597
 
803
- ---
804
-
805
598
  ## WebSocket Authentication
806
599
 
807
- WebSocket connections often require authentication via a token in the
808
- initial handshake (either as a query parameter or a custom header).
600
+ Authenticate during the opening handshake, with either a query parameter or
601
+ a header:
809
602
 
810
603
  ```swift
811
- func authenticatedWebSocket(
812
- baseURL: URL,
813
- token: String
814
- ) -> URLSessionWebSocketTask {
815
- // Option 1: Token as query parameter
816
- guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: true) else {
817
- preconditionFailure("Invalid URL components for: \(baseURL)")
818
- }
819
- components.queryItems = [URLQueryItem(name: "token", value: token)]
820
- guard let authenticatedURL = components.url else {
821
- preconditionFailure("Failed to construct URL from components")
822
- }
823
- let task = URLSession.shared.webSocketTask(with: authenticatedURL)
824
-
825
- // Option 2: Token as custom header (use URLRequest)
826
- var request = URLRequest(url: baseURL)
827
- request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
828
- let taskWithHeader = URLSession.shared.webSocketTask(with: request)
829
-
830
- return taskWithHeader
604
+ func socketUsingQuery(_ base: URL, token: String, session: URLSession) -> URLSessionWebSocketTask? {
605
+ guard var parts = URLComponents(url: base, resolvingAgainstBaseURL: false) else { return nil }
606
+ parts.queryItems = [URLQueryItem(name: "access_token", value: token)]
607
+ guard let url = parts.url else { return nil }
608
+ return session.webSocketTask(with: url)
831
609
  }
832
- ```
833
610
 
834
- **Prefer the header approach** when the server supports it. Query
835
- parameters may appear in server access logs, which is a security
836
- concern for tokens.
611
+ func socketUsingHeader(_ url: URL, token: String, session: URLSession) -> URLSessionWebSocketTask {
612
+ var handshake = URLRequest(url: url)
613
+ handshake.allHTTPHeaderFields = ["Authorization": "Bearer " + token]
614
+ return session.webSocketTask(with: handshake)
615
+ }
616
+ ```
837
617
 
838
- ---
618
+ Prefer the header. Query strings tend to end up in server and proxy access
619
+ logs.
839
620
 
840
- ## WebSocket Subprotocol Negotiation
621
+ ## Choosing a subprotocol
841
622
 
842
623
  ```swift
843
- // Request a specific subprotocol (e.g., graphql-ws)
844
- let task = URLSession.shared.webSocketTask(
845
- with: url,
846
- protocols: ["graphql-transport-ws"]
847
- )
848
- task.resume()
849
-
850
- // After connection, verify the negotiated protocol
851
- // via the URLSessionWebSocketDelegate
852
- ```
624
+ let socket = session.webSocketTask(with: endpoint, protocols: ["graphql-transport-ws"])
853
625
 
854
- ```swift
855
- extension WebSocketConnection: URLSessionWebSocketDelegate {
856
- nonisolated func urlSession(
857
- _ session: URLSession,
858
- webSocketTask: URLSessionWebSocketTask,
859
- didOpenWithProtocol protocol: String?
860
- ) {
861
- print("Connected with protocol: \(`protocol` ?? "none")")
862
- }
863
-
864
- nonisolated func urlSession(
865
- _ session: URLSession,
866
- webSocketTask: URLSessionWebSocketTask,
867
- didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
868
- reason: Data?
869
- ) {
870
- let reasonString = reason.flatMap { String(data: $0, encoding: .utf8) }
871
- print("Closed: \(closeCode) - \(reasonString ?? "no reason")")
626
+ final class SocketObserver: NSObject, URLSessionWebSocketDelegate {
627
+ func urlSession(_: URLSession, webSocketTask socket: URLSessionWebSocketTask,
628
+ didOpenWithProtocol chosen: String?) {
629
+ if chosen != "graphql-transport-ws" { socket.cancel(with: .protocolError, reason: nil) }
630
+ }
631
+
632
+ func urlSession(_: URLSession, webSocketTask _: URLSessionWebSocketTask,
633
+ didCloseWith code: URLSessionWebSocketTask.CloseCode, reason _: Data?) {
634
+ Logger.transfers.info("Socket closed with code \(code.rawValue)")
872
635
  }
873
636
  }
874
637
  ```
638
+
639
+ Check the protocol the server actually chose in `didOpenWithProtocol`. Close
640
+ events report a `CloseCode` and optional reason data.
641
+
642
+ The snippets on this page log through a shared logger:
643
+
644
+ ```swift
645
+ import OSLog
646
+ extension Logger { static let transfers = Logger(subsystem: "app.networking", category: "transfers") }
647
+ ```