@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.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,839 +1,617 @@
|
|
|
1
|
-
# PDFKit
|
|
1
|
+
# PDFKit patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
- [
|
|
10
|
-
- [
|
|
8
|
+
- [Forms](#forms)
|
|
9
|
+
- [Building PDFs in code](#building-pdfs-in-code)
|
|
11
10
|
- [Watermarks](#watermarks)
|
|
12
|
-
- [Merging
|
|
11
|
+
- [Merging and splitting](#merging-and-splitting)
|
|
13
12
|
- [Printing](#printing)
|
|
14
|
-
- [
|
|
15
|
-
- [Custom
|
|
16
|
-
- [Custom
|
|
17
|
-
- [
|
|
18
|
-
- [Rendering
|
|
19
|
-
- [
|
|
20
|
-
- [Burning
|
|
21
|
-
- [
|
|
22
|
-
- [Coordinator
|
|
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
|
-
##
|
|
23
|
+
## Forms
|
|
25
24
|
|
|
26
|
-
PDF
|
|
27
|
-
`widgetFieldType
|
|
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
|
|
28
|
+
### Reading and filling
|
|
30
29
|
|
|
31
30
|
```swift
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
43
|
+
return values
|
|
43
44
|
}
|
|
44
|
-
```
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
func allFields(in form: PDFDocument) -> [PDFAnnotation] {
|
|
47
|
+
(0..<form.pageCount).compactMap { form.page(at: $0) }.flatMap(\.annotations)
|
|
48
|
+
}
|
|
47
49
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
66
|
+
### Creating fields
|
|
82
67
|
|
|
83
68
|
```swift
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
123
|
+
## Building PDFs in code
|
|
168
124
|
|
|
169
|
-
### From
|
|
125
|
+
### From images
|
|
170
126
|
|
|
171
127
|
```swift
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
138
|
+
return book
|
|
180
139
|
}
|
|
181
|
-
```
|
|
182
140
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
149
|
+
`PDFPage(image:)` is failable. `PDFPage(image:options:)` accepts
|
|
150
|
+
`PDFPage.ImageInitializationOption` keys such as `.compressionQuality` and
|
|
151
|
+
`.mediaBox`.
|
|
202
152
|
|
|
203
|
-
|
|
153
|
+
### With full layout control
|
|
204
154
|
|
|
205
155
|
```swift
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
-
|
|
264
|
-
let
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
232
|
+
## Merging and splitting
|
|
335
233
|
|
|
336
234
|
```swift
|
|
337
|
-
|
|
235
|
+
import PDFKit
|
|
236
|
+
|
|
237
|
+
func combine(_ parts: [PDFDocument]) -> PDFDocument {
|
|
338
238
|
let merged = PDFDocument()
|
|
339
|
-
var
|
|
340
|
-
for
|
|
341
|
-
for
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
258
|
+
return piece
|
|
368
259
|
}
|
|
369
|
-
```
|
|
370
260
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
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
|
-
|
|
405
|
-
|
|
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
|
-
|
|
421
|
-
func
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
296
|
+
To print only some pages, build a smaller document with `excerpt` above and
|
|
297
|
+
print that.
|
|
439
298
|
|
|
440
|
-
|
|
299
|
+
## Outlines
|
|
441
300
|
|
|
442
|
-
|
|
301
|
+
`PDFOutline` is the table of contents (the bookmarks sidebar in most readers).
|
|
443
302
|
|
|
444
303
|
```swift
|
|
445
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
if let
|
|
500
|
-
|
|
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
|
-
|
|
338
|
+
Start reading from `document.outlineRoot`.
|
|
508
339
|
|
|
509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
548
|
-
class AnnotationDelegate: NSObject, PDFDocumentDelegate {
|
|
364
|
+
final class AnnotationFactory: NSObject, PDFDocumentDelegate {
|
|
549
365
|
func `class`(forAnnotationType annotationType: String) -> AnyClass {
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
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
|
-
|
|
567
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
-
controller, SwiftUI coordinator, or another object that outlives the `PDFView`.
|
|
405
|
+
## Overlay lifecycle
|
|
612
406
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
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
|
-
|
|
619
|
-
|
|
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
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
return textView
|
|
429
|
+
let pad = StrokePad()
|
|
430
|
+
pad.points = savedStrokes[page] ?? []
|
|
431
|
+
return pad
|
|
626
432
|
}
|
|
627
433
|
|
|
628
|
-
func pdfView(
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
page content.
|
|
645
|
-
|
|
646
|
-
## Rendering Pages to Images
|
|
441
|
+
final class StrokePad: UIView {
|
|
442
|
+
var points: [CGPoint] = []
|
|
443
|
+
}
|
|
444
|
+
```
|
|
647
445
|
|
|
648
|
-
|
|
446
|
+
## Rendering a page to an image
|
|
649
447
|
|
|
650
448
|
```swift
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
internally.
|
|
470
|
+
## Rotation and cropping
|
|
679
471
|
|
|
680
472
|
```swift
|
|
681
|
-
|
|
682
|
-
```
|
|
683
|
-
|
|
684
|
-
## Page Rotation and Cropping
|
|
685
|
-
|
|
686
|
-
### Rotation
|
|
473
|
+
import PDFKit
|
|
687
474
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
482
|
+
`rotation` accepts only 0, 90, 180 or 270. `setBounds(_:for: .cropBox)` trims
|
|
483
|
+
the visible area.
|
|
694
484
|
|
|
695
|
-
|
|
485
|
+
## Burning in annotations
|
|
696
486
|
|
|
697
487
|
```swift
|
|
698
|
-
|
|
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
|
-
|
|
708
|
-
|
|
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
|
-
|
|
716
|
-
longer
|
|
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
|
-
|
|
498
|
+
## Permissions
|
|
721
499
|
|
|
722
500
|
```swift
|
|
723
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
548
|
+
struct TrackedPDFView: UIViewRepresentable {
|
|
763
549
|
let document: PDFDocument
|
|
764
|
-
@Binding var
|
|
765
|
-
var
|
|
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
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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(_
|
|
778
|
-
|
|
779
|
-
|
|
566
|
+
func updateUIView(_ reader: PDFView, context: Context) {
|
|
567
|
+
context.coordinator.owner = self
|
|
568
|
+
if reader.document !== document {
|
|
569
|
+
reader.document = document
|
|
780
570
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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(_
|
|
578
|
+
static func dismantleUIView(_ reader: PDFView, coordinator: Coordinator) {
|
|
790
579
|
NotificationCenter.default.removeObserver(coordinator)
|
|
791
580
|
}
|
|
792
581
|
|
|
793
|
-
|
|
582
|
+
@MainActor
|
|
583
|
+
final class Coordinator: NSObject, @preconcurrency PDFViewDelegate {
|
|
584
|
+
var owner: TrackedPDFView
|
|
794
585
|
|
|
795
|
-
|
|
796
|
-
|
|
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
|
-
|
|
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
|
-
|
|
820
|
-
NotificationCenter.default
|
|
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
|
|
824
|
-
guard let
|
|
825
|
-
let page =
|
|
826
|
-
let
|
|
827
|
-
let index =
|
|
828
|
-
if
|
|
829
|
-
|
|
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
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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.
|