@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,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.