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