@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,869 +1,525 @@
1
- # Image Loading and Caching Patterns
1
+ # Loading, caching and downsampling images
2
2
 
3
- Complete patterns for efficient image handling in iOS apps, from basic AsyncImage usage through production-ready caching and loading pipelines. All patterns use modern Swift async/await and target iOS 26 with Swift 6.3, backward-compatible to iOS 16 unless noted.
3
+ Everything here uses async/await and targets iOS 26 and Swift 6.3.
4
+ The samples need iOS 17 or later because the models use `@Observable`.
4
5
 
5
6
  ## Contents
6
- - AsyncImage Patterns
7
- - NSCache-Based In-Memory Cache
8
- - URLCache-Based Disk Caching
9
- - Image Downsampling with CGImageSource
10
- - Image Prefetching for Lists and Grids
11
- - HEIF/HEIC Handling
12
- - Compression Before Upload
13
- - Memory Budget Management
14
- - Complete Image Loading Pipeline
15
7
 
16
- ## AsyncImage Patterns
8
+ - [AsyncImage](#asyncimage)
9
+ - [In-memory cache on NSCache](#in-memory-cache-on-nscache)
10
+ - [Disk cache with URLCache](#disk-cache-with-urlcache)
11
+ - [Downsampling at decode time](#downsampling-at-decode-time)
12
+ - [Prefetching](#prefetching)
13
+ - [HEIF and HEIC](#heif-and-heic)
14
+ - [Shrinking before upload](#shrinking-before-upload)
15
+ - [Staying inside a memory budget](#staying-inside-a-memory-budget)
16
+ - [One loader for everything](#one-loader-for-everything)
17
+ - [Which layer for which job](#which-layer-for-which-job)
17
18
 
18
- AsyncImage (iOS 15+) provides built-in async image loading from a URL. Suitable for simple cases but has significant limitations for production use.
19
+ ## AsyncImage
19
20
 
20
- ### Basic Usage
21
+ `AsyncImage` (iOS 15+) is fine for a handful of images and weak for anything
22
+ heavier.
21
23
 
22
24
  ```swift
23
- AsyncImage(url: URL(string: "https://example.com/photo.jpg"))
24
- ```
25
-
26
- ### Phase Handling
25
+ AsyncImage(url: logoURL)
27
26
 
28
- ```swift
29
- AsyncImage(url: imageURL) { phase in
27
+ AsyncImage(url: bannerURL) { phase in
30
28
  switch phase {
31
29
  case .empty:
32
30
  ProgressView()
33
- .frame(width: 200, height: 200)
34
- case .success(let image):
35
- image
36
- .resizable()
37
- .aspectRatio(contentMode: .fill)
38
- .frame(width: 200, height: 200)
39
- .clipped()
31
+ case .success(let banner):
32
+ banner.resizable().aspectRatio(contentMode: .fill)
40
33
  case .failure:
41
- Image(systemName: "photo")
42
- .font(.largeTitle)
43
- .foregroundStyle(.secondary)
44
- .frame(width: 200, height: 200)
34
+ Image(systemName: "photo").foregroundStyle(.secondary)
45
35
  @unknown default:
46
36
  EmptyView()
47
37
  }
48
38
  }
49
- ```
50
-
51
- ### Custom Transition
39
+ .frame(width: 200, height: 200)
40
+ .clipped()
52
41
 
53
- ```swift
54
- AsyncImage(url: imageURL, transaction: Transaction(animation: .easeIn(duration: 0.3))) { phase in
55
- switch phase {
56
- case .success(let image):
57
- image
58
- .resizable()
59
- .aspectRatio(contentMode: .fill)
60
- .transition(.opacity)
61
- default:
62
- Color.secondary.opacity(0.2)
42
+ AsyncImage(url: coverURL, transaction: .init(animation: .easeIn(duration: 0.3))) { phase in
43
+ if let cover = phase.image {
44
+ cover.resizable().scaledToFit().transition(.opacity)
45
+ } else {
46
+ Color.gray.opacity(0.2)
63
47
  }
64
48
  }
65
- .frame(width: 200, height: 200)
66
- .clipShape(.rect(cornerRadius: 12))
67
49
  ```
68
50
 
69
- ### Limitations
51
+ What it cannot do:
70
52
 
71
- AsyncImage has several shortcomings for production apps:
53
+ - Cache across redraws. When the view is rebuilt, the image downloads again.
54
+ - Prefetch rows that are about to scroll into view.
55
+ - Use your own `URLSession`. It rides the shared session with no say over cache
56
+ policy.
57
+ - Expose the downloaded bytes, so no processing, downsampling or persistence.
58
+ - Cancel on your terms. Its work lives and dies with the view.
72
59
 
73
- - **No caching across redraws**: images re-download when the view is recreated.
74
- - **No prefetching**: cannot load images ahead of scroll position.
75
- - **No custom URLSession**: uses the shared session with no cache policy control.
76
- - **No access to raw data**: cannot process, downsample, or persist the image data.
77
- - **No cancellation control**: tied entirely to the view lifecycle.
60
+ Past simple, low-volume display, build a loading pipeline like the ones below.
78
61
 
79
- For anything beyond simple, low-volume image display, use a custom loading pipeline.
62
+ ## In-memory cache on NSCache
80
63
 
81
- ---
82
-
83
- ## NSCache-Based In-Memory Cache
84
-
85
- An actor-isolated in-memory image cache backed by NSCache. Thread-safe and automatically evicts entries under memory pressure.
64
+ An actor around `NSCache` is safe to call from anywhere, and `NSCache` drops
65
+ entries on its own when memory gets tight. The actor also coalesces requests:
66
+ two callers asking for the same URL share one download.
86
67
 
87
68
  ```swift
88
69
  import UIKit
89
70
 
90
- actor ImageCache {
91
- static let shared = ImageCache()
71
+ actor PosterCache {
72
+ static let shared = PosterCache()
92
73
 
93
- private let cache = NSCache<NSString, UIImage>()
94
- private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
74
+ private let store: NSCache<NSString, UIImage> = {
75
+ let cache = NSCache<NSString, UIImage>()
76
+ cache.countLimit = 100
77
+ cache.totalCostLimit = 50 * 1024 * 1024
78
+ return cache
79
+ }()
80
+ private var inFlight: [URL: Task<UIImage?, Never>] = [:]
95
81
 
96
- init(countLimit: Int = 100, totalCostLimit: Int = 50 * 1024 * 1024) {
97
- cache.countLimit = countLimit
98
- cache.totalCostLimit = totalCostLimit // 50 MB default
99
- }
82
+ private func key(_ address: URL) -> NSString { address.absoluteString as NSString }
100
83
 
101
- func image(for url: URL) -> UIImage? {
102
- cache.object(forKey: url.absoluteString as NSString)
84
+ func image(for address: URL) -> UIImage? {
85
+ store.object(forKey: key(address))
103
86
  }
104
87
 
105
- func store(_ image: UIImage, for url: URL) {
106
- let cost = image.jpegData(compressionQuality: 1.0)?.count ?? 0
107
- cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
88
+ func store(_ picture: UIImage, for address: URL) {
89
+ store.setObject(picture, forKey: key(address), cost: Self.bitmapBytes(picture))
108
90
  }
109
91
 
110
- func removeImage(for url: URL) {
111
- cache.removeObject(forKey: url.absoluteString as NSString)
92
+ func removeImage(for address: URL) {
93
+ store.removeObject(forKey: key(address))
112
94
  }
113
95
 
114
96
  func removeAll() {
115
- cache.removeAllObjects()
116
- inFlightTasks.removeAll()
117
- }
118
-
119
- /// Fetch an image with request coalescing. Multiple callers for the same URL
120
- /// share a single network request.
121
- func fetch(from url: URL, session: URLSession = .shared) async -> UIImage? {
122
- // Return cached image immediately
123
- if let cached = image(for: url) {
124
- return cached
125
- }
126
-
127
- // Coalesce duplicate in-flight requests
128
- if let existingTask = inFlightTasks[url] {
129
- return await existingTask.value
130
- }
131
-
132
- let task = Task<UIImage?, Never> {
133
- defer { inFlightTasks[url] = nil }
134
-
135
- guard let (data, response) = try? await session.data(from: url),
136
- let httpResponse = response as? HTTPURLResponse,
137
- httpResponse.statusCode == 200,
138
- let image = UIImage(data: data) else {
139
- return nil
140
- }
141
-
142
- store(image, for: url)
143
- return image
97
+ store.removeAllObjects()
98
+ inFlight.values.forEach { $0.cancel() }
99
+ inFlight.removeAll()
100
+ }
101
+
102
+ func fetch(from address: URL, session: URLSession = .shared) async -> UIImage? {
103
+ if let hit = image(for: address) { return hit }
104
+ if let running = inFlight[address] { return await running.value }
105
+ let download = Task { () -> UIImage? in
106
+ guard let (body, reply) = try? await session.data(from: address),
107
+ (reply as? HTTPURLResponse)?.statusCode == 200 else { return nil }
108
+ return UIImage(data: body)
144
109
  }
110
+ inFlight[address] = download
111
+ defer { inFlight[address] = nil }
112
+ let result = await download.value
113
+ if let result { store(result, for: address) }
114
+ return result
115
+ }
145
116
 
146
- inFlightTasks[url] = task
147
- return await task.value
117
+ static func bitmapBytes(_ image: UIImage) -> Int {
118
+ guard let bitmap = image.cgImage else { return 0 }
119
+ return bitmap.bytesPerRow * bitmap.height
148
120
  }
149
121
  }
150
122
  ```
151
123
 
152
- ### Usage in SwiftUI
124
+ Price each entry by its decoded bitmap size. Encoding a JPEG just to measure an
125
+ entry is slow and reports compressed bytes, which is not what the cache holds
126
+ in memory.
153
127
 
154
128
  ```swift
155
- struct CachedImageView: View {
129
+ struct PosterView: View {
156
130
  let url: URL
157
- @State private var image: UIImage?
131
+ @State private var poster: UIImage?
158
132
 
159
133
  var body: some View {
160
134
  Group {
161
- if let image {
162
- Image(uiImage: image)
163
- .resizable()
164
- .aspectRatio(contentMode: .fill)
165
- } else {
166
- Color.secondary.opacity(0.2)
167
- .overlay(ProgressView())
168
- }
169
- }
170
- .task(id: url) {
171
- image = await ImageCache.shared.fetch(from: url)
135
+ if let poster { Image(uiImage: poster).resizable().scaledToFill() } else { ProgressView() }
172
136
  }
137
+ .task(id: url) { poster = await PosterCache.shared.fetch(from: url) }
173
138
  }
174
139
  }
175
140
  ```
176
141
 
177
- ---
178
-
179
- ## URLCache-Based Disk Caching
142
+ ## Disk cache with URLCache
180
143
 
181
- Configure URLCache for persistent image caching that survives app restarts. URLCache handles HTTP cache headers automatically.
182
-
183
- ### Configuring a Dedicated URLSession
144
+ `URLCache` survives relaunches and honours the server's HTTP cache headers with
145
+ no extra code. Give images their own session and cache:
184
146
 
185
147
  ```swift
186
- enum ImageSessionConfiguration {
187
- static func makeSession() -> URLSession {
188
- let config = URLSessionConfiguration.default
189
-
190
- // 50 MB memory / 200 MB disk
191
- config.urlCache = URLCache(
192
- memoryCapacity: 50 * 1024 * 1024,
193
- diskCapacity: 200 * 1024 * 1024,
194
- directory: cacheDirectory
195
- )
196
-
197
- config.requestCachePolicy = .returnCacheDataElseLoad
198
- config.httpMaximumConnectionsPerHost = 6
199
-
200
- return URLSession(configuration: config)
201
- }
202
-
203
- private static var cacheDirectory: URL {
204
- FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
205
- .appendingPathComponent("ImageCache", isDirectory: true)
206
- }
148
+ enum ImageNetwork {
149
+ static let session: URLSession = {
150
+ let folder = URL.cachesDirectory.appending(path: "ImageCache")
151
+ let megabyte = 1024 * 1024
152
+ let setup = URLSessionConfiguration.default
153
+ setup.urlCache = URLCache(memoryCapacity: 50 * megabyte,
154
+ diskCapacity: 200 * megabyte,
155
+ directory: folder)
156
+ setup.requestCachePolicy = .returnCacheDataElseLoad
157
+ setup.httpMaximumConnectionsPerHost = 6
158
+ return URLSession(configuration: setup)
159
+ }()
207
160
  }
208
- ```
209
161
 
210
- ### Cache-Aware Request
162
+ let cached = URLRequest(url: artURL, cachePolicy: .returnCacheDataElseLoad)
163
+ let fresh = URLRequest(url: artURL, cachePolicy: .reloadIgnoringLocalCacheData)
211
164
 
212
- ```swift
213
- func cachedImageData(from url: URL, session: URLSession) async throws -> Data {
214
- let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)
215
- let (data, _) = try await session.data(for: request)
216
- return data
217
- }
165
+ ImageNetwork.session.configuration.urlCache?.removeAllCachedResponses()
166
+ ImageNetwork.session.configuration.urlCache?.removeCachedResponse(for: cached)
218
167
  ```
219
168
 
220
- ### Force Refresh (Bypass Cache)
221
-
222
- ```swift
223
- func refreshImageData(from url: URL, session: URLSession) async throws -> Data {
224
- let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
225
- let (data, _) = try await session.data(for: request)
226
- return data
227
- }
228
- ```
169
+ ## Downsampling at decode time
229
170
 
230
- ### Cache Cleanup
231
-
232
- ```swift
233
- func cleanImageCache(session: URLSession) {
234
- session.configuration.urlCache?.removeAllCachedResponses()
235
- }
236
-
237
- func removeCachedImage(for url: URL, session: URLSession) {
238
- let request = URLRequest(url: url)
239
- session.configuration.urlCache?.removeCachedResponse(for: request)
240
- }
241
- ```
242
-
243
- ---
244
-
245
- ## Image Downsampling with CGImageSource
246
-
247
- Loading a full-resolution image into memory then scaling it in the view wastes significant memory. Downsampling at decode time creates a smaller bitmap directly.
248
-
249
- ### Downsample Function
171
+ Decoding at full size and letting the view scale it down wastes memory.
172
+ `CGImageSource` can decode straight into a small bitmap instead.
250
173
 
251
174
  ```swift
252
175
  import ImageIO
253
- import UIKit
254
176
 
255
- func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
256
- let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
257
-
258
- let options: [CFString: Any] = [
259
- kCGImageSourceCreateThumbnailFromImageAlways: true,
260
- kCGImageSourceShouldCacheImmediately: true,
261
- kCGImageSourceCreateThumbnailWithTransform: true,
262
- kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
263
- ]
264
-
265
- guard let source = CGImageSourceCreateWithData(data as CFData, nil),
266
- let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
267
- return nil
177
+ enum Shrinker {
178
+ static func fromData(_ bytes: Data, maxPixels: CGFloat) -> UIImage? {
179
+ guard let reader = CGImageSourceCreateWithData(bytes as CFData, nil) else { return nil }
180
+ return render(reader, maxPixels: maxPixels)
268
181
  }
269
182
 
270
- return UIImage(cgImage: cgImage)
271
- }
272
- ```
273
-
274
- ### Downsample from URL
275
-
276
- ```swift
277
- func downsample(url: URL, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {
278
- let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
279
-
280
- let options: [CFString: Any] = [
281
- kCGImageSourceCreateThumbnailFromImageAlways: true,
282
- kCGImageSourceShouldCacheImmediately: true,
283
- kCGImageSourceCreateThumbnailWithTransform: true,
284
- kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
285
- ]
286
-
287
- guard let source = CGImageSourceCreateWithURL(url as CFURL, nil),
288
- let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
289
- return nil
183
+ static func fromFile(_ file: URL, maxPixels: CGFloat) -> UIImage? {
184
+ guard let reader = CGImageSourceCreateWithURL(file as CFURL, nil) else { return nil }
185
+ return render(reader, maxPixels: maxPixels)
290
186
  }
291
187
 
292
- return UIImage(cgImage: cgImage)
188
+ private static func render(_ reader: CGImageSource, maxPixels: CGFloat) -> UIImage? {
189
+ let plan: [CFString: Any] = [
190
+ kCGImageSourceThumbnailMaxPixelSize: maxPixels,
191
+ kCGImageSourceShouldCacheImmediately: true,
192
+ kCGImageSourceCreateThumbnailFromImageAlways: true,
193
+ kCGImageSourceCreateThumbnailWithTransform: true,
194
+ ]
195
+ return CGImageSourceCreateThumbnailAtIndex(reader, 0, plan as CFDictionary).map(UIImage.init(cgImage:))
196
+ }
293
197
  }
294
198
  ```
295
199
 
296
- ### When to Downsample
200
+ Downsample when:
297
201
 
298
- - Displaying thumbnails in a list or grid (a 4032x3024 photo for a 100x100 cell wastes ~46 MB per image).
299
- - User-selected photos from the photo library.
300
- - Images fetched from a server that are larger than the display size.
301
- - Any time the source image dimensions exceed 2x the display point size.
202
+ - showing thumbnails in lists or grids (a 4032 x 3024 photo in a 100 x 100 cell
203
+ wastes roughly 46 MB per cell otherwise);
204
+ - showing photos the person picked from the library;
205
+ - the server sends images larger than they will be drawn;
206
+ - as a rule of thumb, the source is more than twice the display size in points.
302
207
 
303
- ### Memory Savings
208
+ Decoded memory at 3x scale, downsampled against a full decode:
304
209
 
305
- | Source Size | Display Size | Without Downsampling | With Downsampling |
210
+ | Source | Drawn at | Downsampled | Full decode |
306
211
  |---|---|---|---|
307
- | 4032x3024 | 100x100 pt @3x | ~46 MB | ~0.35 MB |
308
- | 4032x3024 | 300x300 pt @3x | ~46 MB | ~3.1 MB |
309
- | 1920x1080 | 100x100 pt @3x | ~7.9 MB | ~0.35 MB |
310
-
311
- ---
212
+ | 4032 x 3024 | 100 x 100 pt | ~0.35 MB | ~46 MB |
213
+ | 4032 x 3024 | 300 x 300 pt | ~3.1 MB | ~46 MB |
214
+ | 1920 x 1080 | 100 x 100 pt | ~0.35 MB | ~7.9 MB |
312
215
 
313
- ## Image Prefetching for Lists and Grids
216
+ ## Prefetching
314
217
 
315
- Prefetch images before they scroll into view. Works with both UICollectionView data source prefetching and SwiftUI List.
316
-
317
- ### Prefetch Coordinator
218
+ Start downloads a little before cells appear. This pairs with
219
+ `UICollectionView` prefetching and with SwiftUI grids alike.
318
220
 
319
221
  ```swift
320
- @Observable
321
- @MainActor
322
- final class ImagePrefetcher {
323
- private let cache: ImageCache
324
- private let session: URLSession
325
- private var prefetchTasks: [URL: Task<Void, Never>] = [:]
326
-
327
- init(cache: ImageCache = .shared,
328
- session: URLSession = ImageSessionConfiguration.makeSession()) {
329
- self.cache = cache
330
- self.session = session
331
- }
222
+ @Observable @MainActor
223
+ final class ArtPrefetcher {
224
+ private var jobs: [URL: Task<Void, Never>] = [:]
332
225
 
333
226
  func prefetch(urls: [URL]) {
334
- for url in urls {
335
- guard prefetchTasks[url] == nil else { continue }
336
-
337
- prefetchTasks[url] = Task {
338
- _ = await cache.fetch(from: url, session: session)
339
- prefetchTasks[url] = nil
227
+ for url in urls where jobs[url] == nil {
228
+ jobs[url] = Task {
229
+ _ = await PosterCache.shared.fetch(from: url)
230
+ jobs[url] = nil
340
231
  }
341
232
  }
342
233
  }
343
234
 
344
235
  func cancelPrefetch(urls: [URL]) {
345
236
  for url in urls {
346
- prefetchTasks[url]?.cancel()
347
- prefetchTasks[url] = nil
237
+ jobs[url]?.cancel()
238
+ jobs[url] = nil
348
239
  }
349
240
  }
350
241
 
351
242
  func cancelAll() {
352
- prefetchTasks.values.forEach { $0.cancel() }
353
- prefetchTasks.removeAll()
243
+ jobs.values.forEach { $0.cancel() }
244
+ jobs.removeAll()
354
245
  }
355
246
  }
356
- ```
357
-
358
- ### SwiftUI Integration with ScrollView and LazyVGrid
359
-
360
- ```swift
361
- struct PhotoGrid: View {
362
- let photos: [Photo]
363
- @State private var prefetcher = ImagePrefetcher()
364
247
 
248
+ struct ArtGrid: View {
249
+ let urls: [URL]
250
+ @State private var prefetcher = ArtPrefetcher()
365
251
  private let columns = [GridItem(.adaptive(minimum: 100), spacing: 2)]
366
252
 
367
253
  var body: some View {
368
254
  ScrollView {
369
- LazyVGrid(columns: columns, spacing: 2) {
370
- ForEach(photos) { photo in
371
- CachedImageView(url: photo.thumbnailURL)
372
- .aspectRatio(1, contentMode: .fill)
373
- .clipped()
255
+ LazyVGrid(columns: columns) {
256
+ ForEach(Array(urls.enumerated()), id: \.offset) { index, url in
257
+ PosterView(url: url)
258
+ .frame(height: 100)
374
259
  .onAppear {
375
- prefetchNearby(photo)
260
+ let ahead = urls[(index + 1)..<min(index + 11, urls.count)]
261
+ prefetcher.prefetch(urls: Array(ahead))
376
262
  }
377
263
  }
378
264
  }
379
265
  }
380
- .onDisappear {
381
- prefetcher.cancelAll()
382
- }
383
- }
384
-
385
- private func prefetchNearby(_ photo: Photo) {
386
- guard let index = photos.firstIndex(where: { $0.id == photo.id }) else { return }
387
- let prefetchRange = (index + 1)..<min(index + 10, photos.count)
388
- let urls = prefetchRange.map { photos[$0].thumbnailURL }
389
- prefetcher.prefetch(urls: urls)
266
+ .onDisappear { prefetcher.cancelAll() }
390
267
  }
391
268
  }
392
269
  ```
393
270
 
394
- ### UICollectionView Prefetching (UIKit Interop)
271
+ In UIKit, a `UICollectionViewDataSourcePrefetching` object maps index paths to
272
+ URLs in `collectionView(_:prefetchItemsAt:)` and calls `cancelPrefetch` from
273
+ `collectionView(_:cancelPrefetchingForItemsAt:)`.
395
274
 
396
- ```swift
397
- final class PhotoCollectionPrefetcher: NSObject, UICollectionViewDataSourcePrefetching {
398
- private let prefetcher = ImagePrefetcher()
399
- private let photos: [Photo]
275
+ ## HEIF and HEIC
400
276
 
401
- init(photos: [Photo]) {
402
- self.photos = photos
403
- }
404
-
405
- func collectionView(_ collectionView: UICollectionView,
406
- prefetchItemsAt indexPaths: [IndexPath]) {
407
- let urls = indexPaths.map { photos[$0.item].thumbnailURL }
408
- prefetcher.prefetch(urls: urls)
409
- }
410
-
411
- func collectionView(_ collectionView: UICollectionView,
412
- cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
413
- let urls = indexPaths.map { photos[$0.item].thumbnailURL }
414
- prefetcher.cancelPrefetch(urls: urls)
415
- }
416
- }
417
- ```
418
-
419
- ---
420
-
421
- ## HEIF/HEIC Handling
422
-
423
- HEIF (High Efficiency Image Format) is the default camera format on modern iPhones. Handle detection, display, and conversion.
424
-
425
- ### Detection
277
+ HEIF is what current iPhones shoot by default.
426
278
 
427
279
  ```swift
428
280
  import UniformTypeIdentifiers
429
281
 
430
- func isHEIF(data: Data) -> Bool {
431
- guard data.count >= 12 else { return false }
432
- // Check for 'ftyp' box at byte 4
433
- let ftypRange = data[4..<8]
434
- return ftypRange.elementsEqual("ftyp".utf8)
435
- }
436
-
437
- func isHEIF(url: URL) -> Bool {
438
- guard let type = UTType(filenameExtension: url.pathExtension) else { return false }
439
- return type.conforms(to: .heif) || type.conforms(to: .heic)
440
- }
441
- ```
442
-
443
- ### Conversion to JPEG
444
-
445
- ```swift
446
- func convertHEICToJPEG(data: Data, compressionQuality: CGFloat = 0.9) -> Data? {
447
- guard let image = UIImage(data: data) else { return nil }
448
- return image.jpegData(compressionQuality: compressionQuality)
449
- }
450
- ```
451
-
452
- ### Conversion to PNG
453
-
454
- ```swift
455
- func convertHEICToPNG(data: Data) -> Data? {
456
- guard let image = UIImage(data: data) else { return nil }
457
- return image.pngData()
458
- }
459
- ```
460
-
461
- ### Conversion with CGImageDestination (More Control)
462
-
463
- ```swift
464
- import ImageIO
465
-
466
- func convertHEICToJPEG(sourceData: Data,
467
- quality: CGFloat = 0.9,
468
- preserveMetadata: Bool = true) -> Data? {
469
- guard let source = CGImageSourceCreateWithData(sourceData as CFData, nil),
470
- let cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil) else {
471
- return nil
282
+ enum HEIFTools {
283
+ static func looksLikeHEIF(_ bytes: Data) -> Bool {
284
+ guard bytes.count >= 12 else { return false }
285
+ return bytes[bytes.startIndex + 4 ..< bytes.startIndex + 8] == Data("ftyp".utf8)
472
286
  }
473
287
 
474
- let mutableData = NSMutableData()
475
- guard let destination = CGImageDestinationCreateWithData(
476
- mutableData, UTType.jpeg.identifier as CFString, 1, nil
477
- ) else {
478
- return nil
288
+ static func isHEIF(fileExtension: String) -> Bool {
289
+ guard let kind = UTType(filenameExtension: fileExtension) else { return false }
290
+ return kind.conforms(to: .heif) || kind.conforms(to: .heic)
479
291
  }
480
292
 
481
- var options: [CFString: Any] = [
482
- kCGImageDestinationLossyCompressionQuality: quality
483
- ]
293
+ static func jpeg(from photo: UIImage, quality: CGFloat = 0.9) -> Data? {
294
+ photo.jpegData(compressionQuality: quality)
295
+ }
484
296
 
485
- // Preserve EXIF, GPS, and other metadata
486
- if preserveMetadata,
487
- let metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) {
488
- options[kCGImageDestinationMergeMetadata] = true
489
- CGImageDestinationAddImage(destination, cgImage, metadata)
490
- } else {
491
- CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
297
+ static func png(from photo: UIImage) -> Data? {
298
+ photo.pngData()
492
299
  }
493
300
 
494
- guard CGImageDestinationFinalize(destination) else { return nil }
495
- return mutableData as Data
301
+ static func jpegKeepingMetadata(_ heif: Data, quality: CGFloat = 0.9) -> Data? {
302
+ guard let reader = CGImageSourceCreateWithData(heif as CFData, nil),
303
+ let frame = CGImageSourceCreateImageAtIndex(reader, 0, nil) else { return nil }
304
+ let sink = NSMutableData()
305
+ let jpegID = UTType.jpeg.identifier as CFString
306
+ guard let writer = CGImageDestinationCreateWithData(sink, jpegID, 1, nil)
307
+ else { return nil }
308
+ var properties = (CGImageSourceCopyPropertiesAtIndex(reader, 0, nil) as? [CFString: Any]) ?? [:]
309
+ properties[kCGImageDestinationLossyCompressionQuality] = quality
310
+ properties[kCGImageDestinationMergeMetadata] = true
311
+ CGImageDestinationAddImage(writer, frame, properties as CFDictionary)
312
+ return CGImageDestinationFinalize(writer) ? sink as Data : nil
313
+ }
496
314
  }
497
315
  ```
498
316
 
499
- ---
500
-
501
- ## Compression Before Upload
317
+ The `ftyp` check is a cheap first look at the container box; confirm with
318
+ `CGImageSourceGetType` when it matters. The metadata-preserving path copies the
319
+ source properties (EXIF, GPS) into the destination; the `UIImage` paths drop
320
+ them.
502
321
 
503
- Reduce file size before uploading to a server. Balance quality and size based on the use case.
504
-
505
- ### JPEG Compression with Target Size
322
+ ## Shrinking before upload
506
323
 
507
324
  ```swift
508
- func compressForUpload(image: UIImage,
509
- maxBytes: Int = 1_000_000,
510
- initialQuality: CGFloat = 0.9) -> Data? {
511
- var quality = initialQuality
512
-
513
- while quality > 0.1 {
514
- guard let data = image.jpegData(compressionQuality: quality) else { return nil }
515
- if data.count <= maxBytes {
516
- return data
325
+ enum UploadPrep {
326
+ static func jpeg(_ photo: UIImage, under maxBytes: Int = 1_000_000) -> Data? {
327
+ var quality: CGFloat = 0.9
328
+ while quality > 0.1 {
329
+ if let encoded = photo.jpegData(compressionQuality: quality), encoded.count <= maxBytes {
330
+ return encoded
331
+ }
332
+ quality -= 0.1
517
333
  }
518
- quality -= 0.1
519
- }
520
-
521
- // Final attempt at minimum quality
522
- return image.jpegData(compressionQuality: 0.1)
523
- }
524
- ```
525
-
526
- ### Resize and Compress
527
-
528
- ```swift
529
- func resizeAndCompress(image: UIImage,
530
- maxDimension: CGFloat = 1920,
531
- compressionQuality: CGFloat = 0.8) -> Data? {
532
- let size = image.size
533
- let scale: CGFloat
534
-
535
- if max(size.width, size.height) > maxDimension {
536
- scale = maxDimension / max(size.width, size.height)
537
- } else {
538
- scale = 1.0
334
+ return photo.jpegData(compressionQuality: 0.1)
539
335
  }
540
336
 
541
- let newSize = CGSize(width: size.width * scale, height: size.height * scale)
542
-
543
- let renderer = UIGraphicsImageRenderer(size: newSize)
544
- let resized = renderer.image { _ in
545
- image.draw(in: CGRect(origin: .zero, size: newSize))
337
+ static func resized(_ photo: UIImage, longestSide: CGFloat = 1920) -> Data? {
338
+ let ratio = min(1, longestSide / max(photo.size.width, photo.size.height))
339
+ let target = CGSize(width: photo.size.width * ratio, height: photo.size.height * ratio)
340
+ let redrawn = UIGraphicsImageRenderer(size: target).image { _ in
341
+ photo.draw(in: CGRect(x: 0, y: 0, width: target.width, height: target.height))
342
+ }
343
+ return redrawn.jpegData(compressionQuality: 0.8)
546
344
  }
547
345
 
548
- return resized.jpegData(compressionQuality: compressionQuality)
549
- }
550
- ```
551
-
552
- ### HEIF Compression (Smaller Files)
553
-
554
- ```swift
555
- func compressAsHEIF(image: UIImage, quality: CGFloat = 0.8) -> Data? {
556
- guard let cgImage = image.cgImage else { return nil }
557
-
558
- let mutableData = NSMutableData()
559
- guard let destination = CGImageDestinationCreateWithData(
560
- mutableData, UTType.heic.identifier as CFString, 1, nil
561
- ) else {
562
- return nil
346
+ static func heic(_ photo: UIImage, level: CGFloat = 0.8) -> Data? {
347
+ guard let bitmap = photo.cgImage else { return nil }
348
+ let sink = NSMutableData()
349
+ let heicID = UTType.heic.identifier as CFString
350
+ guard let writer = CGImageDestinationCreateWithData(sink, heicID, 1, nil)
351
+ else { return nil }
352
+ CGImageDestinationAddImage(writer, bitmap, [kCGImageDestinationLossyCompressionQuality: level] as CFDictionary)
353
+ return CGImageDestinationFinalize(writer) ? sink as Data : nil
563
354
  }
564
-
565
- let options: [CFString: Any] = [
566
- kCGImageDestinationLossyCompressionQuality: quality
567
- ]
568
-
569
- CGImageDestinationAddImage(destination, cgImage, options as CFDictionary)
570
- guard CGImageDestinationFinalize(destination) else { return nil }
571
-
572
- return mutableData as Data
573
355
  }
574
356
  ```
575
357
 
576
- ---
577
-
578
- ## Memory Budget Management
358
+ Pick the trade-off per use: the quality ladder steps down from 0.9 in 0.1
359
+ steps until the payload fits, with 0.1 as the floor; resizing caps the long
360
+ edge first; HEIC gives smaller files at the same visual quality.
579
361
 
580
- Monitor and respond to memory pressure to keep your image pipeline stable.
581
-
582
- ### Memory Warning Observer
362
+ ## Staying inside a memory budget
583
363
 
584
364
  ```swift
585
- @Observable
586
- @MainActor
587
- final class MemoryMonitor {
588
- var isUnderPressure = false
365
+ @Observable @MainActor
366
+ final class PressureWatch {
367
+ private(set) var underPressure = false
368
+ @ObservationIgnored private var listener: Task<Void, Never>?
369
+ private let cooldown: Duration = .seconds(10)
589
370
 
590
371
  init() {
591
- NotificationCenter.default.addObserver(
592
- forName: UIApplication.didReceiveMemoryWarningNotification,
593
- object: nil,
594
- queue: .main
595
- ) { [weak self] _ in
596
- self?.handleMemoryWarning()
372
+ listener = Task { [weak self] in
373
+ let warnings = NotificationCenter.default.notifications(named: UIApplication.didReceiveMemoryWarningNotification)
374
+ for await _ in warnings { self?.react() }
597
375
  }
598
376
  }
599
377
 
600
- private func handleMemoryWarning() {
601
- isUnderPressure = true
602
-
603
- Task {
604
- await ImageCache.shared.removeAll()
605
- }
606
-
607
- // Reset after a delay
378
+ private func react() {
379
+ underPressure = true
608
380
  Task {
609
- try? await Task.sleep(for: .seconds(10))
610
- isUnderPressure = false
381
+ await PosterCache.shared.removeAll()
382
+ try? await Task.sleep(for: cooldown)
383
+ underPressure = false
611
384
  }
612
385
  }
613
386
  }
614
- ```
615
387
 
616
- ### Process Memory Usage
617
-
618
- ```swift
619
- func currentMemoryUsageMB() -> Double {
620
- var info = mach_task_basic_info()
621
- var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
622
- let result = withUnsafeMutablePointer(to: &info) {
623
- $0.withMemoryRebound(to: integer_t.self, capacity: Int(count)) {
624
- task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
388
+ func residentMemory() -> UInt64 {
389
+ var usage = mach_task_basic_info()
390
+ var words = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size / MemoryLayout<natural_t>.size)
391
+ let slotCount = Int(words)
392
+ let status = withUnsafeMutablePointer(to: &usage) { raw in
393
+ raw.withMemoryRebound(to: integer_t.self, capacity: slotCount) { slots in
394
+ task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), slots, &words)
625
395
  }
626
396
  }
627
- guard result == KERN_SUCCESS else { return 0 }
628
- return Double(info.resident_size) / (1024 * 1024)
397
+ return status == KERN_SUCCESS ? usage.resident_size : 0
629
398
  }
630
399
  ```
631
400
 
632
- ### Adaptive Cache Sizing
401
+ Size the cache from the device instead of a constant:
633
402
 
634
403
  ```swift
635
- actor AdaptiveImageCache {
636
- private let cache = NSCache<NSString, UIImage>()
637
-
638
- init() {
639
- let totalMemory = ProcessInfo.processInfo.physicalMemory
640
- // Use at most 10% of physical memory for image cache
641
- let budgetBytes = Int(totalMemory / 10)
642
- cache.totalCostLimit = budgetBytes
643
- cache.countLimit = 200
644
- }
645
-
646
- func image(for url: URL) -> UIImage? {
647
- cache.object(forKey: url.absoluteString as NSString)
648
- }
649
-
650
- func store(_ image: UIImage, for url: URL) {
651
- let cost = cgImageMemorySize(image)
652
- cache.setObject(image, forKey: url.absoluteString as NSString, cost: cost)
653
- }
654
-
655
- /// Estimate the decoded bitmap size in bytes.
656
- private func cgImageMemorySize(_ image: UIImage) -> Int {
657
- guard let cgImage = image.cgImage else { return 0 }
658
- return cgImage.bytesPerRow * cgImage.height
659
- }
660
-
661
- func purge() {
662
- cache.removeAllObjects()
663
- }
664
- }
404
+ let adaptive = NSCache<NSString, UIImage>()
405
+ adaptive.totalCostLimit = Int(ProcessInfo.processInfo.physicalMemory / 10)
406
+ adaptive.countLimit = 200
665
407
  ```
666
408
 
667
- ---
668
-
669
- ## Complete Image Loading Pipeline
409
+ Charge each entry `cgImage.bytesPerRow * cgImage.height`, its real decoded
410
+ footprint.
670
411
 
671
- A production-ready pipeline that combines in-memory caching, disk caching via URLCache, downsampling, and request coalescing.
412
+ ## One loader for everything
672
413
 
673
- ### ImageLoader Actor
414
+ `GalleryLoader` stacks the layers: memory cache, `URLCache` on disk,
415
+ downsampling and request coalescing.
674
416
 
675
417
  ```swift
676
- import UIKit
677
- import ImageIO
678
-
679
- actor ImageLoader {
680
- static let shared = ImageLoader()
418
+ actor GalleryLoader {
419
+ static let shared = GalleryLoader()
681
420
 
682
- private let memoryCache = NSCache<NSString, UIImage>()
683
- private let session: URLSession
684
- private var inFlightTasks: [URL: Task<UIImage?, Never>] = [:]
685
-
686
- init() {
687
- let config = URLSessionConfiguration.default
688
- config.urlCache = URLCache(
689
- memoryCapacity: 50 * 1024 * 1024,
690
- diskCapacity: 200 * 1024 * 1024
691
- )
692
- config.requestCachePolicy = .returnCacheDataElseLoad
693
- config.httpMaximumConnectionsPerHost = 6
694
- session = URLSession(configuration: config)
695
-
696
- let totalMemory = ProcessInfo.processInfo.physicalMemory
697
- memoryCache.totalCostLimit = Int(totalMemory / 10)
698
- memoryCache.countLimit = 200
699
- }
700
-
701
- /// Load an image, optionally downsampling to the given display size.
702
- func load(from url: URL, displaySize: CGSize? = nil) async -> UIImage? {
703
- let cacheKey = cacheKey(url: url, size: displaySize)
704
-
705
- // 1. Check memory cache
706
- if let cached = memoryCache.object(forKey: cacheKey as NSString) {
707
- return cached
708
- }
709
-
710
- // 2. Coalesce duplicate in-flight requests
711
- if let existing = inFlightTasks[url] {
712
- return await existing.value
713
- }
714
-
715
- let task = Task<UIImage?, Never> {
716
- defer { inFlightTasks[url] = nil }
717
-
718
- guard let (data, response) = try? await session.data(from: url),
719
- let httpResponse = response as? HTTPURLResponse,
720
- httpResponse.statusCode == 200 else {
721
- return nil
722
- }
723
-
724
- // 3. Downsample if a display size is provided
725
- let image: UIImage?
421
+ private let memory: NSCache<NSString, UIImage> = {
422
+ let cache = NSCache<NSString, UIImage>()
423
+ cache.totalCostLimit = Int(ProcessInfo.processInfo.physicalMemory / 10)
424
+ cache.countLimit = 200
425
+ return cache
426
+ }()
427
+ private let network = ImageNetwork.session
428
+ private var running: [String: Task<UIImage?, Never>] = [:]
429
+
430
+ private func key(_ url: URL, _ size: CGSize?) -> String {
431
+ guard let size else { return url.absoluteString }
432
+ let wide = Int(size.width), tall = Int(size.height)
433
+ return "\(url.absoluteString)_\(wide)x\(tall)"
434
+ }
435
+
436
+ func load(_ url: URL, displaySize: CGSize? = nil, scale: CGFloat = 3) async -> UIImage? {
437
+ let slot = key(url, displaySize)
438
+ if let hit = memory.object(forKey: slot as NSString) { return hit }
439
+ if let shared = running[slot] { return await shared.value }
440
+
441
+ let session = network
442
+ let source = url
443
+ let job = Task { () -> UIImage? in
444
+ guard let (body, _) = try? await session.data(from: source) else { return nil }
726
445
  if let displaySize {
727
- image = Self.downsample(data: data, to: displaySize)
728
- } else {
729
- image = UIImage(data: data)
730
- }
731
-
732
- // 4. Store in memory cache
733
- if let image {
734
- let cost = Self.bitmapSize(of: image)
735
- memoryCache.setObject(image, forKey: cacheKey as NSString, cost: cost)
446
+ return Shrinker.fromData(body, maxPixels: max(displaySize.width, displaySize.height) * scale)
736
447
  }
737
-
738
- return image
448
+ return UIImage(data: body)
739
449
  }
450
+ running[slot] = job
451
+ defer { running[slot] = nil }
740
452
 
741
- inFlightTasks[url] = task
742
- return await task.value
453
+ guard let image = await job.value else { return nil }
454
+ let cost = image.cgImage.map { $0.bytesPerRow * $0.height } ?? 0
455
+ memory.setObject(image, forKey: slot as NSString, cost: cost)
456
+ return image
743
457
  }
744
458
 
745
- /// Remove a specific URL from the memory cache.
746
- func evict(url: URL, displaySize: CGSize? = nil) {
747
- let key = cacheKey(url: url, size: displaySize)
748
- memoryCache.removeObject(forKey: key as NSString)
459
+ func evict(url target: URL, displaySize box: CGSize? = nil) {
460
+ memory.removeObject(forKey: key(target, box) as NSString)
749
461
  }
750
462
 
751
- /// Purge all in-memory cached images.
752
463
  func purgeMemoryCache() {
753
- memoryCache.removeAllObjects()
754
- inFlightTasks.values.forEach { $0.cancel() }
755
- inFlightTasks.removeAll()
756
- }
757
-
758
- // MARK: - Private
759
-
760
- private func cacheKey(url: URL, size: CGSize?) -> String {
761
- if let size {
762
- return "\(url.absoluteString)_\(Int(size.width))x\(Int(size.height))"
763
- }
764
- return url.absoluteString
765
- }
766
-
767
- private static func downsample(data: Data, to pointSize: CGSize) -> UIImage? {
768
- let scale = UITraitCollection.current.displayScale
769
- let maxDimension = max(pointSize.width, pointSize.height) * scale
770
-
771
- let options: [CFString: Any] = [
772
- kCGImageSourceCreateThumbnailFromImageAlways: true,
773
- kCGImageSourceShouldCacheImmediately: true,
774
- kCGImageSourceCreateThumbnailWithTransform: true,
775
- kCGImageSourceThumbnailMaxPixelSize: maxDimension
776
- ]
777
-
778
- guard let source = CGImageSourceCreateWithData(data as CFData, nil),
779
- let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
780
- return nil
781
- }
782
-
783
- return UIImage(cgImage: cgImage)
784
- }
785
-
786
- private static func bitmapSize(of image: UIImage) -> Int {
787
- guard let cgImage = image.cgImage else { return 0 }
788
- return cgImage.bytesPerRow * cgImage.height
464
+ memory.removeAllObjects()
465
+ running.values.forEach { $0.cancel() }
466
+ running.removeAll()
789
467
  }
790
468
  }
791
469
  ```
792
470
 
793
- ### SwiftUI View Using the Pipeline
471
+ Order of lookup: memory, then an in-flight task for the same key, then a fetch
472
+ (downsampled when a display size is given, plain decode otherwise), then store
473
+ with the bitmap cost. The key carries the display size, so one URL can be
474
+ cached at several sizes.
794
475
 
795
476
  ```swift
796
- struct PipelineImageView: View {
477
+ struct GalleryTile: View {
797
478
  let url: URL
798
- var displaySize: CGSize = CGSize(width: 300, height: 300)
799
-
800
479
  @State private var image: UIImage?
801
- @State private var isLoading = true
480
+ @State private var failed = false
802
481
 
803
482
  var body: some View {
804
483
  ZStack {
805
484
  if let image {
806
- Image(uiImage: image)
807
- .resizable()
808
- .aspectRatio(contentMode: .fill)
809
- } else if isLoading {
810
- Color.secondary.opacity(0.15)
811
- .overlay(ProgressView())
485
+ Image(uiImage: image).resizable().scaledToFill()
486
+ } else if failed {
487
+ Image(systemName: "photo").foregroundStyle(.secondary)
812
488
  } else {
813
- Color.secondary.opacity(0.15)
814
- .overlay {
815
- Image(systemName: "photo")
816
- .font(.title2)
817
- .foregroundStyle(.secondary)
818
- }
489
+ ProgressView()
819
490
  }
820
491
  }
821
- .frame(width: displaySize.width, height: displaySize.height)
822
- .clipped()
823
492
  .task(id: url) {
824
- isLoading = true
825
- image = await ImageLoader.shared.load(from: url, displaySize: displaySize)
826
- isLoading = false
493
+ image = await GalleryLoader.shared.load(url, displaySize: CGSize(width: 120, height: 120))
494
+ failed = image == nil
827
495
  }
828
496
  }
829
497
  }
830
- ```
831
-
832
- ### Memory Warning Integration
833
498
 
834
- ```swift
835
- struct PhotoGridView: View {
836
- let photos: [Photo]
499
+ struct GalleryScreen: View {
500
+ let urls: [URL]
501
+ private let memoryWarning = NotificationCenter.default.publisher(for: UIApplication.didReceiveMemoryWarningNotification)
837
502
 
838
503
  var body: some View {
839
504
  ScrollView {
840
- LazyVGrid(columns: [GridItem(.adaptive(minimum: 100), spacing: 2)], spacing: 2) {
841
- ForEach(photos) { photo in
842
- PipelineImageView(
843
- url: photo.thumbnailURL,
844
- displaySize: CGSize(width: 100, height: 100)
845
- )
846
- }
505
+ LazyVGrid(columns: [GridItem(.adaptive(minimum: 120))]) {
506
+ ForEach(urls, id: \.self) { GalleryTile(url: $0).frame(height: 120) }
847
507
  }
848
508
  }
849
- .onReceive(NotificationCenter.default.publisher(
850
- for: UIApplication.didReceiveMemoryWarningNotification
851
- )) { _ in
852
- Task {
853
- await ImageLoader.shared.purgeMemoryCache()
854
- }
509
+ .onReceive(memoryWarning) { _ in
510
+ Task { await GalleryLoader.shared.purgeMemoryCache() }
855
511
  }
856
512
  }
857
513
  }
858
514
  ```
859
515
 
860
- ### When to Use Each Layer
516
+ ## Which layer for which job
861
517
 
862
- | Scenario | Recommended Approach |
518
+ | Use | Layers |
863
519
  |---|---|
864
- | Simple profile avatar | AsyncImage |
865
- | Photo grid with scrolling | ImageLoader + downsampling + prefetch |
866
- | Offline-capable gallery | ImageLoader + URLCache disk caching |
867
- | Chat message images | ImageLoader + in-memory cache |
868
- | Full-resolution photo viewer | ImageLoader without downsampling |
869
- | Thumbnail in a widget | Downsample at fetch time, store in app group |
520
+ | A single profile avatar | `AsyncImage` |
521
+ | A scrolling photo grid | loader + downsampling + prefetching |
522
+ | A gallery that works offline | loader + `URLCache` on disk |
523
+ | Images inside chat messages | loader + memory cache |
524
+ | A full-resolution photo viewer | loader, no downsampling |
525
+ | A widget thumbnail | downsample when fetching, store in the app group |