@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,839 +1,617 @@
1
- # PDFKit Patterns
1
+ # PDFKit patterns
2
2
 
3
- Extended patterns for PDFKit: form filling, creating PDFs programmatically,
4
- watermarks, printing, merging documents, custom page overlays, document
5
- outlines, and custom annotation drawing.
3
+ Longer recipes that build on the main skill. Code targets Swift 6.3 and iOS 26;
4
+ functions that touch views are `@MainActor`.
6
5
 
7
6
  ## Contents
8
7
 
9
- - [Form Filling](#form-filling)
10
- - [Creating PDFs Programmatically](#creating-pdfs-programmatically)
8
+ - [Forms](#forms)
9
+ - [Building PDFs in code](#building-pdfs-in-code)
11
10
  - [Watermarks](#watermarks)
12
- - [Merging Documents](#merging-documents)
11
+ - [Merging and splitting](#merging-and-splitting)
13
12
  - [Printing](#printing)
14
- - [Document Outlines](#document-outlines)
15
- - [Custom Annotation Subclasses](#custom-annotation-subclasses)
16
- - [Custom Page Subclasses](#custom-page-subclasses)
17
- - [Page Overlay Lifecycle](#page-overlay-lifecycle)
18
- - [Rendering Pages to Images](#rendering-pages-to-images)
19
- - [Page Rotation and Cropping](#page-rotation-and-cropping)
20
- - [Burning In Annotations](#burning-in-annotations)
21
- - [Document Permissions](#document-permissions)
22
- - [Coordinator Pattern for PDFView](#coordinator-pattern-for-pdfview)
13
+ - [Outlines](#outlines)
14
+ - [Custom annotation classes](#custom-annotation-classes)
15
+ - [Custom page classes](#custom-page-classes)
16
+ - [Overlay lifecycle](#overlay-lifecycle)
17
+ - [Rendering a page to an image](#rendering-a-page-to-an-image)
18
+ - [Rotation and cropping](#rotation-and-cropping)
19
+ - [Burning in annotations](#burning-in-annotations)
20
+ - [Permissions](#permissions)
21
+ - [Coordinator-based SwiftUI wrapper](#coordinator-based-swiftui-wrapper)
23
22
 
24
- ## Form Filling
23
+ ## Forms
25
24
 
26
- PDF forms use widget annotations. Each widget has a `fieldName`, a
27
- `widgetFieldType`, and a `widgetStringValue`.
25
+ Interactive PDF fields are widget annotations. Each one has a `fieldName`, a
26
+ `widgetFieldType` and, for text fields, a `widgetStringValue`.
28
27
 
29
- ### Reading Form Fields
28
+ ### Reading and filling
30
29
 
31
30
  ```swift
32
- func extractFormFields(from document: PDFDocument) -> [(name: String, value: String)] {
33
- var fields: [(String, String)] = []
34
- for pageIndex in 0..<document.pageCount {
35
- guard let page = document.page(at: pageIndex) else { continue }
36
- for annotation in page.annotations {
37
- guard annotation.widgetFieldType == .text,
38
- let name = annotation.fieldName else { continue }
39
- fields.append((name, annotation.widgetStringValue ?? ""))
31
+ import PDFKit
32
+
33
+ func textFieldValues(in form: PDFDocument) -> [String: String] {
34
+ var values: [String: String] = [:]
35
+ for index in 0..<form.pageCount {
36
+ guard let page = form.page(at: index) else { continue }
37
+ for field in page.annotations where field.widgetFieldType == .text {
38
+ if let name = field.fieldName {
39
+ values[name] = field.widgetStringValue ?? ""
40
+ }
40
41
  }
41
42
  }
42
- return fields
43
+ return values
43
44
  }
44
- ```
45
45
 
46
- ### Filling Text Fields
46
+ func allFields(in form: PDFDocument) -> [PDFAnnotation] {
47
+ (0..<form.pageCount).compactMap { form.page(at: $0) }.flatMap(\.annotations)
48
+ }
47
49
 
48
- ```swift
49
- func fillTextField(in document: PDFDocument, fieldName: String, value: String) {
50
- for pageIndex in 0..<document.pageCount {
51
- guard let page = document.page(at: pageIndex) else { continue }
52
- for annotation in page.annotations {
53
- if annotation.widgetFieldType == .text,
54
- annotation.fieldName == fieldName {
55
- annotation.widgetStringValue = value
56
- return
57
- }
58
- }
50
+ func setText(_ value: String, field name: String, in form: PDFDocument) {
51
+ for field in allFields(in: form) where field.widgetFieldType == .text && field.fieldName == name {
52
+ field.widgetStringValue = value
59
53
  }
60
54
  }
61
- ```
62
-
63
- ### Filling Checkbox Fields
64
55
 
65
- ```swift
66
- func setCheckbox(in document: PDFDocument, fieldName: String, checked: Bool) {
67
- for pageIndex in 0..<document.pageCount {
68
- guard let page = document.page(at: pageIndex) else { continue }
69
- for annotation in page.annotations {
70
- if annotation.widgetFieldType == .button,
71
- annotation.widgetControlType == .checkBoxControl,
72
- annotation.fieldName == fieldName {
73
- annotation.buttonWidgetState = checked ? .onState : .offState
74
- return
75
- }
76
- }
56
+ func setCheckbox(_ name: String, checked: Bool, in form: PDFDocument) {
57
+ for field in allFields(in: form)
58
+ where field.widgetFieldType == .button
59
+ && field.widgetControlType == .checkBoxControl
60
+ && field.fieldName == name {
61
+ field.buttonWidgetState = checked ? .onState : .offState
77
62
  }
78
63
  }
79
64
  ```
80
65
 
81
- ### Creating Widget Annotations
66
+ ### Creating fields
82
67
 
83
68
  ```swift
84
- // Text field widget
85
- func createTextField(
86
- bounds: CGRect,
87
- fieldName: String,
88
- placeholder: String = ""
89
- ) -> PDFAnnotation {
90
- let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
91
- widget.widgetFieldType = .text
92
- widget.fieldName = fieldName
93
- widget.widgetStringValue = placeholder
94
- widget.font = UIFont.systemFont(ofSize: 12)
95
- widget.backgroundColor = UIColor.systemGray6
96
- return widget
97
- }
98
-
99
- // Checkbox widget
100
- func createCheckbox(bounds: CGRect, fieldName: String) -> PDFAnnotation {
101
- let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
102
- widget.widgetFieldType = .button
103
- widget.widgetControlType = .checkBoxControl
104
- widget.fieldName = fieldName
105
- widget.buttonWidgetState = .offState
106
- return widget
107
- }
69
+ import PDFKit
70
+ import UIKit
71
+
72
+ func registrationForm() -> PDFDocument {
73
+ let sheet = PDFPage()
74
+ sheet.setBounds(CGRect(x: 0, y: 0, width: 612, height: 792), for: .mediaBox)
75
+
76
+ let fullName = PDFAnnotation(bounds: CGRect(x: 72, y: 690, width: 300, height: 24), forType: .widget, withProperties: nil)
77
+ fullName.widgetFieldType = .text
78
+ fullName.fieldName = "fullName"
79
+ fullName.widgetStringValue = ""
80
+ fullName.font = .systemFont(ofSize: 13)
81
+ fullName.backgroundColor = UIColor.systemGray6
82
+ sheet.addAnnotation(fullName)
83
+
84
+ let newsletter = PDFAnnotation(bounds: CGRect(x: 72, y: 650, width: 18, height: 18), forType: .widget, withProperties: nil)
85
+ newsletter.widgetFieldType = .button
86
+ newsletter.widgetControlType = .checkBoxControl
87
+ newsletter.fieldName = "newsletter"
88
+ newsletter.buttonWidgetState = .offState
89
+ sheet.addAnnotation(newsletter)
90
+
91
+ for (offset, size) in ["S", "M", "L"].enumerated() {
92
+ let option = PDFAnnotation(bounds: CGRect(x: 72 + offset * 40, y: 610, width: 18, height: 18), forType: .widget, withProperties: nil)
93
+ option.widgetFieldType = .button
94
+ option.widgetControlType = .radioButtonControl
95
+ option.fieldName = "shirtSize"
96
+ option.buttonWidgetStateString = size
97
+ sheet.addAnnotation(option)
98
+ }
108
99
 
109
- // Radio button widget
110
- func createRadioButton(
111
- bounds: CGRect,
112
- groupName: String,
113
- stateString: String
114
- ) -> PDFAnnotation {
115
- let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
116
- widget.widgetFieldType = .button
117
- widget.widgetControlType = .radioButtonControl
118
- widget.fieldName = groupName
119
- widget.buttonWidgetStateString = stateString
120
- return widget
121
- }
100
+ let country = PDFAnnotation(bounds: CGRect(x: 72, y: 560, width: 200, height: 24), forType: .widget, withProperties: nil)
101
+ country.widgetFieldType = .choice
102
+ country.fieldName = "country"
103
+ country.choices = ["Canada", "Chile", "Japan"]
104
+ country.isListChoice = false
105
+ sheet.addAnnotation(country)
122
106
 
123
- // Choice widget (dropdown)
124
- func createDropdown(
125
- bounds: CGRect,
126
- fieldName: String,
127
- options: [String]
128
- ) -> PDFAnnotation {
129
- let widget = PDFAnnotation(bounds: bounds, forType: .widget, withProperties: nil)
130
- widget.widgetFieldType = .choice
131
- widget.fieldName = fieldName
132
- widget.choices = options
133
- widget.isListChoice = false // false = dropdown, true = list box
134
- return widget
107
+ let form = PDFDocument()
108
+ form.insert(sheet, at: 0)
109
+ return form
135
110
  }
136
111
  ```
137
112
 
138
- ### Building a Simple Form
113
+ | Field | `widgetFieldType` | Extra settings |
114
+ |---|---|---|
115
+ | text box | `.text` | `widgetStringValue`, `font`, `backgroundColor` |
116
+ | checkbox | `.button` | `widgetControlType = .checkBoxControl`, start at `.offState` |
117
+ | radio button | `.button` | `.radioButtonControl`; `fieldName` is the group, `buttonWidgetStateString` the value |
118
+ | dropdown | `.choice` | `choices`, `isListChoice = false` |
119
+ | list box | `.choice` | `choices`, `isListChoice = true` |
139
120
 
140
- ```swift
141
- func buildForm() -> PDFDocument {
142
- let document = PDFDocument()
143
- let page = PDFPage()
144
- let pageBounds = CGRect(x: 0, y: 0, width: 612, height: 792) // Letter size
145
- page.setBounds(pageBounds, for: .mediaBox)
146
-
147
- // Name field
148
- let nameField = createTextField(
149
- bounds: CGRect(x: 100, y: 700, width: 200, height: 24),
150
- fieldName: "name",
151
- placeholder: ""
152
- )
153
- page.addAnnotation(nameField)
154
-
155
- // Agree checkbox
156
- let checkbox = createCheckbox(
157
- bounds: CGRect(x: 100, y: 660, width: 20, height: 20),
158
- fieldName: "agree"
159
- )
160
- page.addAnnotation(checkbox)
161
-
162
- document.insert(page, at: 0)
163
- return document
164
- }
165
- ```
121
+ A US Letter page is 612 by 792 points; set it with `setBounds(_:for: .mediaBox)`.
166
122
 
167
- ## Creating PDFs Programmatically
123
+ ## Building PDFs in code
168
124
 
169
- ### From Images
125
+ ### From images
170
126
 
171
127
  ```swift
172
- func createPDFFromImages(_ images: [UIImage]) -> PDFDocument {
173
- let document = PDFDocument()
174
- for (index, image) in images.enumerated() {
175
- if let page = PDFPage(image: image) {
176
- document.insert(page, at: index)
128
+ import PDFKit
129
+ import UIKit
130
+
131
+ func album(from photos: [UIImage]) -> PDFDocument {
132
+ let book = PDFDocument()
133
+ for (slot, photo) in photos.enumerated() {
134
+ if let page = PDFPage(image: photo) {
135
+ book.insert(page, at: slot)
177
136
  }
178
137
  }
179
- return document
138
+ return book
180
139
  }
181
- ```
182
140
 
183
- ### From Images with Options
184
-
185
- ```swift
186
- func createPDFFromImage(
187
- _ image: UIImage,
188
- mediaBox: CGRect? = nil,
189
- compressionQuality: CGFloat = 0.8
190
- ) -> PDFPage? {
191
- var options: [PDFPage.ImageInitializationOption: Any] = [
192
- .compressionQuality: compressionQuality
193
- ]
194
- if let box = mediaBox {
195
- options[.mediaBox] = NSValue(cgRect: box)
196
- }
197
- return PDFPage(image: image, options: options)
141
+ func compactPage(from photo: UIImage) -> PDFPage? {
142
+ PDFPage(image: photo, options: [
143
+ .compressionQuality: 0.7,
144
+ .mediaBox: NSValue(cgRect: CGRect(x: 0, y: 0, width: 595, height: 842))
145
+ ])
198
146
  }
199
147
  ```
200
148
 
201
- ### Using Core Graphics
149
+ `PDFPage(image:)` is failable. `PDFPage(image:options:)` accepts
150
+ `PDFPage.ImageInitializationOption` keys such as `.compressionQuality` and
151
+ `.mediaBox`.
202
152
 
203
- For full control over PDF layout, use `UIGraphicsPDFRenderer`.
153
+ ### With full layout control
204
154
 
205
155
  ```swift
206
- func createPDFWithCoreGraphics() -> Data {
207
- let pageRect = CGRect(x: 0, y: 0, width: 612, height: 792)
208
- let renderer = UIGraphicsPDFRenderer(bounds: pageRect)
209
-
210
- return renderer.pdfData { context in
211
- context.beginPage()
212
-
213
- // Draw title
214
- let title = "Document Title"
215
- let titleAttributes: [NSAttributedString.Key: Any] = [
216
- .font: UIFont.boldSystemFont(ofSize: 24),
217
- .foregroundColor: UIColor.black
218
- ]
219
- title.draw(at: CGPoint(x: 50, y: 50), withAttributes: titleAttributes)
220
-
221
- // Draw body text
222
- let body = "This is the body text of the PDF document."
223
- let bodyAttributes: [NSAttributedString.Key: Any] = [
224
- .font: UIFont.systemFont(ofSize: 12),
225
- .foregroundColor: UIColor.darkGray
226
- ]
227
- let bodyRect = CGRect(x: 50, y: 100, width: 512, height: 600)
228
- body.draw(in: bodyRect, withAttributes: bodyAttributes)
229
-
230
- // Draw a line
231
- context.cgContext.setStrokeColor(UIColor.gray.cgColor)
232
- context.cgContext.setLineWidth(1)
233
- context.cgContext.move(to: CGPoint(x: 50, y: 90))
234
- context.cgContext.addLine(to: CGPoint(x: 562, y: 90))
235
- context.cgContext.strokePath()
156
+ import PDFKit
157
+ import UIKit
158
+
159
+ func receiptPDF(lines: [String]) -> PDFDocument? {
160
+ let paper = CGRect(x: 0, y: 0, width: 420, height: 595)
161
+ let bytes = UIGraphicsPDFRenderer(bounds: paper).pdfData { ctx in
162
+ ctx.beginPage()
163
+ let style: [NSAttributedString.Key: Any] = [.font: UIFont.monospacedSystemFont(ofSize: 12, weight: .regular)]
164
+ for (row, text) in lines.enumerated() {
165
+ text.draw(at: CGPoint(x: 32, y: 40 + CGFloat(row) * 18), withAttributes: style)
166
+ }
167
+ let rule = ctx.cgContext
168
+ rule.move(to: CGPoint(x: 32, y: 30))
169
+ rule.addLine(to: CGPoint(x: paper.width - 32, y: 30))
170
+ rule.strokePath()
236
171
  }
172
+ return PDFDocument(data: bytes)
237
173
  }
238
174
  ```
239
175
 
240
- ### Loading Core Graphics PDF into PDFDocument
241
-
242
- ```swift
243
- let pdfData = createPDFWithCoreGraphics()
244
- let document = PDFDocument(data: pdfData)
245
- ```
176
+ `UIGraphicsPDFRenderer(bounds:)` plus `pdfData { $0.beginPage() ... }` gives a
177
+ normal UIKit drawing context; load the `Data` back with `PDFDocument(data:)`.
246
178
 
247
179
  ## Watermarks
248
180
 
249
- Add watermarks by subclassing `PDFPage` and overriding `draw(with:to:)`.
250
-
251
- ### Text Watermark
181
+ Subclass `PDFPage`, override `draw(with:to:)`, and call `super` first so the
182
+ original content is underneath.
252
183
 
253
184
  ```swift
254
- class WatermarkedPage: PDFPage {
255
- var watermarkText: String = "CONFIDENTIAL"
185
+ import PDFKit
186
+ import UIKit
256
187
 
188
+ final class ConfidentialPage: PDFPage {
257
189
  override func draw(with box: PDFDisplayBox, to context: CGContext) {
258
190
  super.draw(with: box, to: context)
259
-
191
+ let area = bounds(for: box)
260
192
  UIGraphicsPushContext(context)
261
193
  context.saveGState()
262
-
263
- let pageBounds = bounds(for: box)
264
- let center = CGPoint(x: pageBounds.midX, y: pageBounds.midY)
265
-
266
- // Rotate around center
267
- context.translateBy(x: center.x, y: center.y)
268
- context.rotate(by: -.pi / 4) // -45 degrees
269
- context.translateBy(x: -center.x, y: -center.y)
270
-
271
- let attributes: [NSAttributedString.Key: Any] = [
272
- .font: UIFont.boldSystemFont(ofSize: 72),
273
- .foregroundColor: UIColor.red.withAlphaComponent(0.15)
274
- ]
275
-
276
- let textSize = watermarkText.size(withAttributes: attributes)
277
- let textOrigin = CGPoint(
278
- x: center.x - textSize.width / 2,
279
- y: center.y - textSize.height / 2
280
- )
281
- watermarkText.draw(at: textOrigin, withAttributes: attributes)
282
-
194
+ context.translateBy(x: area.midX, y: area.midY)
195
+ context.rotate(by: -.pi / 4)
196
+ let stamp = NSAttributedString(string: "CONFIDENTIAL", attributes: [
197
+ .font: UIFont.systemFont(ofSize: 60, weight: .heavy),
198
+ .foregroundColor: UIColor.systemRed.withAlphaComponent(0.15)
199
+ ])
200
+ let size = stamp.size()
201
+ stamp.draw(at: CGPoint(x: -size.width / 2, y: -size.height / 2))
283
202
  context.restoreGState()
284
203
  UIGraphicsPopContext()
285
204
  }
286
205
  }
287
- ```
288
-
289
- ### Applying Watermarks via Document Delegate
290
-
291
- ```swift
292
- class WatermarkDelegate: NSObject, PDFDocumentDelegate {
293
- func classForPage() -> AnyClass {
294
- WatermarkedPage.self
295
- }
296
- }
297
-
298
- // Usage
299
- let delegate = WatermarkDelegate()
300
- document.delegate = delegate
301
- pdfView.document = document
302
- ```
303
-
304
- ### Image Watermark
305
206
 
306
- ```swift
307
- class ImageWatermarkedPage: PDFPage {
308
- var watermarkImage: UIImage?
207
+ final class LogoPage: PDFPage {
208
+ static let logo = UIImage(systemName: "seal")?.cgImage
309
209
 
310
210
  override func draw(with box: PDFDisplayBox, to context: CGContext) {
311
211
  super.draw(with: box, to: context)
312
-
313
- guard let image = watermarkImage?.cgImage else { return }
314
-
315
- let pageBounds = bounds(for: box)
316
- let imageSize = CGSize(width: 200, height: 200)
317
- let imageRect = CGRect(
318
- x: pageBounds.midX - imageSize.width / 2,
319
- y: pageBounds.midY - imageSize.height / 2,
320
- width: imageSize.width,
321
- height: imageSize.height
322
- )
323
-
212
+ guard let mark = Self.logo else { return }
213
+ let area = bounds(for: box)
324
214
  context.saveGState()
325
215
  context.setAlpha(0.1)
326
- context.draw(image, in: imageRect)
216
+ context.draw(mark, in: CGRect(x: area.midX - 100, y: area.midY - 100, width: 200, height: 200))
327
217
  context.restoreGState()
328
218
  }
329
219
  }
220
+
221
+ final class WatermarkSource: NSObject, PDFDocumentDelegate {
222
+ func classForPage() -> AnyClass {
223
+ ConfidentialPage.self
224
+ }
225
+ }
330
226
  ```
331
227
 
332
- ## Merging Documents
228
+ The document asks its delegate which page class to instantiate through
229
+ `classForPage()`. Set `document.delegate` before the document is handed to a
230
+ `PDFView`, and keep the delegate object alive.
333
231
 
334
- ### Append All Pages
232
+ ## Merging and splitting
335
233
 
336
234
  ```swift
337
- func mergeDocuments(_ documents: [PDFDocument]) -> PDFDocument {
235
+ import PDFKit
236
+
237
+ func combine(_ parts: [PDFDocument]) -> PDFDocument {
338
238
  let merged = PDFDocument()
339
- var insertIndex = 0
340
- for document in documents {
341
- for pageIndex in 0..<document.pageCount {
342
- guard let page = document.page(at: pageIndex) else { continue }
343
- merged.insert(page, at: insertIndex)
344
- insertIndex += 1
239
+ var next = 0
240
+ for part in parts {
241
+ for index in 0..<part.pageCount {
242
+ if let page = part.page(at: index) {
243
+ merged.insert(page, at: next)
244
+ next += 1
245
+ }
345
246
  }
346
247
  }
347
248
  return merged
348
249
  }
349
- ```
350
-
351
- ### Extract Page Range
352
250
 
353
- ```swift
354
- func extractPages(
355
- from document: PDFDocument,
356
- range: ClosedRange<Int>
357
- ) -> PDFDocument {
358
- let extracted = PDFDocument()
359
- var insertIndex = 0
360
- for pageIndex in range {
361
- guard pageIndex >= 0,
362
- pageIndex < document.pageCount,
363
- let page = document.page(at: pageIndex) else { continue }
364
- extracted.insert(page, at: insertIndex)
365
- insertIndex += 1
251
+ func excerpt(_ span: ClosedRange<Int>, of source: PDFDocument) -> PDFDocument {
252
+ let piece = PDFDocument()
253
+ for index in span where index >= 0 && index < source.pageCount {
254
+ if let page = source.page(at: index) {
255
+ piece.insert(page, at: piece.pageCount)
256
+ }
366
257
  }
367
- return extracted
258
+ return piece
368
259
  }
369
- ```
370
260
 
371
- ### Split Document
372
-
373
- ```swift
374
- func splitDocument(_ document: PDFDocument, pagesPerChunk: Int) -> [PDFDocument] {
375
- var chunks: [PDFDocument] = []
376
- var chunkDoc = PDFDocument()
377
- var chunkIndex = 0
378
-
379
- for pageIndex in 0..<document.pageCount {
380
- guard let page = document.page(at: pageIndex) else { continue }
381
- chunkDoc.insert(page, at: chunkIndex)
382
- chunkIndex += 1
383
-
384
- if chunkIndex >= pagesPerChunk {
385
- chunks.append(chunkDoc)
386
- chunkDoc = PDFDocument()
387
- chunkIndex = 0
261
+ func chunks(of source: PDFDocument, size: Int) -> [PDFDocument] {
262
+ var result: [PDFDocument] = []
263
+ var current = PDFDocument()
264
+ for index in 0..<source.pageCount {
265
+ if let page = source.page(at: index) {
266
+ current.insert(page, at: current.pageCount)
267
+ }
268
+ if current.pageCount == size {
269
+ result.append(current)
270
+ current = PDFDocument()
388
271
  }
389
272
  }
390
-
391
- if chunkDoc.pageCount > 0 {
392
- chunks.append(chunkDoc)
393
- }
394
-
395
- return chunks
273
+ if current.pageCount > 0 { result.append(current) }
274
+ return result
396
275
  }
397
276
  ```
398
277
 
399
278
  ## Printing
400
279
 
401
- ### Using UIPrintInteractionController
402
-
403
280
  ```swift
404
- func printPDF(document: PDFDocument, from viewController: UIViewController) {
405
- guard let data = document.dataRepresentation() else { return }
406
-
407
- let printController = UIPrintInteractionController.shared
408
- let printInfo = UIPrintInfo(dictionary: nil)
409
- printInfo.outputType = .general
410
- printInfo.jobName = "PDF Document"
411
-
412
- printController.printInfo = printInfo
413
- printController.printingItem = data
414
- printController.present(animated: true)
415
- }
416
- ```
417
-
418
- ### Print a Specific Page Range
281
+ import PDFKit
282
+ import UIKit
419
283
 
420
- ```swift
421
- func printPageRange(
422
- document: PDFDocument,
423
- range: ClosedRange<Int>,
424
- from viewController: UIViewController
425
- ) {
426
- let subset = extractPages(from: document, range: range)
427
- guard let data = subset.dataRepresentation() else { return }
428
-
429
- let printController = UIPrintInteractionController.shared
430
- let printInfo = UIPrintInfo(dictionary: nil)
431
- printInfo.outputType = .general
432
- printController.printInfo = printInfo
433
- printController.printingItem = data
434
- printController.present(animated: true)
284
+ @MainActor
285
+ func printDocument(_ file: PDFDocument, title: String) {
286
+ let job = UIPrintInfo(dictionary: nil)
287
+ job.outputType = .general
288
+ job.jobName = title
289
+ let printer = UIPrintInteractionController.shared
290
+ printer.printInfo = job
291
+ printer.printingItem = file.dataRepresentation()
292
+ printer.present(animated: true)
435
293
  }
436
294
  ```
437
295
 
438
- ## Document Outlines
296
+ To print only some pages, build a smaller document with `excerpt` above and
297
+ print that.
439
298
 
440
- `PDFOutline` represents the table of contents (bookmarks) of a PDF.
299
+ ## Outlines
441
300
 
442
- ### Reading Outlines
301
+ `PDFOutline` is the table of contents (the bookmarks sidebar in most readers).
443
302
 
444
303
  ```swift
445
- func printOutline(_ outline: PDFOutline, level: Int = 0) {
446
- let indent = String(repeating: " ", count: level)
447
- if let label = outline.label {
448
- print("\(indent)\(label)")
449
- }
450
- for i in 0..<outline.numberOfChildren {
451
- if let child = outline.child(at: i) {
452
- printOutline(child, level: level + 1)
453
- }
454
- }
455
- }
304
+ import PDFKit
456
305
 
457
- // Usage
458
- if let root = document.outlineRoot {
459
- printOutline(root)
306
+ func titles(under node: PDFOutline, depth: Int = 0) -> [String] {
307
+ var found: [String] = []
308
+ for index in 0..<node.numberOfChildren {
309
+ guard let child = node.child(at: index) else { continue }
310
+ found.append(String(repeating: " ", count: depth) + (child.label ?? ""))
311
+ found += titles(under: child, depth: depth + 1)
312
+ }
313
+ return found
460
314
  }
461
- ```
462
315
 
463
- ### Creating Outlines
464
-
465
- ```swift
466
- func buildOutline(for document: PDFDocument) {
316
+ func addChapters(_ names: [String], startPages: [Int], to file: PDFDocument) {
467
317
  let root = PDFOutline()
468
-
469
- func destination(for pageIndex: Int) -> PDFDestination? {
470
- guard pageIndex >= 0,
471
- pageIndex < document.pageCount,
472
- let page = document.page(at: pageIndex) else { return nil }
473
- return PDFDestination(page: page, at: .zero)
318
+ for (slot, name) in names.enumerated() {
319
+ guard slot < startPages.count, let page = file.page(at: startPages[slot]) else { continue }
320
+ let chapter = PDFOutline()
321
+ chapter.label = name
322
+ chapter.destination = PDFDestination(page: page, at: .zero)
323
+ root.insertChild(chapter, at: slot)
474
324
  }
475
-
476
- let chapter1 = PDFOutline()
477
- chapter1.label = "Chapter 1"
478
- chapter1.destination = destination(for: 0)
479
- root.insertChild(chapter1, at: 0)
480
-
481
- let section1_1 = PDFOutline()
482
- section1_1.label = "Section 1.1"
483
- section1_1.destination = destination(for: 2)
484
- chapter1.insertChild(section1_1, at: 0)
485
-
486
- let chapter2 = PDFOutline()
487
- chapter2.label = "Chapter 2"
488
- chapter2.destination = destination(for: 5)
489
- root.insertChild(chapter2, at: 1)
490
-
491
- document.outlineRoot = root
325
+ file.outlineRoot = root
492
326
  }
493
- ```
494
327
 
495
- ### Navigating to an Outline Entry
496
-
497
- ```swift
498
- func goToOutlineEntry(_ outline: PDFOutline, in pdfView: PDFView) {
499
- if let destination = outline.destination {
500
- pdfView.go(to: destination)
501
- } else if let action = outline.action {
502
- pdfView.perform(action)
328
+ @MainActor
329
+ func open(_ entry: PDFOutline, in reader: PDFView) {
330
+ if let target = entry.destination {
331
+ reader.go(to: target)
332
+ } else if let action = entry.action {
333
+ reader.perform(action)
503
334
  }
504
335
  }
505
336
  ```
506
337
 
507
- ## Custom Annotation Subclasses
338
+ Start reading from `document.outlineRoot`.
508
339
 
509
- Override `draw(with:in:)` to render custom annotation graphics.
340
+ ## Custom annotation classes
341
+
342
+ Annotations draw with `draw(with:in:)`; pages use `draw(with:to:)`. Mind the
343
+ different label.
510
344
 
511
345
  ```swift
512
- class CircleStampAnnotation: PDFAnnotation {
346
+ import PDFKit
347
+ import UIKit
348
+
349
+ final class CircledNote: PDFAnnotation {
513
350
  override func draw(with box: PDFDisplayBox, in context: CGContext) {
514
351
  super.draw(with: box, in: context)
515
-
516
352
  UIGraphicsPushContext(context)
517
- context.saveGState()
518
-
519
- let insetBounds = bounds.insetBy(dx: 2, dy: 2)
520
- let path = UIBezierPath(ovalIn: insetBounds)
521
- path.lineWidth = 3
522
-
523
- UIColor.systemRed.setStroke()
524
- path.stroke()
525
-
526
- // Draw centered text
527
- let text = "REVIEWED"
528
- let attributes: [NSAttributedString.Key: Any] = [
529
- .font: UIFont.boldSystemFont(ofSize: 10),
530
- .foregroundColor: UIColor.systemRed
531
- ]
532
- let textSize = text.size(withAttributes: attributes)
533
- let textOrigin = CGPoint(
534
- x: bounds.midX - textSize.width / 2,
535
- y: bounds.midY - textSize.height / 2
536
- )
537
- text.draw(at: textOrigin, withAttributes: attributes)
538
-
539
- context.restoreGState()
353
+ let ring = UIBezierPath(ovalIn: bounds.insetBy(dx: 3, dy: 3))
354
+ ring.lineWidth = 2
355
+ UIColor.systemPurple.setStroke()
356
+ ring.stroke()
357
+ let tag = NSAttributedString(string: contents ?? "", attributes: [.font: UIFont.systemFont(ofSize: 10)])
358
+ let size = tag.size()
359
+ tag.draw(at: CGPoint(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2))
540
360
  UIGraphicsPopContext()
541
361
  }
542
362
  }
543
- ```
544
-
545
- ### Registering Custom Annotations via Delegate
546
363
 
547
- ```swift
548
- class AnnotationDelegate: NSObject, PDFDocumentDelegate {
364
+ final class AnnotationFactory: NSObject, PDFDocumentDelegate {
549
365
  func `class`(forAnnotationType annotationType: String) -> AnyClass {
550
- switch annotationType {
551
- case "CircleStamp":
552
- return CircleStampAnnotation.self
553
- default:
554
- return PDFAnnotation.self
555
- }
366
+ annotationType == "Circle" ? CircledNote.self : PDFAnnotation.self
556
367
  }
557
368
  }
558
369
  ```
559
370
 
560
- ## Custom Page Subclasses
371
+ A document asks `class(forAnnotationType:)` which class to create for each
372
+ annotation it reads; return `PDFAnnotation.self` for anything you do not
373
+ customise.
374
+
375
+ ## Custom page classes
561
376
 
562
- Override `draw(with:to:)` for page-level custom drawing like headers,
563
- footers, or decorative borders.
377
+ Headers, footers and borders work the same way as watermarks. Remember that
378
+ y = 0 is the bottom edge.
564
379
 
565
380
  ```swift
566
- class HeaderFooterPage: PDFPage {
567
- var headerText: String = ""
568
- var footerText: String = ""
381
+ import PDFKit
382
+ import UIKit
569
383
 
384
+ final class NumberedPage: PDFPage {
570
385
  override func draw(with box: PDFDisplayBox, to context: CGContext) {
571
386
  super.draw(with: box, to: context)
572
-
387
+ let area = bounds(for: box)
388
+ let small: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 9)]
573
389
  UIGraphicsPushContext(context)
574
390
  context.saveGState()
575
-
576
- let pageBounds = bounds(for: box)
577
- let attributes: [NSAttributedString.Key: Any] = [
578
- .font: UIFont.systemFont(ofSize: 10),
579
- .foregroundColor: UIColor.gray
580
- ]
581
-
582
- // Header (top of page in PDF coordinates)
583
- if !headerText.isEmpty {
584
- let headerSize = headerText.size(withAttributes: attributes)
585
- let headerOrigin = CGPoint(
586
- x: pageBounds.midX - headerSize.width / 2,
587
- y: pageBounds.maxY - 30
588
- )
589
- headerText.draw(at: headerOrigin, withAttributes: attributes)
590
- }
591
-
592
- // Footer (bottom of page in PDF coordinates)
593
- if !footerText.isEmpty {
594
- let footerSize = footerText.size(withAttributes: attributes)
595
- let footerOrigin = CGPoint(
596
- x: pageBounds.midX - footerSize.width / 2,
597
- y: 20
598
- )
599
- footerText.draw(at: footerOrigin, withAttributes: attributes)
600
- }
601
-
391
+ context.translateBy(x: 0, y: area.height)
392
+ context.scaleBy(x: 1, y: -1)
393
+ ("Quarterly report" as NSString).draw(at: CGPoint(x: 40, y: 18), withAttributes: small)
394
+ ("Page \(pageRef?.pageNumber ?? 0)" as NSString).draw(at: CGPoint(x: 40, y: area.height - 30), withAttributes: small)
602
395
  context.restoreGState()
603
396
  UIGraphicsPopContext()
604
397
  }
605
398
  }
606
399
  ```
607
400
 
608
- ## Page Overlay Lifecycle
401
+ In unflipped PDF space the header sits near `area.maxY - 30` and the footer
402
+ near y = 20. The example flips the context so UIKit text draws upright, which
403
+ turns those into a small y for the header and `height - 30` for the footer.
609
404
 
610
- `PDFView.pageOverlayViewProvider` is weak. Store the provider on a view
611
- controller, SwiftUI coordinator, or another object that outlives the `PDFView`.
405
+ ## Overlay lifecycle
612
406
 
613
- PDFKit requests overlay views for pages it is preparing or displaying, then
614
- calls the lifecycle hooks as pages enter and leave the visible range. Keep
615
- overlay state outside the view so scrolling does not discard edits.
407
+ - Keep the `PDFPageOverlayViewProvider` on something that outlives the
408
+ `PDFView`: a view controller, a SwiftUI coordinator or a model object.
409
+ - PDFKit asks for overlays as pages are prepared for display and tells the
410
+ provider when they leave the visible range. Keep editing state outside the
411
+ views so scrolling away and back does not lose it.
412
+ - `pdfView(_:willEndDisplayingOverlayView:for:)` is where to capture what the
413
+ user did, for example into a dictionary keyed by `PDFPage`.
414
+ - Overlay views are UIKit (or AppKit) views, not PDF content. `write(to:)` and
415
+ `dataRepresentation()` ignore them.
416
+ - Before saving, turn overlay content into real PDF annotations, annotations
417
+ with custom appearance streams, or drawn page content. Add
418
+ `.burnInAnnotationsOption` when the result must not be editable.
616
419
 
617
420
  ```swift
618
- class PageOverlayProvider: NSObject, PDFPageOverlayViewProvider {
619
- private var pageNotes: [PDFPage: String] = [:]
421
+ import PDFKit
422
+ import UIKit
423
+
424
+ @MainActor
425
+ final class SignatureOverlays: NSObject, @preconcurrency PDFPageOverlayViewProvider {
426
+ private var savedStrokes: [PDFPage: [CGPoint]] = [:]
620
427
 
621
428
  func pdfView(_ view: PDFView, overlayViewFor page: PDFPage) -> UIView? {
622
- let textView = UITextView()
623
- textView.text = pageNotes[page] ?? ""
624
- textView.backgroundColor = .clear
625
- return textView
429
+ let pad = StrokePad()
430
+ pad.points = savedStrokes[page] ?? []
431
+ return pad
626
432
  }
627
433
 
628
- func pdfView(
629
- _ view: PDFView,
630
- willEndDisplayingOverlayView overlayView: UIView,
631
- for page: PDFPage
632
- ) {
633
- if let textView = overlayView as? UITextView {
634
- pageNotes[page] = textView.text
434
+ func pdfView(_ pdfView: PDFView, willEndDisplayingOverlayView overlayView: UIView, for page: PDFPage) {
435
+ if let pad = overlayView as? StrokePad {
436
+ savedStrokes[page] = pad.points
635
437
  }
636
438
  }
637
439
  }
638
- ```
639
440
 
640
- Overlay views are UIKit/AppKit views, not PDF content. Before calling
641
- `write(to:)` or `dataRepresentation()`, convert overlay data into PDF
642
- annotations, custom annotation appearance streams, or rendered page content.
643
- Use `.burnInAnnotationsOption` when saved annotations should become permanent
644
- page content.
645
-
646
- ## Rendering Pages to Images
441
+ final class StrokePad: UIView {
442
+ var points: [CGPoint] = []
443
+ }
444
+ ```
647
445
 
648
- ### Single Page
446
+ ## Rendering a page to an image
649
447
 
650
448
  ```swift
651
- func renderPage(_ page: PDFPage, scale: CGFloat = 2.0) -> UIImage? {
652
- let pageBounds = page.bounds(for: .mediaBox)
653
- let size = CGSize(
654
- width: pageBounds.width * scale,
655
- height: pageBounds.height * scale
656
- )
657
-
658
- let renderer = UIGraphicsImageRenderer(size: size)
659
- return renderer.image { ctx in
660
- ctx.cgContext.scaleBy(x: scale, y: scale)
661
-
662
- // White background
449
+ import PDFKit
450
+ import UIKit
451
+
452
+ func snapshot(of page: PDFPage, scale: CGFloat = 2.0) -> UIImage {
453
+ let box = page.bounds(for: .mediaBox)
454
+ let size = CGSize(width: box.width * scale, height: box.height * scale)
455
+ return UIGraphicsImageRenderer(size: size).image { ctx in
663
456
  UIColor.white.setFill()
664
457
  ctx.fill(CGRect(origin: .zero, size: size))
665
-
666
- // PDFPage draw uses bottom-left origin; flip the context
667
- ctx.cgContext.translateBy(x: 0, y: pageBounds.height)
668
- ctx.cgContext.scaleBy(x: 1, y: -1)
669
-
670
- page.draw(with: .mediaBox, to: ctx.cgContext)
458
+ let cg = ctx.cgContext
459
+ cg.translateBy(x: 0, y: size.height)
460
+ cg.scaleBy(x: scale, y: -scale)
461
+ page.draw(with: .mediaBox, to: cg)
671
462
  }
672
463
  }
673
464
  ```
674
465
 
675
- ### Thumbnail Shortcut
466
+ `draw(with:to:)` assumes a bottom-left origin, so the context is flipped first.
467
+ `thumbnail(of:for:)` does that flip for you and is the simpler choice when a
468
+ preview-sized image is enough.
676
469
 
677
- The simpler `thumbnail(of:for:)` method handles coordinate flipping
678
- internally.
470
+ ## Rotation and cropping
679
471
 
680
472
  ```swift
681
- let thumbnail = page.thumbnail(of: CGSize(width: 200, height: 260), for: .mediaBox)
682
- ```
683
-
684
- ## Page Rotation and Cropping
685
-
686
- ### Rotation
473
+ import PDFKit
687
474
 
688
- ```swift
689
- // Rotate a page (must be a multiple of 90)
690
- page.rotation = 90 // 0, 90, 180, or 270
475
+ func straighten(_ page: PDFPage) {
476
+ page.rotation = (page.rotation + 90) % 360
477
+ let media = page.bounds(for: .mediaBox)
478
+ page.setBounds(media.insetBy(dx: 36, dy: 36), for: .cropBox)
479
+ }
691
480
  ```
692
481
 
693
- ### Cropping
482
+ `rotation` accepts only 0, 90, 180 or 270. `setBounds(_:for: .cropBox)` trims
483
+ the visible area.
694
484
 
695
- Set the crop box to display only a portion of the page.
485
+ ## Burning in annotations
696
486
 
697
487
  ```swift
698
- let pageBounds = page.bounds(for: .mediaBox)
699
- let cropRect = pageBounds.insetBy(dx: 50, dy: 50)
700
- page.setBounds(cropRect, for: .cropBox)
701
- ```
702
-
703
- ## Burning In Annotations
704
-
705
- Write annotations permanently into the PDF content so they cannot be removed.
488
+ import PDFKit
706
489
 
707
- ```swift
708
- func burnInAnnotations(_ document: PDFDocument, to url: URL) -> Bool {
709
- document.write(to: url, withOptions: [
710
- .burnInAnnotationsOption: true
711
- ])
490
+ func flatten(_ file: PDFDocument, to url: URL) -> Bool {
491
+ file.write(to: url, withOptions: [.burnInAnnotationsOption: true])
712
492
  }
713
493
  ```
714
494
 
715
- After burning in, annotations become part of the page content and are no
716
- longer editable or removable as separate objects.
717
-
718
- ## Document Permissions
495
+ The option is iOS 16+ and the call returns `Bool`. Afterwards the marks are part of the page drawing and
496
+ can no longer be selected, edited or removed as annotations.
719
497
 
720
- Check what operations the PDF allows.
498
+ ## Permissions
721
499
 
722
500
  ```swift
723
- func checkPermissions(_ document: PDFDocument) {
724
- let status = document.permissionsStatus // .none, .user, .owner
725
-
726
- let canCopy = document.allowsCopying
727
- let canPrint = document.allowsPrinting
728
- let canComment = document.allowsCommenting
729
- let canFillForms = document.allowsFormFieldEntry
730
- let canAssemble = document.allowsDocumentAssembly
731
- let canModify = document.allowsDocumentChanges
732
- let canAccessibility = document.allowsContentAccessibility
733
- }
734
- ```
735
-
736
- ### Writing with Access Permissions
501
+ import PDFKit
737
502
 
738
- ```swift
739
- func saveWithRestrictions(_ document: PDFDocument, to url: URL) {
740
- let permissions: PDFAccessPermissions = [
741
- .allowsLowQualityPrinting,
742
- .allowsContentCopying,
743
- .allowsCommenting
744
- ]
745
- document.write(to: url, withOptions: [
746
- .ownerPasswordOption: "ownerSecret",
747
- .userPasswordOption: "userPass",
748
- .accessPermissionsOption: permissions.rawValue
503
+ func describeAccess(_ file: PDFDocument) -> [String] {
504
+ var notes: [String] = []
505
+ switch file.permissionsStatus {
506
+ case .none: notes.append("opened without a password")
507
+ case .user: notes.append("opened with the user password")
508
+ case .owner: notes.append("opened with the owner password")
509
+ @unknown default: break
510
+ }
511
+ if !file.allowsCopying { notes.append("copying blocked") }
512
+ if !file.allowsPrinting { notes.append("printing blocked") }
513
+ if !file.allowsCommenting { notes.append("comments blocked") }
514
+ if !file.allowsFormFieldEntry { notes.append("form entry blocked") }
515
+ if !file.allowsDocumentAssembly { notes.append("page assembly blocked") }
516
+ if !file.allowsDocumentChanges { notes.append("edits blocked") }
517
+ if !file.allowsContentAccessibility { notes.append("accessibility extraction blocked") }
518
+ return notes
519
+ }
520
+
521
+ func lockDown(_ file: PDFDocument, to url: URL) -> Bool {
522
+ let allowed = PDFAccessPermissions.allowsLowQualityPrinting.rawValue
523
+ | PDFAccessPermissions.allowsCommenting.rawValue
524
+ return file.write(to: url, withOptions: [
525
+ .ownerPasswordOption: "archive-owner",
526
+ .userPasswordOption: "",
527
+ .accessPermissionsOption: allowed
749
528
  ])
750
529
  }
751
530
  ```
752
531
 
753
- ## Coordinator Pattern for PDFView
532
+ `permissionsStatus` is `.none`, `.user` or `.owner`. To restrict a saved file,
533
+ pass `PDFAccessPermissions` flags (for example `.allowsLowQualityPrinting`,
534
+ `.allowsContentCopying`, `.allowsCommenting`) as the value of
535
+ `.accessPermissionsOption` (iOS 15+), together with the password options.
536
+ In Swift `PDFAccessPermissions` imports as a plain enum, not an `OptionSet`,
537
+ so combine flags by OR-ing their `rawValue`s. Some flags imply others:
538
+ content copying includes accessibility extraction, commenting includes form
539
+ entry, and document changes include both.
754
540
 
755
- A reusable coordinator that handles delegate callbacks, notifications, and
756
- annotation hit detection for a SwiftUI-wrapped PDFView.
541
+ ## Coordinator-based SwiftUI wrapper
757
542
 
758
543
  ```swift
759
- import SwiftUI
760
544
  import PDFKit
545
+ import SwiftUI
546
+ import UIKit
761
547
 
762
- struct ManagedPDFView: UIViewRepresentable {
548
+ struct TrackedPDFView: UIViewRepresentable {
763
549
  let document: PDFDocument
764
- @Binding var currentPageIndex: Int
765
- var onAnnotationTapped: ((PDFAnnotation) -> Void)?
550
+ @Binding var pageIndex: Int
551
+ var onAnnotationTap: (PDFAnnotation) -> Void = { _ in }
552
+
553
+ func makeCoordinator() -> Coordinator {
554
+ Coordinator(owner: self)
555
+ }
766
556
 
767
557
  func makeUIView(context: Context) -> PDFView {
768
- let pdfView = PDFView()
769
- pdfView.autoScales = true
770
- pdfView.displayMode = .singlePageContinuous
771
- pdfView.document = document
772
- pdfView.delegate = context.coordinator
773
- context.coordinator.attach(to: pdfView)
774
- return pdfView
558
+ let reader = PDFView()
559
+ reader.autoScales = true
560
+ reader.document = document
561
+ reader.delegate = context.coordinator
562
+ context.coordinator.attach(to: reader)
563
+ return reader
775
564
  }
776
565
 
777
- func updateUIView(_ pdfView: PDFView, context: Context) {
778
- if pdfView.document !== document {
779
- pdfView.document = document
566
+ func updateUIView(_ reader: PDFView, context: Context) {
567
+ context.coordinator.owner = self
568
+ if reader.document !== document {
569
+ reader.document = document
780
570
  }
781
- if currentPageIndex >= 0,
782
- currentPageIndex < document.pageCount,
783
- let page = document.page(at: currentPageIndex),
784
- pdfView.currentPage !== page {
785
- pdfView.go(to: page)
571
+ guard pageIndex >= 0, pageIndex < document.pageCount,
572
+ let wanted = document.page(at: pageIndex) else { return }
573
+ if reader.currentPage !== wanted {
574
+ reader.go(to: wanted)
786
575
  }
787
576
  }
788
577
 
789
- static func dismantleUIView(_ uiView: PDFView, coordinator: Coordinator) {
578
+ static func dismantleUIView(_ reader: PDFView, coordinator: Coordinator) {
790
579
  NotificationCenter.default.removeObserver(coordinator)
791
580
  }
792
581
 
793
- func makeCoordinator() -> Coordinator { Coordinator(self) }
582
+ @MainActor
583
+ final class Coordinator: NSObject, @preconcurrency PDFViewDelegate {
584
+ var owner: TrackedPDFView
794
585
 
795
- class Coordinator: NSObject, PDFViewDelegate {
796
- let parent: ManagedPDFView
797
-
798
- init(_ parent: ManagedPDFView) {
799
- self.parent = parent
800
- super.init()
586
+ init(owner: TrackedPDFView) {
587
+ self.owner = owner
801
588
  }
802
589
 
803
- func attach(to pdfView: PDFView) {
590
+ deinit {
804
591
  NotificationCenter.default.removeObserver(self)
805
- NotificationCenter.default.addObserver(
806
- self,
807
- selector: #selector(pageChanged),
808
- name: .PDFViewPageChanged,
809
- object: pdfView
810
- )
811
- NotificationCenter.default.addObserver(
812
- self,
813
- selector: #selector(annotationHit),
814
- name: .PDFViewAnnotationHit,
815
- object: pdfView
816
- )
817
592
  }
818
593
 
819
- deinit {
820
- NotificationCenter.default.removeObserver(self)
594
+ func attach(to reader: PDFView) {
595
+ let center = NotificationCenter.default
596
+ center.removeObserver(self)
597
+ center.addObserver(self, selector: #selector(pageDidChange(_:)), name: .PDFViewPageChanged, object: reader)
598
+ center.addObserver(self, selector: #selector(annotationWasHit(_:)), name: .PDFViewAnnotationHit, object: reader)
821
599
  }
822
600
 
823
- @objc func pageChanged(_ notification: Notification) {
824
- guard let pdfView = notification.object as? PDFView,
825
- let page = pdfView.currentPage,
826
- let doc = pdfView.document else { return }
827
- let index = doc.index(for: page)
828
- if parent.currentPageIndex != index {
829
- parent.currentPageIndex = index
601
+ @objc private func pageDidChange(_ note: Notification) {
602
+ guard let reader = note.object as? PDFView,
603
+ let page = reader.currentPage,
604
+ let file = reader.document else { return }
605
+ let index = file.index(for: page)
606
+ if index != owner.pageIndex {
607
+ owner.pageIndex = index
830
608
  }
831
609
  }
832
610
 
833
- @objc func annotationHit(_ notification: Notification) {
834
- guard let annotation = notification.userInfo?["PDFAnnotationHit"] as? PDFAnnotation
835
- else { return }
836
- parent.onAnnotationTapped?(annotation)
611
+ @objc private func annotationWasHit(_ note: Notification) {
612
+ if let tapped = note.userInfo?["PDFAnnotationHit"] as? PDFAnnotation {
613
+ owner.onAnnotationTap(tapped)
614
+ }
837
615
  }
838
616
 
839
617
  func pdfViewWillClick(onLink sender: PDFView, with url: URL) {
@@ -842,3 +620,14 @@ struct ManagedPDFView: UIViewRepresentable {
842
620
  }
843
621
  }
844
622
  ```
623
+
624
+ - `updateUIView` swaps the document only when `!==` says it changed, and
625
+ navigates only when the requested index is valid and differs from
626
+ `currentPage`.
627
+ - The page handler writes to the binding only on a real change, which avoids
628
+ an update loop between SwiftUI and the view.
629
+ - `dismantleUIView(_:coordinator:)` and the coordinator's `deinit` both remove
630
+ the notification observers; `attach` clears old ones before adding.
631
+ - `.PDFViewAnnotationHit` carries the tapped annotation under the
632
+ `"PDFAnnotationHit"` user-info key.
633
+ - `pdfViewWillClick(onLink:with:)` lets the app decide what a link tap does.