@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
|
@@ -1,78 +1,72 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Picker and PhotoKit recipes
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
---
|
|
3
|
+
Each recipe uses SwiftUI with async/await and needs iOS 17 or later (the views
|
|
4
|
+
use the two-parameter `onChange(of:)`). `PhotosPicker` itself is iOS 16.
|
|
6
5
|
|
|
7
6
|
## Contents
|
|
8
7
|
|
|
9
|
-
- [1.
|
|
10
|
-
- [2.
|
|
11
|
-
- [3.
|
|
12
|
-
- [4.
|
|
13
|
-
- [5. PHPickerViewController
|
|
14
|
-
- [6. Saving
|
|
15
|
-
- [7.
|
|
16
|
-
- [8. HEIC
|
|
17
|
-
- [9.
|
|
18
|
-
- [10.
|
|
8
|
+
- [1. One photo with an explicit loading state](#1-one-photo-with-an-explicit-loading-state)
|
|
9
|
+
- [2. Many photos with progress](#2-many-photos-with-progress)
|
|
10
|
+
- [3. Video from the picker](#3-video-from-the-picker)
|
|
11
|
+
- [4. Live Photos](#4-live-photos)
|
|
12
|
+
- [5. PHPickerViewController inside SwiftUI](#5-phpickerviewcontroller-inside-swiftui)
|
|
13
|
+
- [6. Saving into the library](#6-saving-into-the-library)
|
|
14
|
+
- [7. Thumbnails with ImageIO](#7-thumbnails-with-imageio)
|
|
15
|
+
- [8. HEIC and HEIF](#8-heic-and-heif)
|
|
16
|
+
- [9. Picker appearance](#9-picker-appearance)
|
|
17
|
+
- [10. Cropping](#10-cropping)
|
|
19
18
|
|
|
20
|
-
## 1.
|
|
19
|
+
## 1. One photo with an explicit loading state
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
Model what the screen shows as a small enum instead of a pile of optionals.
|
|
23
22
|
|
|
24
23
|
```swift
|
|
25
24
|
import SwiftUI
|
|
26
25
|
import PhotosUI
|
|
27
26
|
|
|
28
|
-
struct
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
case
|
|
27
|
+
struct AvatarEditor: View {
|
|
28
|
+
enum Stage {
|
|
29
|
+
case empty
|
|
30
|
+
case working
|
|
31
|
+
case ready(Image)
|
|
32
|
+
case failed(String)
|
|
34
33
|
}
|
|
35
34
|
|
|
35
|
+
@State private var pick: PhotosPickerItem?
|
|
36
|
+
@State private var stage: Stage = .empty
|
|
37
|
+
|
|
36
38
|
var body: some View {
|
|
37
|
-
VStack {
|
|
38
|
-
switch
|
|
39
|
-
case .
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
case .loading:
|
|
39
|
+
VStack(spacing: 16) {
|
|
40
|
+
switch stage {
|
|
41
|
+
case .empty:
|
|
42
|
+
Label("No avatar", systemImage: "person.crop.circle.fill")
|
|
43
|
+
.labelStyle(.iconOnly)
|
|
44
|
+
.font(.system(size: 96))
|
|
45
|
+
case .working:
|
|
45
46
|
ProgressView()
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.clipShape(Circle())
|
|
53
|
-
case .error(let message):
|
|
54
|
-
ContentUnavailableView(message, systemImage: "exclamationmark.triangle")
|
|
47
|
+
case .ready(let face):
|
|
48
|
+
face.resizable().aspectRatio(contentMode: .fill)
|
|
49
|
+
.frame(width: 120, height: 120).clipShape(.circle)
|
|
50
|
+
case .failed(let reason):
|
|
51
|
+
ContentUnavailableView("Could Not Load", systemImage: "exclamationmark.triangle",
|
|
52
|
+
description: Text(reason))
|
|
55
53
|
}
|
|
56
|
-
|
|
57
|
-
PhotosPicker("Choose Photo", selection: $selectedItem, matching: .images)
|
|
54
|
+
PhotosPicker("Change Avatar", selection: $pick, matching: .images)
|
|
58
55
|
.buttonStyle(.borderedProminent)
|
|
59
56
|
}
|
|
60
|
-
.onChange(of:
|
|
61
|
-
guard let
|
|
62
|
-
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
loadState = .loading
|
|
57
|
+
.onChange(of: pick) { _, chosen in
|
|
58
|
+
guard let chosen else { stage = .empty; return }
|
|
59
|
+
stage = .working
|
|
66
60
|
Task {
|
|
67
61
|
do {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
loadState = .error("Unable to load image")
|
|
62
|
+
guard let raw = try await chosen.loadTransferable(type: Data.self),
|
|
63
|
+
let decoded = UIImage(data: raw) else {
|
|
64
|
+
stage = .failed("That file is not an image this app can read.")
|
|
65
|
+
return
|
|
73
66
|
}
|
|
67
|
+
stage = .ready(Image(uiImage: decoded))
|
|
74
68
|
} catch {
|
|
75
|
-
|
|
69
|
+
stage = .failed(error.localizedDescription)
|
|
76
70
|
}
|
|
77
71
|
}
|
|
78
72
|
}
|
|
@@ -80,518 +74,378 @@ struct ProfilePhotoPicker: View {
|
|
|
80
74
|
}
|
|
81
75
|
```
|
|
82
76
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
77
|
+
A cleared selection goes back to `.empty`. A `nil` payload or an undecodable
|
|
78
|
+
one becomes a readable failure; a thrown error shows its
|
|
79
|
+
`localizedDescription`.
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
## 2. Many photos with progress
|
|
88
82
|
|
|
89
83
|
```swift
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
@State private var
|
|
95
|
-
@State private var loadedImages: [UIImage] = []
|
|
96
|
-
@State private var loadProgress: Double = 0
|
|
97
|
-
@State private var isLoading = false
|
|
84
|
+
struct AlbumImporter: View {
|
|
85
|
+
@State private var picks: [PhotosPickerItem] = []
|
|
86
|
+
@State private var imported: [Image] = []
|
|
87
|
+
@State private var fraction = 0.0
|
|
88
|
+
@State private var busy = false
|
|
98
89
|
|
|
99
90
|
var body: some View {
|
|
100
91
|
VStack {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.padding()
|
|
104
|
-
}
|
|
105
|
-
|
|
92
|
+
PhotosPicker("Import", selection: $picks, maxSelectionCount: 10, matching: .images)
|
|
93
|
+
if busy { ProgressView(value: fraction) }
|
|
106
94
|
ScrollView(.horizontal) {
|
|
107
95
|
LazyHStack {
|
|
108
|
-
ForEach(
|
|
109
|
-
|
|
110
|
-
.resizable()
|
|
111
|
-
.scaledToFill()
|
|
112
|
-
.frame(width: 100, height: 100)
|
|
113
|
-
.clipShape(.rect(cornerRadius: 8))
|
|
96
|
+
ForEach(imported.indices, id: \.self) { i in
|
|
97
|
+
imported[i].resizable().scaledToFill().frame(width: 108, height: 108).clipped()
|
|
114
98
|
}
|
|
115
99
|
}
|
|
116
|
-
.padding(.horizontal)
|
|
117
100
|
}
|
|
118
|
-
.frame(height:
|
|
119
|
-
|
|
120
|
-
PhotosPicker(
|
|
121
|
-
"Select Photos",
|
|
122
|
-
selection: $selectedItems,
|
|
123
|
-
maxSelectionCount: 10,
|
|
124
|
-
matching: .images
|
|
125
|
-
)
|
|
101
|
+
.frame(height: imported.isEmpty ? 0 : 116)
|
|
126
102
|
}
|
|
127
|
-
.onChange(of:
|
|
128
|
-
Task { await
|
|
103
|
+
.onChange(of: picks) { _, batch in
|
|
104
|
+
Task { await importSequentially(batch) }
|
|
129
105
|
}
|
|
130
106
|
}
|
|
131
107
|
|
|
132
|
-
private func
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let uiImage = UIImage(data: data) {
|
|
140
|
-
images.append(uiImage)
|
|
108
|
+
private func importSequentially(_ batch: [PhotosPickerItem]) async {
|
|
109
|
+
busy = true
|
|
110
|
+
var collected: [Image] = []
|
|
111
|
+
for (position, entry) in batch.enumerated() {
|
|
112
|
+
if let raw = try? await entry.loadTransferable(type: Data.self),
|
|
113
|
+
let decoded = UIImage(data: raw) {
|
|
114
|
+
collected.append(Image(uiImage: decoded))
|
|
141
115
|
}
|
|
142
|
-
|
|
116
|
+
fraction = Double(position + 1) / Double(batch.count)
|
|
143
117
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
isLoading = false
|
|
118
|
+
imported = collected
|
|
119
|
+
busy = false
|
|
147
120
|
}
|
|
148
121
|
}
|
|
149
122
|
```
|
|
150
123
|
|
|
151
|
-
Load
|
|
124
|
+
Load one item after another. Ten full-resolution decodes running at the same
|
|
125
|
+
time can push the app past its memory limit and get it terminated.
|
|
152
126
|
|
|
153
|
-
|
|
127
|
+
## 3. Video from the picker
|
|
154
128
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
Use a `Transferable` wrapper that writes video data to a temporary file, since videos are too large to hold in memory as `Data`.
|
|
129
|
+
Movies are too large to hold as `Data`. Import them as a file and copy that
|
|
130
|
+
file somewhere the app owns.
|
|
158
131
|
|
|
159
132
|
```swift
|
|
160
|
-
import SwiftUI
|
|
161
|
-
import PhotosUI
|
|
162
133
|
import AVKit
|
|
134
|
+
import CoreTransferable
|
|
163
135
|
|
|
164
|
-
struct
|
|
165
|
-
let
|
|
136
|
+
struct PickedClip: Transferable {
|
|
137
|
+
let location: URL
|
|
166
138
|
|
|
167
139
|
static var transferRepresentation: some TransferRepresentation {
|
|
168
|
-
FileRepresentation(contentType: .movie) {
|
|
169
|
-
SentTransferredFile(
|
|
170
|
-
} importing: {
|
|
171
|
-
|
|
172
|
-
let tempURL = FileManager.default.temporaryDirectory
|
|
140
|
+
FileRepresentation(contentType: .movie) { clip in
|
|
141
|
+
SentTransferredFile(clip.location)
|
|
142
|
+
} importing: { incoming in
|
|
143
|
+
let copy = URL.temporaryDirectory
|
|
173
144
|
.appendingPathComponent(UUID().uuidString)
|
|
174
145
|
.appendingPathExtension("mov")
|
|
175
|
-
try FileManager.default.copyItem(at:
|
|
176
|
-
return
|
|
146
|
+
try FileManager.default.copyItem(at: incoming.file, to: copy)
|
|
147
|
+
return PickedClip(location: copy)
|
|
177
148
|
}
|
|
178
149
|
}
|
|
179
150
|
}
|
|
180
151
|
|
|
181
|
-
struct
|
|
182
|
-
@State private var
|
|
183
|
-
@State private var
|
|
152
|
+
struct ClipPreview: View {
|
|
153
|
+
@State private var pick: PhotosPickerItem?
|
|
154
|
+
@State private var clipPlayer: AVPlayer?
|
|
184
155
|
|
|
185
156
|
var body: some View {
|
|
186
157
|
VStack {
|
|
187
|
-
if let
|
|
188
|
-
VideoPlayer(player:
|
|
189
|
-
.frame(height: 300)
|
|
158
|
+
if let clipPlayer {
|
|
159
|
+
VideoPlayer(player: clipPlayer).frame(height: 280)
|
|
190
160
|
}
|
|
191
|
-
|
|
192
|
-
PhotosPicker("Select Video", selection: $selectedItem, matching: .videos)
|
|
161
|
+
PhotosPicker("Pick Clip", selection: $pick, matching: .videos)
|
|
193
162
|
}
|
|
194
|
-
.onChange(of:
|
|
163
|
+
.onChange(of: pick) { _, chosen in
|
|
195
164
|
Task {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
}
|
|
165
|
+
guard let clip = try? await chosen?.loadTransferable(type: PickedClip.self) else { return }
|
|
166
|
+
clipPlayer = AVPlayer(url: clip.location)
|
|
199
167
|
}
|
|
200
168
|
}
|
|
201
169
|
}
|
|
202
170
|
}
|
|
203
171
|
```
|
|
204
172
|
|
|
205
|
-
|
|
173
|
+
The file handed to `importing:` is temporary and the system may remove it at
|
|
174
|
+
any time, so copy it before returning.
|
|
206
175
|
|
|
207
|
-
|
|
176
|
+
## 4. Live Photos
|
|
208
177
|
|
|
209
|
-
|
|
178
|
+
`PHLivePhoto` conforms to `Transferable` through PhotosUI starting with iOS 16,
|
|
179
|
+
so a Live Photo loads like any other type. Display it with `PHLivePhotoView`
|
|
180
|
+
wrapped for SwiftUI.
|
|
210
181
|
|
|
211
182
|
```swift
|
|
212
|
-
|
|
213
|
-
|
|
183
|
+
struct LiveMoment: UIViewRepresentable {
|
|
184
|
+
let moment: PHLivePhoto
|
|
214
185
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
186
|
+
func makeUIView(context: Context) -> PHLivePhotoView {
|
|
187
|
+
let view = PHLivePhotoView()
|
|
188
|
+
view.contentMode = .scaleAspectFit
|
|
189
|
+
return view
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func updateUIView(_ view: PHLivePhotoView, context: Context) {
|
|
193
|
+
view.livePhoto = moment
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
struct LiveMomentPicker: View {
|
|
198
|
+
@State private var pick: PhotosPickerItem?
|
|
199
|
+
@State private var moment: PHLivePhoto?
|
|
219
200
|
|
|
220
201
|
var body: some View {
|
|
221
202
|
VStack {
|
|
222
|
-
if let
|
|
223
|
-
|
|
224
|
-
.frame(height: 300)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
PhotosPicker(
|
|
228
|
-
"Select Live Photo",
|
|
229
|
-
selection: $selectedItem,
|
|
230
|
-
matching: .livePhotos
|
|
231
|
-
)
|
|
203
|
+
if let moment { LiveMoment(moment: moment).frame(height: 320) }
|
|
204
|
+
PhotosPicker("Pick Live Photo", selection: $pick, matching: .livePhotos)
|
|
232
205
|
}
|
|
233
|
-
.onChange(of:
|
|
234
|
-
Task {
|
|
235
|
-
livePhoto = try? await newItem?.loadTransferable(type: PHLivePhoto.self)
|
|
236
|
-
}
|
|
206
|
+
.onChange(of: pick) { _, chosen in
|
|
207
|
+
Task { moment = try? await chosen?.loadTransferable(type: PHLivePhoto.self) }
|
|
237
208
|
}
|
|
238
209
|
}
|
|
239
210
|
}
|
|
240
211
|
```
|
|
241
212
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
## 5. PHPickerViewController Wrapping (UIKit Interop)
|
|
213
|
+
## 5. PHPickerViewController inside SwiftUI
|
|
247
214
|
|
|
248
|
-
|
|
215
|
+
Reach for `PHPickerViewController` when the surrounding code is UIKit or needs
|
|
216
|
+
UIKit-level control. In pure SwiftUI, `PhotosPicker` is simpler.
|
|
249
217
|
|
|
250
218
|
```swift
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
struct PHPickerWrapper: UIViewControllerRepresentable {
|
|
255
|
-
@Binding var selectedImages: [UIImage]
|
|
256
|
-
var selectionLimit: Int = 0
|
|
257
|
-
@Environment(\.dismiss) private var dismiss
|
|
258
|
-
|
|
259
|
-
func makeCoordinator() -> Coordinator { Coordinator(self) }
|
|
219
|
+
struct LegacyGalleryPicker: UIViewControllerRepresentable {
|
|
220
|
+
@Binding var chosen: [UIImage]
|
|
221
|
+
var limit = 0
|
|
260
222
|
|
|
261
223
|
func makeUIViewController(context: Context) -> PHPickerViewController {
|
|
262
|
-
var
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
let picker = PHPickerViewController(configuration: config)
|
|
224
|
+
var setup = PHPickerConfiguration(photoLibrary: .shared())
|
|
225
|
+
setup.filter = .images
|
|
226
|
+
setup.selectionLimit = limit
|
|
227
|
+
setup.preferredAssetRepresentationMode = .current
|
|
228
|
+
let picker = PHPickerViewController(configuration: setup)
|
|
268
229
|
picker.delegate = context.coordinator
|
|
269
230
|
return picker
|
|
270
231
|
}
|
|
271
232
|
|
|
272
|
-
func updateUIViewController(_
|
|
273
|
-
// Configuration is immutable after creation
|
|
274
|
-
}
|
|
233
|
+
func updateUIViewController(_ controller: PHPickerViewController, context: Context) {}
|
|
275
234
|
|
|
276
|
-
|
|
277
|
-
let parent: PHPickerWrapper
|
|
235
|
+
func makeCoordinator() -> Coordinator { Coordinator(owner: self) }
|
|
278
236
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
_ picker: PHPickerViewController,
|
|
283
|
-
didFinishPicking results: [PHPickerResult]
|
|
284
|
-
) {
|
|
285
|
-
parent.dismiss()
|
|
286
|
-
guard !results.isEmpty else { return }
|
|
237
|
+
final class Coordinator: NSObject, PHPickerViewControllerDelegate {
|
|
238
|
+
let owner: LegacyGalleryPicker
|
|
239
|
+
init(owner: LegacyGalleryPicker) { self.owner = owner }
|
|
287
240
|
|
|
241
|
+
func picker(_ sheet: PHPickerViewController, didFinishPicking picks: [PHPickerResult]) {
|
|
242
|
+
sheet.dismiss(animated: true)
|
|
243
|
+
let providers = picks.map(\.itemProvider)
|
|
288
244
|
Task { @MainActor in
|
|
289
245
|
var images: [UIImage] = []
|
|
290
|
-
for
|
|
291
|
-
|
|
292
|
-
|
|
246
|
+
for provider in providers where provider.canLoadObject(ofClass: UIImage.self) {
|
|
247
|
+
let image: UIImage? = await withCheckedContinuation { done in
|
|
248
|
+
provider.loadObject(ofClass: UIImage.self) { object, _ in
|
|
249
|
+
done.resume(returning: object as? UIImage)
|
|
250
|
+
}
|
|
293
251
|
}
|
|
252
|
+
if let image { images.append(image) }
|
|
294
253
|
}
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private func loadImage(from provider: NSItemProvider) async -> UIImage? {
|
|
300
|
-
await withCheckedContinuation { continuation in
|
|
301
|
-
if provider.canLoadObject(ofClass: UIImage.self) {
|
|
302
|
-
provider.loadObject(ofClass: UIImage.self) { image, _ in
|
|
303
|
-
continuation.resume(returning: image as? UIImage)
|
|
304
|
-
}
|
|
305
|
-
} else {
|
|
306
|
-
continuation.resume(returning: nil)
|
|
307
|
-
}
|
|
254
|
+
owner.chosen = images
|
|
308
255
|
}
|
|
309
256
|
}
|
|
310
257
|
}
|
|
311
258
|
}
|
|
312
259
|
```
|
|
313
260
|
|
|
314
|
-
|
|
261
|
+
- A `selectionLimit` of 0 means the system maximum.
|
|
262
|
+
- The configuration is fixed once the controller exists, which is why
|
|
263
|
+
`updateUIViewController` does nothing.
|
|
264
|
+
- `picker(_:didFinishPicking:)` fires for Cancel too, with an empty array.
|
|
265
|
+
Dismiss the picker there in every case.
|
|
315
266
|
|
|
316
|
-
|
|
267
|
+
## 6. Saving into the library
|
|
317
268
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
### Using PHPhotoLibrary (Preferred)
|
|
269
|
+
Saving needs `NSPhotoLibraryAddUsageDescription`. Use `PHPhotoLibrary` when you
|
|
270
|
+
want control over the stored data and metadata;
|
|
271
|
+
`UIImageWriteToSavedPhotosAlbum` is enough for a quick one-off save.
|
|
323
272
|
|
|
324
273
|
```swift
|
|
325
274
|
import Photos
|
|
326
275
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
let
|
|
330
|
-
|
|
276
|
+
enum LibraryWriter {
|
|
277
|
+
static func store(_ image: UIImage) async throws {
|
|
278
|
+
guard let encoded = image.jpegData(compressionQuality: 0.9) else { return }
|
|
279
|
+
try await store(encoded)
|
|
331
280
|
}
|
|
332
|
-
}
|
|
333
281
|
|
|
334
|
-
func
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
282
|
+
static func store(_ encoded: Data) async throws {
|
|
283
|
+
try await PHPhotoLibrary.shared().performChanges {
|
|
284
|
+
let request = PHAssetCreationRequest.forAsset()
|
|
285
|
+
request.addResource(with: .photo, data: encoded, options: PHAssetResourceCreationOptions())
|
|
286
|
+
}
|
|
339
287
|
}
|
|
340
288
|
}
|
|
341
289
|
```
|
|
342
290
|
|
|
343
|
-
###
|
|
344
|
-
|
|
345
|
-
```swift
|
|
346
|
-
import Photos
|
|
347
|
-
|
|
348
|
-
func saveToAlbum(image: UIImage, albumName: String) async throws {
|
|
349
|
-
let album = try await findOrCreateAlbum(named: albumName)
|
|
291
|
+
### Into a named album
|
|
350
292
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
with: .photo,
|
|
355
|
-
data: image.jpegData(compressionQuality: 0.9)!,
|
|
356
|
-
options: nil
|
|
357
|
-
)
|
|
293
|
+
Look the album up by title; if it is missing, create it and capture its
|
|
294
|
+
placeholder, then add the new asset in the same change block. The fallback is an
|
|
295
|
+
`if`, not `??`: the right side of `??` is an autoclosure and cannot `await`.
|
|
358
296
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
private func findOrCreateAlbum(named name: String) async throws -> PHAssetCollection {
|
|
366
|
-
let fetchOptions = PHFetchOptions()
|
|
367
|
-
fetchOptions.predicate = NSPredicate(format: "title = %@", name)
|
|
368
|
-
let result = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
|
|
297
|
+
```swift
|
|
298
|
+
enum AlbumWriter {
|
|
299
|
+
enum Failure: Error { case albumMissing }
|
|
369
300
|
|
|
370
|
-
|
|
371
|
-
|
|
301
|
+
static func album(titled title: String) -> PHAssetCollection? {
|
|
302
|
+
let query = PHFetchOptions()
|
|
303
|
+
query.predicate = NSPredicate(format: "title = %@", title)
|
|
304
|
+
return PHAssetCollection.fetchAssetCollections(with: PHAssetCollectionType.album, subtype: .any, options: query).firstObject
|
|
372
305
|
}
|
|
373
306
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
307
|
+
static func makeAlbum(titled title: String) async throws -> PHAssetCollection {
|
|
308
|
+
var marker: PHObjectPlaceholder?
|
|
309
|
+
try await PHPhotoLibrary.shared().performChanges {
|
|
310
|
+
let create = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: title)
|
|
311
|
+
marker = create.placeholderForCreatedAssetCollection
|
|
312
|
+
}
|
|
313
|
+
guard let id = marker?.localIdentifier,
|
|
314
|
+
let made = PHAssetCollection.fetchAssetCollections(withLocalIdentifiers: [id], options: nil).firstObject
|
|
315
|
+
else { throw Failure.albumMissing }
|
|
316
|
+
return made
|
|
379
317
|
}
|
|
380
318
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
319
|
+
static func save(_ encoded: Data, into title: String) async throws {
|
|
320
|
+
let target: PHAssetCollection
|
|
321
|
+
if let existing = album(titled: title) {
|
|
322
|
+
target = existing
|
|
323
|
+
} else {
|
|
324
|
+
target = try await makeAlbum(titled: title)
|
|
325
|
+
}
|
|
326
|
+
try await PHPhotoLibrary.shared().performChanges {
|
|
327
|
+
let request = PHAssetCreationRequest.forAsset()
|
|
328
|
+
request.addResource(with: .photo, data: encoded, options: nil)
|
|
329
|
+
guard let newAsset = request.placeholderForCreatedAsset,
|
|
330
|
+
let edit = PHAssetCollectionChangeRequest(for: target) else { return }
|
|
331
|
+
edit.addAssets([newAsset] as NSArray)
|
|
332
|
+
}
|
|
387
333
|
}
|
|
388
|
-
|
|
389
|
-
return collection
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
enum PhotoLibraryError: Error {
|
|
393
|
-
case albumCreationFailed
|
|
394
334
|
}
|
|
395
335
|
```
|
|
396
336
|
|
|
397
|
-
|
|
337
|
+
## 7. Thumbnails with ImageIO
|
|
398
338
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
339
|
+
Build thumbnails without decoding the whole image; with a grid of hundreds of
|
|
340
|
+
photos this matters more than anything else. `UIImage(data:)` followed by a
|
|
341
|
+
resize still decodes the full bitmap first, so prefer downsampling. When the
|
|
342
|
+
image is on disk, open it by URL with caching switched off so the file is not
|
|
343
|
+
read into memory up front:
|
|
402
344
|
|
|
403
345
|
```swift
|
|
404
346
|
import ImageIO
|
|
405
|
-
import UIKit
|
|
406
|
-
|
|
407
|
-
/// Downsample image data to a target display size.
|
|
408
|
-
/// Use this instead of UIImage(data:) followed by resizing, which decodes the full image.
|
|
409
|
-
func downsample(
|
|
410
|
-
data: Data,
|
|
411
|
-
to pointSize: CGSize,
|
|
412
|
-
scale: CGFloat = UITraitCollection.current.displayScale
|
|
413
|
-
) -> UIImage? {
|
|
414
|
-
let maxDimension = max(pointSize.width, pointSize.height) * scale
|
|
415
|
-
|
|
416
|
-
let options: [CFString: Any] = [
|
|
417
|
-
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
418
|
-
kCGImageSourceShouldCacheImmediately: true,
|
|
419
|
-
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
420
|
-
kCGImageSourceThumbnailMaxPixelSize: maxDimension
|
|
421
|
-
]
|
|
422
347
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
return UIImage(cgImage: cgImage)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/// Downsample from a file URL (avoids loading full data into memory).
|
|
432
|
-
func downsample(
|
|
433
|
-
url: URL,
|
|
434
|
-
to pointSize: CGSize,
|
|
435
|
-
scale: CGFloat = UITraitCollection.current.displayScale
|
|
436
|
-
) -> UIImage? {
|
|
437
|
-
let maxDimension = max(pointSize.width, pointSize.height) * scale
|
|
438
|
-
|
|
439
|
-
let sourceOptions: [CFString: Any] = [kCGImageSourceShouldCache: false]
|
|
440
|
-
guard let source = CGImageSourceCreateWithURL(url as CFURL, sourceOptions as CFDictionary) else {
|
|
441
|
-
return nil
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
let downsampleOptions: [CFString: Any] = [
|
|
348
|
+
func fileThumbnail(at file: URL, longestPixelEdge: CGFloat) -> UIImage? {
|
|
349
|
+
let openOptions = [kCGImageSourceShouldCache: false] as CFDictionary
|
|
350
|
+
guard let reader = CGImageSourceCreateWithURL(file as CFURL, openOptions) else { return nil }
|
|
351
|
+
let shrink: [CFString: Any] = [
|
|
445
352
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
446
|
-
kCGImageSourceShouldCacheImmediately: true,
|
|
447
353
|
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
448
|
-
|
|
354
|
+
kCGImageSourceShouldCacheImmediately: true,
|
|
355
|
+
kCGImageSourceThumbnailMaxPixelSize: longestPixelEdge,
|
|
449
356
|
]
|
|
450
|
-
|
|
451
|
-
guard let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary) else {
|
|
452
|
-
return nil
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return UIImage(cgImage: cgImage)
|
|
357
|
+
return CGImageSourceCreateThumbnailAtIndex(reader, 0, shrink as CFDictionary).map(UIImage.init(cgImage:))
|
|
456
358
|
}
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Usage in a Photo Grid
|
|
460
359
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
@State private var thumbnail: UIImage?
|
|
360
|
+
struct GridCell: View {
|
|
361
|
+
let file: URL
|
|
362
|
+
@Environment(\.displayScale) private var scale
|
|
363
|
+
@State private var small: UIImage?
|
|
467
364
|
|
|
468
365
|
var body: some View {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
.scaledToFill()
|
|
474
|
-
} else {
|
|
475
|
-
Rectangle()
|
|
476
|
-
.fill(.quaternary)
|
|
477
|
-
.overlay(ProgressView())
|
|
366
|
+
Rectangle()
|
|
367
|
+
.fill(.quaternary)
|
|
368
|
+
.overlay {
|
|
369
|
+
if let small { Image(uiImage: small).resizable().scaledToFill() } else { ProgressView() }
|
|
478
370
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
.task {
|
|
483
|
-
thumbnail = downsample(data: imageData, to: thumbnailSize)
|
|
484
|
-
}
|
|
371
|
+
.frame(width: 100, height: 100)
|
|
372
|
+
.clipShape(.rect(cornerRadius: 6))
|
|
373
|
+
.task { small = fileThumbnail(at: file, longestPixelEdge: 100 * scale) }
|
|
485
374
|
}
|
|
486
375
|
}
|
|
487
376
|
```
|
|
488
377
|
|
|
489
|
-
|
|
378
|
+
## 8. HEIC and HEIF
|
|
490
379
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
Modern iPhones capture photos in HEIC format by default. Handle both HEIC and JPEG transparently.
|
|
494
|
-
|
|
495
|
-
### Checking Image Format
|
|
380
|
+
Recent iPhones capture HEIC unless the user changed the setting, so code must
|
|
381
|
+
accept HEIC and JPEG alike.
|
|
496
382
|
|
|
497
383
|
```swift
|
|
498
384
|
import UniformTypeIdentifiers
|
|
499
385
|
|
|
500
|
-
func
|
|
501
|
-
guard let
|
|
502
|
-
let
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
return UTType(uti)
|
|
386
|
+
func detectedType(of bytes: Data) -> UTType? {
|
|
387
|
+
guard let reader = CGImageSourceCreateWithData(bytes as CFData, nil),
|
|
388
|
+
let identifier = CGImageSourceGetType(reader) else { return nil }
|
|
389
|
+
return UTType(identifier as String)
|
|
506
390
|
}
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
### Converting HEIC to JPEG
|
|
510
|
-
|
|
511
|
-
```swift
|
|
512
|
-
import UIKit
|
|
513
391
|
|
|
514
|
-
func
|
|
515
|
-
|
|
516
|
-
return image.jpegData(compressionQuality: compressionQuality)
|
|
392
|
+
func jpegCopy(of heic: Data, quality: CGFloat = 0.9) -> Data? {
|
|
393
|
+
UIImage(data: heic)?.jpegData(compressionQuality: quality)
|
|
517
394
|
}
|
|
518
395
|
```
|
|
519
396
|
|
|
520
|
-
|
|
397
|
+
- Keep HEIC when saving or uploading: better quality per byte. Convert only
|
|
398
|
+
when the receiver demands JPEG, such as a server that rejects HEIC.
|
|
399
|
+
- On `PHPickerConfiguration`, `preferredAssetRepresentationMode = .current`
|
|
400
|
+
avoids transcoding and hands over the HEIC as stored. `.compatible` asks for
|
|
401
|
+
the most compatible representation and transcodes when needed, which for
|
|
402
|
+
HEIC photos usually means JPEG.
|
|
521
403
|
|
|
522
|
-
|
|
404
|
+
## 9. Picker appearance
|
|
523
405
|
|
|
524
|
-
|
|
525
|
-
import PhotosUI
|
|
526
|
-
|
|
527
|
-
// Request the current representation to avoid transcoding
|
|
528
|
-
var config = PHPickerConfiguration(photoLibrary: .shared())
|
|
529
|
-
config.preferredAssetRepresentationMode = .current // Keeps HEIC as-is
|
|
530
|
-
// vs .compatible which transcodes to JPEG
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
---
|
|
534
|
-
|
|
535
|
-
## 9. Custom PhotosPicker Appearance
|
|
536
|
-
|
|
537
|
-
Customize the picker trigger with any SwiftUI view using the label closure:
|
|
406
|
+
Any SwiftUI view can be the picker's label:
|
|
538
407
|
|
|
539
408
|
```swift
|
|
540
|
-
PhotosPicker(selection: $
|
|
541
|
-
Label("
|
|
542
|
-
.font(.headline)
|
|
409
|
+
PhotosPicker(selection: $picks, maxSelectionCount: 4, matching: .images) {
|
|
410
|
+
Label("Attach", systemImage: "photo.on.rectangle.angled")
|
|
543
411
|
.padding()
|
|
544
|
-
.background(.ultraThinMaterial, in:
|
|
412
|
+
.background(.ultraThinMaterial, in: .rect(cornerRadius: 12))
|
|
545
413
|
}
|
|
546
414
|
```
|
|
547
415
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
Display the picker inline rather than as a sheet:
|
|
416
|
+
iOS 17 adds presentation control:
|
|
551
417
|
|
|
552
418
|
```swift
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
)
|
|
559
|
-
.photosPickerStyle(.inline)
|
|
560
|
-
.frame(height: 300)
|
|
419
|
+
PhotosPicker("Pick", selection: $picks, selectionBehavior: .continuous, matching: .images)
|
|
420
|
+
.photosPickerStyle(.inline)
|
|
421
|
+
.photosPickerAccessoryVisibility(.hidden, edges: .bottom)
|
|
422
|
+
.photosPickerDisabledCapabilities([.search, .collectionNavigation])
|
|
423
|
+
.frame(height: 300)
|
|
561
424
|
```
|
|
562
425
|
|
|
563
|
-
|
|
426
|
+
- `.photosPickerStyle(.inline)` embeds the picker in the layout instead of a
|
|
427
|
+
sheet; `.compact` gives a single row. Give an inline picker a fixed height.
|
|
428
|
+
- `.continuous` selection reports changes as the person taps, which suits an
|
|
429
|
+
embedded picker.
|
|
430
|
+
- Limited library access is not a picker modifier. With `.limited` status, let
|
|
431
|
+
the person revise the selection through
|
|
432
|
+
`PHPhotoLibrary.shared().presentLimitedLibraryPicker(from:)`, and set
|
|
433
|
+
`PHPhotoLibraryPreventAutomaticLimitedAccessAlert` in Info.plist if the app
|
|
434
|
+
offers its own entry point for that.
|
|
564
435
|
|
|
565
|
-
|
|
436
|
+
## 10. Cropping
|
|
566
437
|
|
|
567
|
-
|
|
568
|
-
@available(iOS 17.0, *)
|
|
569
|
-
PhotosPicker(selection: $selectedItems, matching: .images)
|
|
570
|
-
.photosPickerAccessBehavior(.limited) // Only show user-approved photos
|
|
571
|
-
// .automatic (default) -- system decides
|
|
572
|
-
// .limited -- only previously approved photos
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
---
|
|
576
|
-
|
|
577
|
-
## 10. Image Cropping Pattern
|
|
578
|
-
|
|
579
|
-
A basic square crop using Core Graphics after selecting a photo:
|
|
438
|
+
A centered square crop needs only Core Graphics:
|
|
580
439
|
|
|
581
440
|
```swift
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
let x = (cgImage.width - side) / 2
|
|
589
|
-
let y = (cgImage.height - side) / 2
|
|
590
|
-
let cropRect = CGRect(x: x, y: y, width: side, height: side)
|
|
591
|
-
|
|
592
|
-
guard let cropped = cgImage.cropping(to: cropRect) else { return nil }
|
|
593
|
-
return UIImage(cgImage: cropped, scale: image.scale, orientation: image.imageOrientation)
|
|
441
|
+
func centerSquare(of photo: UIImage) -> UIImage? {
|
|
442
|
+
guard let bitmap = photo.cgImage else { return nil }
|
|
443
|
+
let side = min(bitmap.width, bitmap.height)
|
|
444
|
+
let frame = CGRect(x: (bitmap.width - side) / 2, y: (bitmap.height - side) / 2, width: side, height: side)
|
|
445
|
+
guard let square = bitmap.cropping(to: frame) else { return nil }
|
|
446
|
+
return UIImage(cgImage: square, scale: photo.scale, orientation: photo.imageOrientation)
|
|
594
447
|
}
|
|
595
448
|
```
|
|
596
449
|
|
|
597
|
-
|
|
450
|
+
The system offers no crop controller. Interactive cropping means wrapping a
|
|
451
|
+
third-party view or building a drag-and-pinch overlay in SwiftUI yourself.
|