@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,363 +1,327 @@
1
1
  # Representable Recipes
2
2
 
3
- Complete working recipes for common UIKit wrapping scenarios. Each recipe includes the full `UIViewRepresentable` or `UIViewControllerRepresentable` struct, the Coordinator with delegate methods, a SwiftUI usage example, and gotchas specific to that wrapper.
4
-
5
- ---
3
+ Nine wrappers for UIKit surfaces that SwiftUI apps still reach for. Every recipe
4
+ gives the representable itself, its Coordinator and the delegate methods that
5
+ matter, a short SwiftUI call site, and the traps specific to that wrapper. Many
6
+ recipes end with a "Prefer native" note: when a SwiftUI equivalent exists for
7
+ your deployment target, start there and only wrap when you need what it lacks.
8
+
9
+ Web content is deliberately absent. On iOS 26 and later the native WebKit for
10
+ SwiftUI types replace a wrapped web view; see the `swiftui-webkit` skill. This
11
+ file sticks to general representable technique.
12
+
13
+ All recipes assume a deployment target of iOS 16 or later, so `sizeThatFits`
14
+ needs no availability annotation here. Each coordinator keeps a `parent`
15
+ reference and each update method starts by refreshing it with
16
+ `context.coordinator.parent = self`, so callbacks always write through the
17
+ current bindings and closures.
6
18
 
7
19
  ## Contents
8
20
 
9
- - [1. MKMapView Wrapper](#1-mkmapview-wrapper)
10
- - [2. UITextView Wrapper (Attributed Text)](#2-uitextview-wrapper-attributed-text)
11
- - [3. AVCaptureVideoPreviewLayer Wrapper](#3-avcapturevideopreviewlayer-wrapper)
12
- - [4. PHPickerViewController Wrapper](#4-phpickerviewcontroller-wrapper)
13
- - [5. MFMailComposeViewController Wrapper](#5-mfmailcomposeviewcontroller-wrapper)
14
- - [6. UIActivityViewController Wrapper (Share Sheet)](#6-uiactivityviewcontroller-wrapper-share-sheet)
15
- - [7. UISearchBar Wrapper](#7-uisearchbar-wrapper)
16
- - [8. PDFView Wrapper (PDFKit)](#8-pdfview-wrapper-pdfkit)
17
- - [9. MFMessageComposeViewController Wrapper](#9-mfmessagecomposeviewcontroller-wrapper)
18
-
19
- > Native WebKit for SwiftUI now covers modern embedded web content on iOS 26+. See the `swiftui-webkit` skill for `WebView`, `WebPage`, navigation policies, JavaScript calls, and migration guidance. Keep this file focused on generic representable patterns.
21
+ 1. [Map](#1-mkmapview-wrapper)
22
+ 2. [Attributed text view](#2-uitextview-wrapper-attributed-text)
23
+ 3. [Camera preview](#3-avcapturevideopreviewlayer-wrapper)
24
+ 4. [Photo picker](#4-phpickerviewcontroller-wrapper)
25
+ 5. [Mail composer](#5-mfmailcomposeviewcontroller-wrapper)
26
+ 6. [Share sheet](#6-uiactivityviewcontroller-wrapper-share-sheet)
27
+ 7. [Search bar](#7-uisearchbar-wrapper)
28
+ 8. [PDF viewer](#8-pdfview-wrapper-pdfkit)
29
+ 9. [Text message composer](#9-mfmessagecomposeviewcontroller-wrapper)
20
30
 
21
31
  ## 1. MKMapView Wrapper
22
32
 
23
- Display a map with annotations, track region changes, and toggle map type.
24
-
25
33
  ```swift
26
34
  import SwiftUI
27
35
  import MapKit
28
36
 
29
- struct MapViewRepresentable: UIViewRepresentable {
30
- @Binding var region: MKCoordinateRegion
31
- @Binding var mapType: MKMapType
32
- var annotations: [MKPointAnnotation]
33
- var onRegionChanged: ((MKCoordinateRegion) -> Void)?
37
+ struct TrailMapView: UIViewRepresentable {
38
+ @Binding var visibleArea: MKCoordinateRegion
39
+ @Binding var baseMap: MKMapType
40
+ var pins: [MKPointAnnotation]
41
+ var onAreaChanged: ((MKCoordinateRegion) -> Void)?
34
42
 
35
- func makeCoordinator() -> Coordinator { Coordinator(self) }
43
+ private static let centerTolerance = 0.0001
44
+
45
+ func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
36
46
 
37
47
  func makeUIView(context: Context) -> MKMapView {
38
- let mapView = MKMapView()
39
- mapView.delegate = context.coordinator
40
- mapView.showsUserLocation = true
41
- return mapView
48
+ let map = MKMapView(frame: .zero)
49
+ map.showsUserLocation = true
50
+ map.delegate = context.coordinator
51
+ return map
42
52
  }
43
53
 
44
- func updateUIView(_ uiView: MKMapView, context: Context) {
45
- // Update map type
46
- if uiView.mapType != mapType {
47
- uiView.mapType = mapType
48
- }
54
+ func updateUIView(_ map: MKMapView, context: Context) {
55
+ context.coordinator.parent = self
56
+ if map.mapType != baseMap { map.mapType = baseMap }
49
57
 
50
- // Update region -- guard against tiny differences to avoid feedback loops
51
- let currentCenter = uiView.region.center
52
- let threshold = 0.0001
53
- if abs(currentCenter.latitude - region.center.latitude) > threshold ||
54
- abs(currentCenter.longitude - region.center.longitude) > threshold {
55
- uiView.setRegion(region, animated: true)
58
+ let shown = map.region.center
59
+ let wanted = visibleArea.center
60
+ if abs(shown.latitude - wanted.latitude) > Self.centerTolerance
61
+ || abs(shown.longitude - wanted.longitude) > Self.centerTolerance {
62
+ map.setRegion(visibleArea, animated: true)
56
63
  }
57
64
 
58
- // Diff annotations
59
- let existing = Set(uiView.annotations.compactMap { $0 as? MKPointAnnotation })
60
- let incoming = Set(annotations)
61
- let toRemove = existing.subtracting(incoming)
62
- let toAdd = incoming.subtracting(existing)
63
- uiView.removeAnnotations(Array(toRemove))
64
- uiView.addAnnotations(Array(toAdd))
65
+ let onMap = Set(map.annotations.compactMap { item in item as? MKPointAnnotation })
66
+ let requested = Set(pins)
67
+ map.removeAnnotations(Array(onMap.subtracting(requested)))
68
+ map.addAnnotations(Array(requested.subtracting(onMap)))
65
69
  }
66
70
 
71
+ @MainActor
67
72
  final class Coordinator: NSObject, MKMapViewDelegate {
68
- var parent: MapViewRepresentable
69
-
70
- init(_ parent: MapViewRepresentable) { self.parent = parent }
73
+ init(parent: TrailMapView) { self.parent = parent }
74
+ var parent: TrailMapView
71
75
 
72
- func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
73
- parent.region = mapView.region
74
- parent.onRegionChanged?(mapView.region)
76
+ func mapView(_ map: MKMapView, regionDidChangeAnimated isAnimated: Bool) {
77
+ parent.visibleArea = map.region
78
+ parent.onAreaChanged?(map.region)
75
79
  }
76
80
 
77
- func mapView(
78
- _ mapView: MKMapView,
79
- viewFor annotation: MKAnnotation
80
- ) -> MKAnnotationView? {
81
- guard !(annotation is MKUserLocation) else { return nil }
82
- let id = "pin"
83
- let view = mapView.dequeueReusableAnnotationView(withIdentifier: id)
84
- ?? MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: id)
85
- view.annotation = annotation
86
- return view
81
+ func mapView(_ map: MKMapView, viewFor marker: MKAnnotation) -> MKAnnotationView? {
82
+ guard !(marker is MKUserLocation) else { return nil }
83
+ let reuseID = "trailhead"
84
+ guard let recycled = map.dequeueReusableAnnotationView(withIdentifier: reuseID) else {
85
+ return MKMarkerAnnotationView(annotation: marker, reuseIdentifier: reuseID)
86
+ }
87
+ recycled.annotation = marker
88
+ return recycled
87
89
  }
88
90
  }
89
91
  }
90
92
  ```
91
93
 
92
- ### Usage
94
+ Call site:
93
95
 
94
96
  ```swift
95
- struct MapScreen: View {
96
- @State private var region = MKCoordinateRegion(
97
- center: CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
98
- span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
97
+ struct TrailScreen: View {
98
+ @State private var area = MKCoordinateRegion(
99
+ center: CLLocationCoordinate2D(latitude: 46.55, longitude: 7.98),
100
+ span: MKCoordinateSpan(latitudeDelta: 0.08, longitudeDelta: 0.08)
99
101
  )
100
- @State private var mapType: MKMapType = .standard
102
+ @State private var style = MKMapType.hybrid
101
103
 
102
104
  var body: some View {
103
- MapViewRepresentable(
104
- region: $region,
105
- mapType: $mapType,
106
- annotations: []
107
- )
108
- .ignoresSafeArea()
105
+ TrailMapView(visibleArea: $area, baseMap: $style, pins: [])
106
+ .ignoresSafeArea()
109
107
  }
110
108
  }
111
109
  ```
112
110
 
113
- ### Gotchas
114
-
115
- - **Region update loops.** The delegate writes to `@Binding region`, which triggers `updateUIView`, which calls `setRegion`, which triggers the delegate again. The threshold guard is essential.
116
- - **Annotation diffing.** MKMapView does not handle duplicate annotations well. Always diff before adding/removing.
117
- - **Native SwiftUI Map.** For iOS 17+, prefer the native `Map` view unless you need delegate-level control (custom overlays, clustering, etc.).
111
+ Pitfalls:
118
112
 
119
- ---
113
+ - The region can loop: the delegate writes the binding, update calls
114
+ `setRegion`, which fires the delegate again. The tolerance check on the
115
+ center coordinate is what stops it; do not remove it.
116
+ - `MKMapView` copes poorly with the same annotation added twice. Diff with sets
117
+ before adding or removing.
118
+ - Prefer native: from iOS 17 the SwiftUI `Map` covers most needs. Keep this
119
+ wrapper for delegate-level control such as custom overlays or clustering.
120
120
 
121
121
  ## 2. UITextView Wrapper (Attributed Text)
122
122
 
123
- Wrap `UITextView` for rich text editing with `NSAttributedString` binding and placeholder support.
124
-
125
123
  ```swift
126
- import SwiftUI
124
+ struct RichNoteEditor: UIViewRepresentable {
125
+ @Binding var content: NSAttributedString
126
+ @Binding var isFocused: Bool
127
+ var hint: String
127
128
 
128
- struct RichTextEditor: UIViewRepresentable {
129
- @Binding var attributedText: NSAttributedString
130
- var placeholder: String = ""
131
- @Binding var isFirstResponder: Bool
129
+ fileprivate static let hintTag = 999
130
+ private static let verticalInset: CGFloat = 8
131
+ private static let horizontalInset: CGFloat = 4
132
+ private static let minimumHeight: CGFloat = 44
132
133
 
133
- func makeCoordinator() -> Coordinator { Coordinator(self) }
134
+ func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
134
135
 
135
136
  func makeUIView(context: Context) -> UITextView {
136
- let textView = UITextView()
137
- textView.delegate = context.coordinator
138
- textView.font = .preferredFont(forTextStyle: .body)
139
- textView.adjustsFontForContentSizeCategory = true
140
- textView.backgroundColor = .clear
141
- textView.textContainerInset = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4)
142
-
143
- // Placeholder label
144
- let label = UILabel()
145
- label.text = placeholder
146
- label.font = .preferredFont(forTextStyle: .body)
147
- label.textColor = .placeholderText
148
- label.tag = 999
149
- label.translatesAutoresizingMaskIntoConstraints = false
150
- textView.addSubview(label)
137
+ let editor = UITextView(frame: .zero)
138
+ editor.delegate = context.coordinator
139
+ editor.backgroundColor = .clear
140
+ editor.adjustsFontForContentSizeCategory = true
141
+ editor.textContainerInset = UIEdgeInsets(top: Self.verticalInset,
142
+ left: Self.horizontalInset,
143
+ bottom: Self.verticalInset,
144
+ right: Self.horizontalInset)
145
+
146
+ let hintLabel = UILabel()
147
+ hintLabel.tag = Self.hintTag
148
+ hintLabel.text = hint
149
+ hintLabel.font = .preferredFont(forTextStyle: .body)
150
+ hintLabel.textColor = .placeholderText
151
+ hintLabel.translatesAutoresizingMaskIntoConstraints = false
152
+ editor.addSubview(hintLabel)
151
153
  NSLayoutConstraint.activate([
152
- label.topAnchor.constraint(equalTo: textView.topAnchor, constant: 8),
153
- label.leadingAnchor.constraint(equalTo: textView.leadingAnchor, constant: 8),
154
+ hintLabel.leadingAnchor.constraint(equalTo: editor.leadingAnchor, constant: 8),
155
+ hintLabel.topAnchor.constraint(equalTo: editor.topAnchor, constant: 8)
154
156
  ])
155
-
156
- return textView
157
+ return editor
157
158
  }
158
159
 
159
- func updateUIView(_ uiView: UITextView, context: Context) {
160
- if uiView.attributedText != attributedText {
161
- uiView.attributedText = attributedText
162
- }
163
-
164
- // Update placeholder visibility
165
- if let label = uiView.viewWithTag(999) as? UILabel {
166
- label.isHidden = !uiView.text.isEmpty
160
+ func updateUIView(_ editor: UITextView, context: Context) {
161
+ context.coordinator.parent = self
162
+ if editor.attributedText != content {
163
+ editor.attributedText = content
167
164
  }
165
+ editor.viewWithTag(Self.hintTag)?.isHidden = editor.hasText
168
166
 
169
- // First responder management
170
- if isFirstResponder && !uiView.isFirstResponder {
171
- uiView.becomeFirstResponder()
172
- } else if !isFirstResponder && uiView.isFirstResponder {
173
- uiView.resignFirstResponder()
167
+ switch (isFocused, editor.isFirstResponder) {
168
+ case (true, false): editor.becomeFirstResponder()
169
+ case (false, true): editor.resignFirstResponder()
170
+ default: break
174
171
  }
175
172
  }
176
173
 
177
- @available(iOS 16.0, *)
178
- func sizeThatFits(
179
- _ proposal: ProposedViewSize,
180
- uiView: UITextView,
181
- context: Context
182
- ) -> CGSize? {
183
- let width = proposal.width ?? UIView.layoutFittingExpandedSize.width
184
- let size = uiView.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude))
185
- return CGSize(width: width, height: max(size.height, 44))
174
+ func sizeThatFits(_ proposal: ProposedViewSize,
175
+ uiView editor: UITextView,
176
+ context: Context) -> CGSize? {
177
+ let targetWidth = proposal.width ?? UIView.layoutFittingExpandedSize.width
178
+ let targetHeight = max(proposal.height ?? Self.minimumHeight, Self.minimumHeight)
179
+ return CGSize(width: targetWidth, height: targetHeight)
186
180
  }
187
181
 
182
+ @MainActor
188
183
  final class Coordinator: NSObject, UITextViewDelegate {
189
- var parent: RichTextEditor
184
+ init(parent: RichNoteEditor) { self.parent = parent }
185
+ var parent: RichNoteEditor
190
186
 
191
- init(_ parent: RichTextEditor) { self.parent = parent }
192
-
193
- func textViewDidChange(_ textView: UITextView) {
194
- parent.attributedText = textView.attributedText ?? NSAttributedString()
195
- if let label = textView.viewWithTag(999) as? UILabel {
196
- label.isHidden = !textView.text.isEmpty
197
- }
187
+ func textViewDidChange(_ editor: UITextView) {
188
+ parent.content = editor.attributedText ?? NSAttributedString()
189
+ editor.viewWithTag(RichNoteEditor.hintTag)?.isHidden = editor.hasText
198
190
  }
199
191
 
200
- func textViewDidBeginEditing(_ textView: UITextView) {
201
- parent.isFirstResponder = true
192
+ func textViewDidBeginEditing(_ editor: UITextView) {
193
+ parent.isFocused = true
202
194
  }
203
195
 
204
- func textViewDidEndEditing(_ textView: UITextView) {
205
- parent.isFirstResponder = false
196
+ func textViewDidEndEditing(_ editor: UITextView) {
197
+ parent.isFocused = false
206
198
  }
207
199
  }
208
200
  }
209
201
  ```
210
202
 
211
- ### Usage
203
+ Call site: `RichNoteEditor(content: $note, isFocused: $editing, hint: "Add a note").frame(minHeight: 100)`.
212
204
 
213
- ```swift
214
- struct NotesEditorView: View {
215
- @State private var text = NSAttributedString()
216
- @State private var isFocused = false
205
+ Pitfalls:
217
206
 
218
- var body: some View {
219
- RichTextEditor(
220
- attributedText: $text,
221
- placeholder: "Write something...",
222
- isFirstResponder: $isFocused
223
- )
224
- .frame(minHeight: 100)
225
- }
226
- }
227
- ```
228
-
229
- ### Gotchas
230
-
231
- - **`NSAttributedString` comparison.** The equality check in `updateUIView` is critical -- without it, every keystroke triggers a full re-render loop.
232
- - **First responder management.** Avoid calling `becomeFirstResponder()` unconditionally in `updateUIView` -- it steals focus from other fields.
233
- - **iOS 26 alternative.** `TextEditor` in iOS 26 supports `AttributedString` natively. Prefer it unless you need `NSAttributedString` or delegate-level control.
234
-
235
- ---
207
+ - Drop the `NSAttributedString` comparison and every keystroke starts a render
208
+ loop.
209
+ - Calling `becomeFirstResponder()` unconditionally in update pulls focus away
210
+ from whatever field the user is typing in; only change focus when the binding
211
+ and the actual state disagree.
212
+ - Prefer native: on iOS 26 `TextEditor` edits `AttributedString` directly. Wrap
213
+ only when you need `NSAttributedString` or delegate hooks.
236
214
 
237
215
  ## 3. AVCaptureVideoPreviewLayer Wrapper
238
216
 
239
- Display a live camera preview. The preview layer requires a `UIView` host.
217
+ The preview layer needs a view to live in. Making the layer the view's backing
218
+ layer is simpler than adding a sublayer.
240
219
 
241
220
  ```swift
242
- import SwiftUI
243
221
  import AVFoundation
244
222
 
245
- struct CameraPreview: UIViewRepresentable {
246
- let session: AVCaptureSession
247
-
248
- func makeUIView(context: Context) -> CameraPreviewUIView {
249
- let view = CameraPreviewUIView()
250
- view.previewLayer.session = session
251
- view.previewLayer.videoGravity = .resizeAspectFill
252
- return view
223
+ final class CapturePreviewUIView: UIView {
224
+ override func layoutSubviews() {
225
+ super.layoutSubviews()
226
+ captureLayer.frame = bounds
253
227
  }
254
228
 
255
- func updateUIView(_ uiView: CameraPreviewUIView, context: Context) {
256
- // Session is reference type -- no update needed unless swapping sessions
257
- if uiView.previewLayer.session !== session {
258
- uiView.previewLayer.session = session
259
- }
229
+ override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
230
+
231
+ var captureLayer: AVCaptureVideoPreviewLayer {
232
+ layer as! AVCaptureVideoPreviewLayer
260
233
  }
261
234
  }
262
235
 
263
- final class CameraPreviewUIView: UIView {
264
- override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
236
+ struct CapturePreview: UIViewRepresentable {
237
+ let captureSession: AVCaptureSession
265
238
 
266
- var previewLayer: AVCaptureVideoPreviewLayer {
267
- layer as! AVCaptureVideoPreviewLayer
239
+ func makeUIView(context: Context) -> CapturePreviewUIView {
240
+ let preview = CapturePreviewUIView(frame: .zero)
241
+ preview.captureLayer.videoGravity = .resizeAspectFill
242
+ preview.captureLayer.session = captureSession
243
+ return preview
268
244
  }
269
245
 
270
- override func layoutSubviews() {
271
- super.layoutSubviews()
272
- previewLayer.frame = bounds
246
+ func updateUIView(_ preview: CapturePreviewUIView, context: Context) {
247
+ guard preview.captureLayer.session !== captureSession else { return }
248
+ preview.captureLayer.session = captureSession
273
249
  }
274
250
  }
275
251
  ```
276
252
 
277
- ### Usage
253
+ Call site:
278
254
 
279
255
  ```swift
280
- struct CameraScreen: View {
281
- @State private var cameraManager = CameraManager()
256
+ struct ReceiptCameraScreen: View {
257
+ @State private var camera = ReceiptCamera()
282
258
 
283
259
  var body: some View {
284
- CameraPreview(session: cameraManager.session)
260
+ CapturePreview(captureSession: camera.session)
285
261
  .ignoresSafeArea()
286
- .task { await cameraManager.start() }
262
+ .task { await camera.start() }
287
263
  }
288
264
  }
289
265
  ```
290
266
 
291
- ### Gotchas
267
+ Notes:
292
268
 
293
- - **Use a custom UIView subclass with `layerClass`.** Overriding `layerClass` avoids adding a sublayer and ensures the preview layer resizes automatically with the view.
294
- - **Session management belongs outside the representable.** Create and manage `AVCaptureSession` in a separate model. The representable only displays it.
295
- - **Orientation.** Set `previewLayer.connection?.videoRotationAngle` if supporting device rotation.
296
-
297
- ---
269
+ - The `layerClass` override means no extra sublayer, and the layer resizes with
270
+ the view automatically. The forced cast is safe because `layerClass`
271
+ guarantees the type.
272
+ - Build and run the `AVCaptureSession` in a separate model object (here
273
+ `ReceiptCamera`). The representable only shows it.
274
+ - For rotation, set `captureLayer.connection?.videoRotationAngle`.
298
275
 
299
276
  ## 4. PHPickerViewController Wrapper
300
277
 
301
- Multi-select photo picker that loads selected images asynchronously.
302
-
303
278
  ```swift
304
- import SwiftUI
305
279
  import PhotosUI
306
280
 
307
- struct PhotoPicker: UIViewControllerRepresentable {
308
- @Binding var selectedImages: [UIImage]
309
- var selectionLimit: Int = 0 // 0 = unlimited
310
- @Environment(\.dismiss) private var dismiss
281
+ struct GalleryPicker: UIViewControllerRepresentable {
282
+ @Environment(\.dismiss) var close
283
+ @Binding var picked: [UIImage]
284
+ var maxSelection: Int
311
285
 
312
- func makeCoordinator() -> Coordinator { Coordinator(self) }
286
+ func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
313
287
 
314
288
  func makeUIViewController(context: Context) -> PHPickerViewController {
315
- var config = PHPickerConfiguration(photoLibrary: .shared())
316
- config.filter = .images
317
- config.selectionLimit = selectionLimit
318
- config.preferredAssetRepresentationMode = .current
319
-
320
- let picker = PHPickerViewController(configuration: config)
321
- picker.delegate = context.coordinator
322
- return picker
289
+ var options = PHPickerConfiguration(photoLibrary: .shared())
290
+ options.selectionLimit = maxSelection
291
+ options.preferredAssetRepresentationMode = .current
292
+ options.filter = .images
293
+ let sheet = PHPickerViewController(configuration: options)
294
+ sheet.delegate = context.coordinator
295
+ return sheet
323
296
  }
324
297
 
325
- func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
326
- // Nothing to update -- configuration is immutable after creation
298
+ func updateUIViewController(_ sheet: PHPickerViewController, context: Context) {
299
+ context.coordinator.parent = self
327
300
  }
328
301
 
302
+ @MainActor
329
303
  final class Coordinator: NSObject, PHPickerViewControllerDelegate {
330
- let parent: PhotoPicker
331
-
332
- init(_ parent: PhotoPicker) { self.parent = parent }
333
-
334
- func picker(
335
- _ picker: PHPickerViewController,
336
- didFinishPicking results: [PHPickerResult]
337
- ) {
338
- parent.dismiss()
339
-
340
- guard !results.isEmpty else { return }
304
+ init(parent: GalleryPicker) { self.parent = parent }
305
+ var parent: GalleryPicker
341
306
 
307
+ func picker(_ sheet: PHPickerViewController, didFinishPicking picks: [PHPickerResult]) {
308
+ parent.close()
309
+ if picks.isEmpty { return }
310
+ let providers = picks.map(\.itemProvider)
342
311
  Task { @MainActor in
343
312
  var images: [UIImage] = []
344
- for result in results {
345
- if let image = await loadImage(from: result.itemProvider) {
346
- images.append(image)
347
- }
313
+ for provider in providers {
314
+ if let image = await Self.loadImage(provider) { images.append(image) }
348
315
  }
349
- parent.selectedImages = images
316
+ self.parent.picked = images
350
317
  }
351
318
  }
352
319
 
353
- private func loadImage(from provider: NSItemProvider) async -> UIImage? {
354
- await withCheckedContinuation { continuation in
355
- if provider.canLoadObject(ofClass: UIImage.self) {
356
- provider.loadObject(ofClass: UIImage.self) { image, _ in
357
- continuation.resume(returning: image as? UIImage)
358
- }
359
- } else {
360
- continuation.resume(returning: nil)
320
+ private static func loadImage(_ provider: NSItemProvider) async -> UIImage? {
321
+ guard provider.canLoadObject(ofClass: UIImage.self) else { return nil }
322
+ return await withCheckedContinuation { continuation in
323
+ provider.loadObject(ofClass: UIImage.self) { object, _ in
324
+ continuation.resume(returning: object as? UIImage)
361
325
  }
362
326
  }
363
327
  }
@@ -365,584 +329,406 @@ struct PhotoPicker: UIViewControllerRepresentable {
365
329
  }
366
330
  ```
367
331
 
368
- ### Usage
332
+ A `selectionLimit` of 0 means no limit. There is nothing to push in the update
333
+ method beyond the parent refresh, because a picker's configuration is fixed
334
+ once the controller exists.
369
335
 
370
- ```swift
371
- struct ImagePickerDemo: View {
372
- @State private var images: [UIImage] = []
373
- @State private var showPicker = false
336
+ Call site:
374
337
 
375
- var body: some View {
376
- VStack {
377
- ScrollView(.horizontal) {
378
- HStack {
379
- ForEach(images.indices, id: \.self) { i in
380
- Image(uiImage: images[i])
381
- .resizable()
382
- .scaledToFill()
383
- .frame(width: 100, height: 100)
384
- .clipShape(.rect(cornerRadius: 8))
385
- }
386
- }
387
- }
388
- Button("Pick Photos") { showPicker = true }
389
- }
390
- .sheet(isPresented: $showPicker) {
391
- PhotoPicker(selectedImages: $images, selectionLimit: 5)
392
- }
393
- }
338
+ ```swift
339
+ .sheet(isPresented: $showingPicker) {
340
+ GalleryPicker(picked: $attachments, maxSelection: 5)
394
341
  }
395
- ```
396
342
 
397
- ### Gotchas
343
+ // thumbnail grid cell
344
+ Image(uiImage: image)
345
+ .resizable()
346
+ .scaledToFill()
347
+ .frame(width: 96, height: 96)
348
+ .clipShape(.rect(cornerRadius: 10))
349
+ ```
398
350
 
399
- - **Always dismiss in the delegate.** `picker(_:didFinishPicking:)` is called for both selection and cancellation (with empty results). Dismiss in both cases.
400
- - **Async image loading.** `NSItemProvider.loadObject` is completion-based. Wrap in `withCheckedContinuation` for async/await usage. Load images after dismissal to avoid blocking the picker UI.
401
- - **iOS 17 alternative.** `PhotosUI.PhotosPicker` is a native SwiftUI view. Prefer it unless you need custom picker UI or advanced filtering.
351
+ Pitfalls:
402
352
 
403
- ---
353
+ - `didFinishPicking` fires for a selection and for a cancel; a cancel delivers
354
+ an empty array. Dismiss in both cases.
355
+ - Load images after dismissing so the picker does not stall.
356
+ - Prefer native: `PhotosPicker` from PhotosUI (iOS 17 guidance) covers ordinary
357
+ picking. Reach for `PHPickerViewController` when you need your own picker
358
+ presentation or filters the native control lacks.
404
359
 
405
360
  ## 5. MFMailComposeViewController Wrapper
406
361
 
407
- Present the system email composer with pre-filled fields and handle the result.
408
-
409
362
  ```swift
410
- import SwiftUI
411
363
  import MessageUI
412
364
 
413
- struct MailComposer: UIViewControllerRepresentable {
414
- let subject: String
415
- let recipients: [String]
416
- let body: String
417
- var isHTML: Bool = false
418
- var onResult: ((MFMailComposeResult) -> Void)?
419
- @Environment(\.dismiss) private var dismiss
365
+ struct SupportMailView: UIViewControllerRepresentable {
366
+ @Environment(\.dismiss) var close
367
+ var subject: String
368
+ var recipients: [String]
369
+ var message: String
370
+ var completion: (MFMailComposeResult) -> Void
420
371
 
421
- func makeCoordinator() -> Coordinator { Coordinator(self) }
372
+ func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
422
373
 
423
374
  func makeUIViewController(context: Context) -> MFMailComposeViewController {
424
- let controller = MFMailComposeViewController()
425
- controller.mailComposeDelegate = context.coordinator
426
- controller.setSubject(subject)
427
- controller.setToRecipients(recipients)
428
- controller.setMessageBody(body, isHTML: isHTML)
429
- return controller
375
+ let composer = MFMailComposeViewController()
376
+ composer.setToRecipients(recipients)
377
+ composer.setSubject(subject)
378
+ composer.setMessageBody(message, isHTML: false)
379
+ composer.mailComposeDelegate = context.coordinator
380
+ return composer
430
381
  }
431
382
 
432
- func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: Context) {
433
- // Cannot update mail compose after presentation
383
+ func updateUIViewController(_ composer: MFMailComposeViewController, context: Context) {
384
+ context.coordinator.parent = self
434
385
  }
435
386
 
436
- final class Coordinator: NSObject, MFMailComposeViewControllerDelegate {
437
- let parent: MailComposer
438
-
439
- init(_ parent: MailComposer) { self.parent = parent }
387
+ @MainActor
388
+ final class Coordinator: NSObject, @preconcurrency MFMailComposeViewControllerDelegate {
389
+ init(parent: SupportMailView) { self.parent = parent }
390
+ var parent: SupportMailView
440
391
 
441
- func mailComposeController(
442
- _ controller: MFMailComposeViewController,
443
- didFinishWith result: MFMailComposeResult,
444
- error: Error?
445
- ) {
446
- parent.onResult?(result)
447
- parent.dismiss()
392
+ func mailComposeController(_ composer: MFMailComposeViewController,
393
+ didFinishWith outcome: MFMailComposeResult,
394
+ error failure: Error?) {
395
+ parent.completion(outcome)
396
+ parent.close()
448
397
  }
449
398
  }
450
399
  }
451
400
  ```
452
401
 
453
- ### Usage
402
+ Present it only after `MFMailComposeViewController.canSendMail()` returns true:
454
403
 
455
404
  ```swift
456
- struct FeedbackView: View {
457
- @State private var showMail = false
458
-
459
- var body: some View {
460
- Button("Send Feedback") {
461
- guard MFMailComposeViewController.canSendMail() else { return }
462
- showMail = true
463
- }
464
- .sheet(isPresented: $showMail) {
465
- MailComposer(
466
- subject: "App Feedback",
467
- recipients: ["support@example.com"],
468
- body: "I have feedback about..."
469
- ) { result in
470
- print("Mail result: \(result.rawValue)")
471
- }
472
- }
473
- }
405
+ Button("Contact support") {
406
+ showingMail = MFMailComposeViewController.canSendMail()
407
+ }
408
+ .sheet(isPresented: $showingMail) {
409
+ SupportMailView(subject: "Order help", recipients: ["help@example.com"],
410
+ message: "", completion: { _ in })
474
411
  }
475
412
  ```
476
413
 
477
- ### Gotchas
414
+ Notes:
478
415
 
479
- - **Check `canSendMail()` before presenting.** The app crashes if `MFMailComposeViewController` is presented on a device with no mail account configured.
480
- - **Cannot update after presentation.** `updateUIViewController` is intentionally empty -- the mail compose API does not support changing fields after the controller is shown.
481
- - **The delegate protocol name is `MFMailComposeViewControllerDelegate`**, not `MFMailComposeDelegate`.
482
-
483
- ---
416
+ - Presenting the composer on a device without a configured mail account
417
+ crashes. Always check `canSendMail()` first.
418
+ - Subject, recipients and body are fixed once the composer is on screen, so
419
+ the update method does nothing beyond refreshing `parent`.
420
+ - The delegate protocol is `MFMailComposeViewControllerDelegate`. A type named
421
+ `MFMailComposeDelegate` does not exist, and code that uses it will not
422
+ compile.
484
423
 
485
424
  ## 6. UIActivityViewController Wrapper (Share Sheet)
486
425
 
487
- Present the system share sheet. This is a `UIViewControllerRepresentable` because `UIActivityViewController` is a controller, not a view.
426
+ The share sheet is a view controller, so it needs
427
+ `UIViewControllerRepresentable`.
488
428
 
489
429
  ```swift
490
- import SwiftUI
491
-
492
- struct ShareSheet: UIViewControllerRepresentable {
493
- let items: [Any]
494
- var activities: [UIActivity]? = nil
495
- var excludedTypes: [UIActivity.ActivityType]? = nil
430
+ struct ActivitySheet: UIViewControllerRepresentable {
431
+ var items: [Any]
432
+ var extraActivities: [UIActivity]? = nil
433
+ var hiddenTypes: [UIActivity.ActivityType] = []
496
434
 
497
435
  func makeUIViewController(context: Context) -> UIActivityViewController {
498
- let controller = UIActivityViewController(
499
- activityItems: items,
500
- applicationActivities: activities
501
- )
502
- controller.excludedActivityTypes = excludedTypes
503
- return controller
436
+ let sheet = UIActivityViewController(activityItems: items,
437
+ applicationActivities: extraActivities)
438
+ sheet.excludedActivityTypes = hiddenTypes
439
+ return sheet
504
440
  }
505
441
 
506
- func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
507
- // Cannot update after presentation
508
- }
442
+ func updateUIViewController(_ sheet: UIActivityViewController, context: Context) {}
509
443
  }
510
444
  ```
511
445
 
512
- ### Usage
446
+ Call site:
513
447
 
514
448
  ```swift
515
- struct ContentView: View {
516
- @State private var showShare = false
517
-
518
- var body: some View {
519
- Button("Share") { showShare = true }
520
- .sheet(isPresented: $showShare) {
521
- ShareSheet(items: ["Check out this app!", URL(string: "https://example.com")!])
522
- .presentationDetents([.medium])
523
- }
524
- }
449
+ .sheet(isPresented: $sharing) {
450
+ ActivitySheet(items: [exportURL], hiddenTypes: [.assignToContact])
451
+ .presentationDetents([.medium])
525
452
  }
526
453
  ```
527
454
 
528
- ### Gotchas
455
+ Notes:
529
456
 
530
- - **Present via `.sheet`.** Do not try to use `UIActivityViewController` as an inline view -- it is a modal controller.
531
- - **iPad requires `popoverPresentationController`.** When using on iPad outside of `.sheet`, set the source view/rect on the popover controller. SwiftUI's `.sheet` handles this automatically.
532
- - **iOS 16+ alternative.** `ShareLink` is a native SwiftUI view for Transferable items. Prefer it for simple sharing.
533
-
534
- ---
457
+ - Present it with `.sheet`. It does not work as an inline view.
458
+ - On iPad, presenting outside a `.sheet` requires setting the
459
+ `popoverPresentationController` source view and rect. `.sheet` takes care of
460
+ that for you.
461
+ - Prefer native: from iOS 16 `ShareLink` shares `Transferable` items with no
462
+ wrapper for the simple cases.
535
463
 
536
464
  ## 7. UISearchBar Wrapper
537
465
 
538
- Wrap `UISearchBar` with delegate-based callbacks, debounce support, and cancel button handling.
539
-
540
466
  ```swift
541
- import SwiftUI
542
- import Combine
543
-
544
- struct SearchBar: UIViewRepresentable {
545
- @Binding var text: String
546
- var placeholder: String = "Search"
547
- var onSearch: ((String) -> Void)?
548
- var onCancel: (() -> Void)?
467
+ struct DirectorySearchBar: UIViewRepresentable {
468
+ @Binding var query: String
469
+ var prompt: String
470
+ var onSearch: (String) -> Void
471
+ var onClear: () -> Void
549
472
 
550
- func makeCoordinator() -> Coordinator { Coordinator(self) }
473
+ func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
551
474
 
552
475
  func makeUIView(context: Context) -> UISearchBar {
553
- let searchBar = UISearchBar()
554
- searchBar.delegate = context.coordinator
555
- searchBar.placeholder = placeholder
556
- searchBar.searchBarStyle = .minimal
557
- searchBar.autocapitalizationType = .none
558
- return searchBar
476
+ let field = UISearchBar(frame: .zero)
477
+ field.placeholder = prompt
478
+ field.searchBarStyle = .minimal
479
+ field.autocapitalizationType = .none
480
+ field.delegate = context.coordinator
481
+ return field
559
482
  }
560
483
 
561
- func updateUIView(_ uiView: UISearchBar, context: Context) {
562
- if uiView.text != text {
563
- uiView.text = text
564
- }
484
+ func updateUIView(_ field: UISearchBar, context: Context) {
485
+ context.coordinator.parent = self
486
+ if field.text != query { field.text = query }
565
487
  }
566
488
 
489
+ @MainActor
567
490
  final class Coordinator: NSObject, UISearchBarDelegate {
568
- var parent: SearchBar
569
- private var debounceTask: Task<Void, Never>?
570
-
571
- init(_ parent: SearchBar) { self.parent = parent }
572
-
573
- func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
574
- parent.text = searchText
575
- searchBar.showsCancelButton = !searchText.isEmpty
576
-
577
- // Debounce search
578
- debounceTask?.cancel()
579
- debounceTask = Task { @MainActor in
580
- try? await Task.sleep(for: .milliseconds(300))
491
+ init(parent: DirectorySearchBar) { self.parent = parent }
492
+ var parent: DirectorySearchBar
493
+
494
+ private var pending: Task<Void, Never>?
495
+ private static let quietPeriod: Duration = .milliseconds(300)
496
+
497
+ func searchBar(_ field: UISearchBar, textDidChange typed: String) {
498
+ parent.query = typed
499
+ field.setShowsCancelButton(!typed.isEmpty, animated: true)
500
+ pending?.cancel()
501
+ pending = Task { @MainActor [weak self] in
502
+ do {
503
+ try await Task.sleep(for: Coordinator.quietPeriod)
504
+ } catch {
505
+ return
506
+ }
581
507
  guard !Task.isCancelled else { return }
582
- parent.onSearch?(searchText)
508
+ self?.parent.onSearch(typed)
583
509
  }
584
510
  }
585
511
 
586
- func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
587
- debounceTask?.cancel()
588
- parent.onSearch?(parent.text)
589
- searchBar.resignFirstResponder()
512
+ func searchBarSearchButtonClicked(_ field: UISearchBar) {
513
+ pending?.cancel()
514
+ parent.onSearch(field.text ?? "")
515
+ field.resignFirstResponder()
590
516
  }
591
517
 
592
- func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
593
- parent.text = ""
594
- parent.onCancel?()
595
- searchBar.resignFirstResponder()
596
- searchBar.showsCancelButton = false
597
- }
598
- }
599
- }
600
- ```
601
-
602
- ### Usage
603
-
604
- ```swift
605
- struct SearchableList: View {
606
- @State private var query = ""
607
- @State private var results: [String] = []
608
-
609
- var body: some View {
610
- VStack(spacing: 0) {
611
- SearchBar(text: $query, placeholder: "Search items") { text in
612
- results = performSearch(text)
613
- }
614
- List(results, id: \.self) { Text($0) }
518
+ func searchBarCancelButtonClicked(_ field: UISearchBar) {
519
+ parent.query = ""
520
+ parent.onClear()
521
+ field.resignFirstResponder()
522
+ field.setShowsCancelButton(false, animated: true)
615
523
  }
616
524
  }
617
525
  }
618
526
  ```
619
527
 
620
- ### Gotchas
621
-
622
- - **Native `.searchable` modifier.** Prefer SwiftUI's `.searchable(text:)` modifier for standard search patterns. Use this wrapper only when you need precise control over search bar appearance or delegate timing.
623
- - **Debounce with `Task.sleep`.** Cancel the previous task before starting a new one to debounce. `Combine` is not needed.
624
- - **Cancel button state.** Toggle `showsCancelButton` in the delegate, not in `updateUIView`, to avoid layout jumps.
528
+ Notes:
625
529
 
626
- ---
530
+ - The debounce is a stored `Task` that is cancelled on each keystroke and
531
+ sleeps 300 ms before searching. No Combine is needed; `Task.sleep` plus a
532
+ cancellation check does the job.
533
+ - The search button cancels any pending debounce, searches at once and drops
534
+ the keyboard. The cancel button clears the query, reports it, drops the
535
+ keyboard and hides itself.
536
+ - Show and hide the cancel button from the delegate, not from `updateUIView`,
537
+ or the bar jumps during layout.
538
+ - Prefer native: `.searchable(text:)` for standard search. Wrap `UISearchBar`
539
+ only when you need exact appearance control or delegate timing.
627
540
 
628
541
  ## 8. PDFView Wrapper (PDFKit)
629
542
 
630
- Display PDF documents in SwiftUI using `PDFView` from PDFKit. Supports loading from URL, Data, or file path, with configurable display mode and auto-scaling.
543
+ `PDFView` is a `UIView`, so this is a `UIViewRepresentable`.
631
544
 
632
545
  ```swift
633
- import SwiftUI
634
546
  import PDFKit
635
547
 
636
- struct PDFViewer: UIViewRepresentable {
637
- let document: PDFDocument?
638
- var displayMode: PDFDisplayMode = .singlePageContinuous
639
- var autoScales: Bool = true
640
- var displayDirection: PDFDisplayDirection = .vertical
641
- var pageShadowsEnabled: Bool = true
548
+ struct ManualPDFView: UIViewRepresentable {
549
+ let file: PDFDocument?
550
+ @Binding var pageIndex: Int
551
+ var layout: PDFDisplayMode = .singlePageContinuous
552
+ var direction: PDFDisplayDirection = .vertical
553
+ var fitsWidth = true
554
+ var showsPageShadows = true
642
555
 
643
- func makeUIView(context: Context) -> PDFView {
644
- let pdfView = PDFView()
645
- pdfView.displayMode = displayMode
646
- pdfView.displayDirection = displayDirection
647
- pdfView.autoScales = autoScales
648
- pdfView.pageShadowsEnabled = pageShadowsEnabled
649
- pdfView.document = document
650
- return pdfView
651
- }
556
+ func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
652
557
 
653
- func updateUIView(_ uiView: PDFView, context: Context) {
654
- // Update document if it changed (reference comparison)
655
- if uiView.document !== document {
656
- uiView.document = document
657
- }
658
-
659
- if uiView.displayMode != displayMode {
660
- uiView.displayMode = displayMode
661
- }
558
+ func makeUIView(context: Context) -> PDFView {
559
+ let viewer = PDFView(frame: .zero)
560
+ viewer.document = file
561
+ viewer.displayMode = layout
562
+ viewer.displayDirection = direction
563
+ viewer.autoScales = fitsWidth
564
+ viewer.pageShadowsEnabled = showsPageShadows
662
565
 
663
- if uiView.autoScales != autoScales {
664
- uiView.autoScales = autoScales
665
- }
566
+ let center = NotificationCenter.default
567
+ center.addObserver(context.coordinator,
568
+ selector: #selector(Coordinator.pageDidChange(_:)),
569
+ name: .PDFViewPageChanged,
570
+ object: viewer)
571
+ return viewer
666
572
  }
667
- }
668
- ```
669
573
 
670
- ### Convenience Initializers
574
+ func updateUIView(_ viewer: PDFView, context: Context) {
575
+ context.coordinator.parent = self
576
+ if viewer.document !== file { viewer.document = file }
577
+ if viewer.displayMode != layout { viewer.displayMode = layout }
578
+ if viewer.autoScales != fitsWidth { viewer.autoScales = fitsWidth }
671
579
 
672
- ```swift
673
- extension PDFViewer {
674
- /// Load a PDF from a URL (local file or remote).
675
- init(url: URL, displayMode: PDFDisplayMode = .singlePageContinuous) {
676
- self.document = PDFDocument(url: url)
677
- self.displayMode = displayMode
580
+ guard let pdf = viewer.document,
581
+ let target = pdf.page(at: pageIndex),
582
+ viewer.currentPage != target else { return }
583
+ viewer.go(to: target)
678
584
  }
679
585
 
680
- /// Load a PDF from raw data.
681
- init(data: Data, displayMode: PDFDisplayMode = .singlePageContinuous) {
682
- self.document = PDFDocument(data: data)
683
- self.displayMode = displayMode
586
+ static func dismantleUIView(_ viewer: PDFView, coordinator: Coordinator) {
587
+ NotificationCenter.default.removeObserver(coordinator,
588
+ name: .PDFViewPageChanged,
589
+ object: viewer)
684
590
  }
685
- }
686
- ```
687
591
 
688
- ### Usage
689
-
690
- ```swift
691
- struct DocumentView: View {
692
- let pdfURL: URL
592
+ @MainActor
593
+ final class Coordinator: NSObject {
594
+ init(parent: ManualPDFView) { self.parent = parent }
595
+ var parent: ManualPDFView
693
596
 
694
- var body: some View {
695
- PDFViewer(url: pdfURL)
696
- .ignoresSafeArea(edges: .bottom)
697
- .navigationTitle("Document")
698
- .navigationBarTitleDisplayMode(.inline)
597
+ @objc func pageDidChange(_ note: Notification) {
598
+ guard let viewer = note.object as? PDFView,
599
+ let pdf = viewer.document,
600
+ let page = viewer.currentPage else { return }
601
+ let position = pdf.index(for: page)
602
+ if position != parent.pageIndex { parent.pageIndex = position }
603
+ }
699
604
  }
700
605
  }
701
606
  ```
702
607
 
703
- ### With Async Loading
608
+ `PDFDocument(url:)` and `PDFDocument(data:)` create documents from a file URL
609
+ or from bytes. For remote files, load asynchronously:
704
610
 
705
611
  ```swift
706
- struct RemotePDFView: View {
707
- let url: URL
708
- @State private var document: PDFDocument?
709
- @State private var isLoading = true
710
- @State private var errorMessage: String?
612
+ struct RemoteManualScreen: View {
613
+ let source: URL
614
+ @State private var loaded: PDFDocument?
615
+ @State private var loadFailed = false
616
+ @State private var page = 0
711
617
 
712
618
  var body: some View {
713
- Group {
714
- if let document {
715
- PDFViewer(document: document)
716
- } else if isLoading {
717
- ProgressView("Loading PDF...")
718
- } else if let errorMessage {
719
- ContentUnavailableView(
720
- "Could Not Load PDF",
721
- systemImage: "doc.text.fill",
722
- description: Text(errorMessage)
723
- )
619
+ ZStack {
620
+ if let loaded {
621
+ ManualPDFView(file: loaded, pageIndex: $page)
622
+ } else if loadFailed {
623
+ ContentUnavailableView("Manual unavailable",
624
+ systemImage: "doc.questionmark")
625
+ } else {
626
+ ProgressView()
724
627
  }
725
628
  }
726
629
  .task {
727
630
  do {
728
- let (data, _) = try await URLSession.shared.data(from: url)
729
- document = PDFDocument(data: data)
631
+ let (bytes, _) = try await URLSession.shared.data(from: source)
632
+ loaded = PDFDocument(data: bytes)
730
633
  } catch {
731
- errorMessage = error.localizedDescription
732
- }
733
- isLoading = false
734
- }
735
- }
736
- }
737
- ```
738
-
739
- ### PDFView with Page Navigation
740
-
741
- ```swift
742
- struct NavigablePDFView: UIViewRepresentable {
743
- let document: PDFDocument?
744
- @Binding var currentPageIndex: Int
745
-
746
- func makeCoordinator() -> Coordinator { Coordinator(self) }
747
-
748
- func makeUIView(context: Context) -> PDFView {
749
- let pdfView = PDFView()
750
- pdfView.displayMode = .singlePageContinuous
751
- pdfView.autoScales = true
752
- pdfView.document = document
753
-
754
- NotificationCenter.default.addObserver(
755
- context.coordinator,
756
- selector: #selector(Coordinator.pageChanged(_:)),
757
- name: .PDFViewPageChanged,
758
- object: pdfView
759
- )
760
-
761
- return pdfView
762
- }
763
-
764
- func updateUIView(_ uiView: PDFView, context: Context) {
765
- if uiView.document !== document {
766
- uiView.document = document
767
- }
768
-
769
- // Navigate to page if binding changed externally
770
- if let doc = uiView.document,
771
- let page = doc.page(at: currentPageIndex),
772
- uiView.currentPage != page {
773
- uiView.go(to: page)
774
- }
775
- }
776
-
777
- static func dismantleUIView(_ uiView: PDFView, coordinator: Coordinator) {
778
- NotificationCenter.default.removeObserver(coordinator)
779
- }
780
-
781
- final class Coordinator: NSObject {
782
- var parent: NavigablePDFView
783
-
784
- init(_ parent: NavigablePDFView) { self.parent = parent }
785
-
786
- @objc func pageChanged(_ notification: Notification) {
787
- guard let pdfView = notification.object as? PDFView,
788
- let currentPage = pdfView.currentPage,
789
- let document = pdfView.document else { return }
790
- let index = document.index(for: currentPage)
791
- if parent.currentPageIndex != index {
792
- parent.currentPageIndex = index
634
+ loadFailed = true
793
635
  }
794
636
  }
795
637
  }
796
638
  }
797
639
  ```
798
640
 
799
- ### Gotchas
800
-
801
- - **`PDFView` inherits from `UIView`.** Use `UIViewRepresentable`, not `UIViewControllerRepresentable`.
802
- - **Document is a reference type.** Use `!==` for identity comparison in `updateUIView` to avoid unnecessary reloads.
803
- - **Page change notifications.** Use `NotificationCenter` with `.PDFViewPageChanged` -- `PDFView` does not use a delegate pattern for page changes.
804
- - **Remove observers in `dismantleUIView`.** Failing to remove `NotificationCenter` observers causes crashes after the view is removed.
805
- - **`autoScales`** fits the PDF to the view width. Disable it if you want the user to start at a specific zoom level.
806
- - **Thread safety.** `PDFDocument` loading can be expensive. Load asynchronously and assign on the main thread.
641
+ `ContentUnavailableView` needs iOS 17; on an iOS 16 target show a `Label` or
642
+ `Text` there instead.
807
643
 
808
- > **Docs:** [PDFView](https://sosumi.ai/documentation/pdfkit/pdfview) | [PDFKit](https://sosumi.ai/documentation/pdfkit)
644
+ Notes:
809
645
 
810
- ---
646
+ - `PDFView` has no delegate callback for page changes. Observe
647
+ `.PDFViewPageChanged` instead, and remove the observer in `dismantleUIView`;
648
+ a leftover observer can crash after the view is gone.
649
+ - `autoScales` fits the page width. Turn it off when the document should open
650
+ at a zoom factor you choose.
651
+ - Building a `PDFDocument` can be expensive. Load it off the main path and
652
+ assign it on the main actor.
653
+ - Apple: [PDFView](https://developer.apple.com/documentation/pdfkit/pdfview),
654
+ [PDFKit](https://developer.apple.com/documentation/pdfkit)
811
655
 
812
656
  ## 9. MFMessageComposeViewController Wrapper
813
657
 
814
- Present the system SMS/MMS composer with pre-filled recipients, body, and optional attachments. Companion to Recipe 6 (MFMailComposeViewController).
658
+ This is the text-message counterpart to the mail wrapper in
659
+ [recipe 5](#5-mfmailcomposeviewcontroller-wrapper), and it follows the same
660
+ structure.
815
661
 
816
662
  ```swift
817
- import SwiftUI
818
663
  import MessageUI
664
+ import UniformTypeIdentifiers
819
665
 
820
- struct MessageComposer: UIViewControllerRepresentable {
821
- let recipients: [String]
822
- let body: String
823
- var attachments: [MessageAttachment] = []
824
- var onResult: ((MessageComposeResult) -> Void)?
825
- @Environment(\.dismiss) private var dismiss
666
+ struct OutgoingAttachment {
667
+ let payload: Data
668
+ let uti: String
669
+ let name: String
670
+ }
826
671
 
827
- func makeCoordinator() -> Coordinator { Coordinator(self) }
672
+ struct InviteMessageView: UIViewControllerRepresentable {
673
+ @Environment(\.dismiss) var close
674
+ var recipients: [String]
675
+ var text: String
676
+ var attachments: [OutgoingAttachment] = []
677
+ var completion: (MessageComposeResult) -> Void
828
678
 
829
- func makeUIViewController(context: Context) -> MFMessageComposeViewController {
830
- let controller = MFMessageComposeViewController()
831
- controller.messageComposeDelegate = context.coordinator
832
- controller.recipients = recipients
833
- controller.body = body
679
+ func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
834
680
 
681
+ func makeUIViewController(context: Context) -> MFMessageComposeViewController {
682
+ let composer = MFMessageComposeViewController()
683
+ composer.recipients = recipients
684
+ composer.body = text
685
+ composer.messageComposeDelegate = context.coordinator
686
+ guard MFMessageComposeViewController.canSendAttachments() else { return composer }
835
687
  for attachment in attachments {
836
- controller.addAttachmentData(
837
- attachment.data,
838
- typeIdentifier: attachment.typeIdentifier,
839
- filename: attachment.filename
840
- )
688
+ composer.addAttachmentData(attachment.payload,
689
+ typeIdentifier: attachment.uti,
690
+ filename: attachment.name)
841
691
  }
842
-
843
- return controller
692
+ return composer
844
693
  }
845
694
 
846
- func updateUIViewController(
847
- _ uiViewController: MFMessageComposeViewController,
848
- context: Context
849
- ) {
850
- // Cannot update message compose after presentation
695
+ func updateUIViewController(_ composer: MFMessageComposeViewController, context: Context) {
696
+ context.coordinator.parent = self
851
697
  }
852
698
 
853
- final class Coordinator: NSObject, MFMessageComposeViewControllerDelegate {
854
- let parent: MessageComposer
855
-
856
- init(_ parent: MessageComposer) { self.parent = parent }
857
-
858
- func messageComposeViewController(
859
- _ controller: MFMessageComposeViewController,
860
- didFinishWith result: MessageComposeResult
861
- ) {
862
- parent.onResult?(result)
863
- parent.dismiss()
864
- }
865
- }
866
- }
699
+ @MainActor
700
+ final class Coordinator: NSObject, @preconcurrency MFMessageComposeViewControllerDelegate {
701
+ init(parent: InviteMessageView) { self.parent = parent }
702
+ var parent: InviteMessageView
867
703
 
868
- struct MessageAttachment {
869
- let data: Data
870
- let typeIdentifier: String // UTI, e.g., "public.jpeg"
871
- let filename: String
872
- }
873
- ```
874
-
875
- ### Usage
876
-
877
- ```swift
878
- struct InviteView: View {
879
- @State private var showMessage = false
880
-
881
- var body: some View {
882
- Button("Send Invite via SMS") {
883
- guard MFMessageComposeViewController.canSendText() else { return }
884
- showMessage = true
885
- }
886
- .sheet(isPresented: $showMessage) {
887
- MessageComposer(
888
- recipients: ["+1234567890"],
889
- body: "Join me on this app!"
890
- ) { result in
891
- switch result {
892
- case .sent:
893
- print("Message sent")
894
- case .cancelled:
895
- print("User cancelled")
896
- case .failed:
897
- print("Message failed")
898
- @unknown default:
899
- break
900
- }
901
- }
902
- }
903
- }
904
- }
905
- ```
906
-
907
- ### With Image Attachment
908
-
909
- ```swift
910
- struct SharePhotoView: View {
911
- @State private var showMessage = false
912
- let image: UIImage
913
-
914
- var body: some View {
915
- Button("Send Photo") {
916
- guard MFMessageComposeViewController.canSendText(),
917
- MFMessageComposeViewController.canSendAttachments() else {
918
- return
704
+ func messageComposeViewController(_ composer: MFMessageComposeViewController,
705
+ didFinishWith outcome: MessageComposeResult) {
706
+ switch outcome {
707
+ case .sent, .cancelled, .failed:
708
+ parent.completion(outcome)
709
+ @unknown default:
710
+ parent.completion(outcome)
919
711
  }
920
- showMessage = true
921
- }
922
- .sheet(isPresented: $showMessage) {
923
- MessageComposer(
924
- recipients: [],
925
- body: "Check out this photo!",
926
- attachments: [
927
- MessageAttachment(
928
- data: image.jpegData(compressionQuality: 0.8) ?? Data(),
929
- typeIdentifier: "public.jpeg",
930
- filename: "photo.jpg"
931
- )
932
- ]
933
- )
712
+ parent.close()
934
713
  }
935
714
  }
936
715
  }
937
716
  ```
938
717
 
939
- ### Gotchas
940
-
941
- - **Check `canSendText()` before presenting.** The app crashes if `MFMessageComposeViewController` is presented on a device that cannot send texts (e.g., iPod touch without iMessage).
942
- - **Check `canSendAttachments()` before adding attachments.** Not all devices or carriers support MMS attachments.
943
- - **The delegate protocol is `MFMessageComposeViewControllerDelegate`**, not `MFMessageComposeDelegate`. It has a single required method.
944
- - **Cannot update after presentation.** Like `MFMailComposeViewController`, the message composer API does not support changing fields after the controller is shown.
945
- - **iMessage vs. SMS.** The controller automatically uses iMessage when available. You cannot force one protocol over the other.
946
- - **Simulator limitation.** `canSendText()` returns `false` on the simulator. Test on a physical device.
947
-
948
- > **Docs:** [MFMessageComposeViewController](https://sosumi.ai/documentation/messageui/mfmessagecomposeviewcontroller) | [MFMessageComposeViewControllerDelegate](https://sosumi.ai/documentation/messageui/mfmessagecomposeviewcontrollerdelegate)
718
+ Attaching a photo: `photo.jpegData(compressionQuality: 0.8)` supplies the
719
+ bytes, `UTType.jpeg.identifier` (the string `public.jpeg`) the type, and a name
720
+ such as `invite.jpg` the filename.
721
+
722
+ Notes:
723
+
724
+ - Check `MFMessageComposeViewController.canSendText()` before presenting; if it
725
+ is false, presenting crashes. Check `canSendAttachments()` before adding
726
+ attachments, because MMS support varies by device and carrier.
727
+ - `MFMessageComposeViewControllerDelegate` has one required method,
728
+ `messageComposeViewController(_:didFinishWith:)`.
729
+ - As with mail in recipe 5, the fields cannot be edited after presentation.
730
+ - The system chooses iMessage when it is available. The app cannot force SMS or
731
+ iMessage.
732
+ - `canSendText()` returns false in the Simulator, so test on a device.
733
+ - Apple: [MFMessageComposeViewController](https://developer.apple.com/documentation/messageui/mfmessagecomposeviewcontroller),
734
+ [MFMessageComposeViewControllerDelegate](https://developer.apple.com/documentation/messageui/mfmessagecomposeviewcontrollerdelegate)