@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,95 +1,84 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: photos-camera-media
|
|
3
|
-
description: "
|
|
3
|
+
description: "PhotoKit and AVFoundation media on iOS: PhotosPicker and PHPickerViewController, Transferable loading of images, movies and Live Photos, photo library and camera permissions, saving to the library, AVCaptureSession photo capture and video recording, image loading, downsampling, caching and display, AVPlayer playback. Use when implementing, reviewing or improving photo picking, camera capture, video recording, image processing or media permissions in Swift."
|
|
4
4
|
metadata:
|
|
5
|
-
source:
|
|
5
|
+
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
7
|
-
# PhotoKit
|
|
8
|
-
|
|
9
|
-
Modern patterns for photo picking, camera capture, image loading, and media permissions targeting iOS 26+ with Swift 6.3. Patterns are backward-compatible to iOS 16 unless noted. See [references/photokit-patterns.md](references/photokit-patterns.md) for complete picker recipes and [references/camera-capture.md](references/camera-capture.md) for AVCaptureSession patterns.
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
# PhotoKit
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- [Review Checklist](#review-checklist)
|
|
19
|
-
- [References](#references)
|
|
10
|
+
Written for iOS 26 and Swift 6.3. The samples need iOS 17 or later: they use
|
|
11
|
+
the two-parameter `onChange(of:)`, `@Observable` and `ContentUnavailableView`.
|
|
12
|
+
Where the framework API itself is older, the heading says so. Longer
|
|
13
|
+
recipes live in two companion files: [picker recipes](references/photokit-patterns.md) and
|
|
14
|
+
[capture session recipes](references/camera-capture.md).
|
|
20
15
|
|
|
21
|
-
## PhotosPicker (SwiftUI, iOS 16+)
|
|
16
|
+
## Picking with PhotosPicker (SwiftUI, iOS 16+)
|
|
22
17
|
|
|
23
|
-
`PhotosPicker`
|
|
18
|
+
`PhotosPicker` replaces `UIImagePickerController` for choosing library items in
|
|
19
|
+
SwiftUI. The picker UI runs in a separate process, so the app can browse
|
|
20
|
+
without any photo library permission and only ever receives what the person
|
|
21
|
+
selected. It handles one item or many and can filter by media type.
|
|
24
22
|
|
|
25
|
-
###
|
|
23
|
+
### One item
|
|
26
24
|
|
|
27
25
|
```swift
|
|
28
26
|
import SwiftUI
|
|
29
27
|
import PhotosUI
|
|
30
28
|
|
|
31
|
-
struct
|
|
32
|
-
@State private var
|
|
33
|
-
@State private var
|
|
29
|
+
struct CoverPhotoField: View {
|
|
30
|
+
@State private var pick: PhotosPickerItem?
|
|
31
|
+
@State private var cover: Image?
|
|
34
32
|
|
|
35
33
|
var body: some View {
|
|
36
34
|
VStack {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
PhotosPicker("Select Photo", selection: $selectedItem, matching: .images)
|
|
35
|
+
cover?
|
|
36
|
+
.resizable()
|
|
37
|
+
.scaledToFit()
|
|
38
|
+
.frame(maxHeight: 300)
|
|
39
|
+
PhotosPicker("Choose Cover", selection: $pick, matching: .images)
|
|
45
40
|
}
|
|
46
|
-
.onChange(of:
|
|
41
|
+
.onChange(of: pick) { _, item in
|
|
47
42
|
Task {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
guard let chosen = item,
|
|
44
|
+
let bytes = try? await chosen.loadTransferable(type: Data.self),
|
|
45
|
+
let decoded = UIImage(data: bytes) else { return }
|
|
46
|
+
cover = Image(uiImage: decoded)
|
|
52
47
|
}
|
|
53
48
|
}
|
|
54
49
|
}
|
|
55
50
|
}
|
|
56
51
|
```
|
|
57
52
|
|
|
58
|
-
###
|
|
53
|
+
### Several items
|
|
54
|
+
|
|
55
|
+
Bind `selection` to an array and cap it with `maxSelectionCount`:
|
|
59
56
|
|
|
60
57
|
```swift
|
|
61
|
-
struct
|
|
62
|
-
@State private var
|
|
63
|
-
@State private var
|
|
58
|
+
struct MoodBoardPicker: View {
|
|
59
|
+
@State private var picks: [PhotosPickerItem] = []
|
|
60
|
+
@State private var tiles: [Image] = []
|
|
64
61
|
|
|
65
62
|
var body: some View {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
63
|
+
PhotosPicker("Add Photos", selection: $picks, maxSelectionCount: 5, matching: .images)
|
|
64
|
+
ScrollView(.horizontal) {
|
|
65
|
+
HStack {
|
|
66
|
+
ForEach(tiles.indices, id: \.self) { index in
|
|
67
|
+
tiles[index]
|
|
68
|
+
.resizable()
|
|
69
|
+
.scaledToFill()
|
|
70
|
+
.frame(width: 100, height: 100)
|
|
71
|
+
.clipShape(.rect(cornerRadius: 8))
|
|
76
72
|
}
|
|
77
73
|
}
|
|
78
|
-
|
|
79
|
-
PhotosPicker(
|
|
80
|
-
"Select Photos",
|
|
81
|
-
selection: $selectedItems,
|
|
82
|
-
maxSelectionCount: 5,
|
|
83
|
-
matching: .images
|
|
84
|
-
)
|
|
85
74
|
}
|
|
86
|
-
.onChange(of:
|
|
75
|
+
.onChange(of: picks) { _, items in
|
|
87
76
|
Task {
|
|
88
|
-
|
|
89
|
-
for
|
|
90
|
-
if let
|
|
91
|
-
let
|
|
92
|
-
|
|
77
|
+
tiles.removeAll()
|
|
78
|
+
for entry in items {
|
|
79
|
+
if let bytes = try? await entry.loadTransferable(type: Data.self),
|
|
80
|
+
let decoded = UIImage(data: bytes) {
|
|
81
|
+
tiles.append(Image(uiImage: decoded))
|
|
93
82
|
}
|
|
94
83
|
}
|
|
95
84
|
}
|
|
@@ -98,136 +87,125 @@ struct MultiPhotoPicker: View {
|
|
|
98
87
|
}
|
|
99
88
|
```
|
|
100
89
|
|
|
101
|
-
###
|
|
90
|
+
### Filtering by media type
|
|
102
91
|
|
|
103
|
-
|
|
92
|
+
`matching:` takes a `PHPickerFilter`:
|
|
104
93
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
PhotosPicker(selection: $items, matching: .livePhotos)
|
|
114
|
-
|
|
115
|
-
// Screenshots only
|
|
116
|
-
PhotosPicker(selection: $items, matching: .screenshots)
|
|
117
|
-
|
|
118
|
-
// Images and videos combined
|
|
119
|
-
PhotosPicker(selection: $items, matching: .any(of: [.images, .videos]))
|
|
94
|
+
| Filter | Shows |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `.images` | still images only |
|
|
97
|
+
| `.videos` | videos only |
|
|
98
|
+
| `.livePhotos` | Live Photos only |
|
|
99
|
+
| `.screenshots` | screenshots only |
|
|
100
|
+
| `.any(of: [.images, .videos])` | either kind (logical OR) |
|
|
101
|
+
| `.all(of: [.images, .not(.screenshots)])` | images that are not screenshots (AND plus negation) |
|
|
120
102
|
|
|
121
|
-
|
|
122
|
-
PhotosPicker(selection: $items, matching: .all(of: [.images, .not(.screenshots)]))
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Loading Selected Items with Transferable
|
|
103
|
+
### Decoding through Transferable
|
|
126
104
|
|
|
127
|
-
`PhotosPickerItem`
|
|
105
|
+
A `PhotosPickerItem` hands over its content asynchronously via
|
|
106
|
+
`loadTransferable(type:)`. A custom `Transferable` type moves the decode step
|
|
107
|
+
into the type itself:
|
|
128
108
|
|
|
129
109
|
```swift
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
110
|
+
import CoreTransferable
|
|
111
|
+
import UniformTypeIdentifiers
|
|
112
|
+
|
|
113
|
+
struct ScannedPage: Transferable {
|
|
114
|
+
enum DecodeFailure: Error { case notAnImage }
|
|
115
|
+
let bytes: Data
|
|
116
|
+
let preview: Image
|
|
133
117
|
|
|
134
118
|
static var transferRepresentation: some TransferRepresentation {
|
|
135
|
-
DataRepresentation(importedContentType: .image) {
|
|
136
|
-
guard let
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
return PickedImage(data: data, image: Image(uiImage: uiImage))
|
|
119
|
+
DataRepresentation(importedContentType: .image) { bytes in
|
|
120
|
+
guard let decoded = UIImage(data: bytes) else { throw DecodeFailure.notAnImage }
|
|
121
|
+
return ScannedPage(bytes: bytes, preview: Image(uiImage: decoded))
|
|
140
122
|
}
|
|
141
123
|
}
|
|
142
124
|
}
|
|
143
125
|
|
|
144
|
-
|
|
145
|
-
case importFailed
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Usage
|
|
149
|
-
if let picked = try? await item.loadTransferable(type: PickedImage.self) {
|
|
150
|
-
selectedImage = picked.image
|
|
151
|
-
}
|
|
126
|
+
// let page = try? await item.loadTransferable(type: ScannedPage.self)
|
|
152
127
|
```
|
|
153
128
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
129
|
+
- Run the load inside a `Task`; never wait for it on the main thread.
|
|
130
|
+
- Expect both a `nil` result and a thrown error. People can pick formats that
|
|
131
|
+
fail to decode.
|
|
157
132
|
|
|
158
|
-
|
|
133
|
+
## Privacy and permissions
|
|
159
134
|
|
|
160
|
-
|
|
135
|
+
### Library access levels
|
|
161
136
|
|
|
162
|
-
|
|
|
163
|
-
|
|
164
|
-
| Add-only |
|
|
165
|
-
| Read-write |
|
|
137
|
+
| Level | What the app may do | Info.plist key |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| Add-only | write new items, never read | `NSPhotoLibraryAddUsageDescription` |
|
|
140
|
+
| Read-write | read everything or a limited subset, and write | `NSPhotoLibraryUsageDescription` |
|
|
166
141
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
142
|
+
Asking for `.readWrite` lets the system offer its limited-library picker, where
|
|
143
|
+
the person decides which photos the app sees. `PhotosPicker` needs no grant at
|
|
144
|
+
all and exposes only the chosen items. Ask for library access only when the app
|
|
145
|
+
reads the whole library (a custom gallery, for example) or saves into it.
|
|
170
146
|
|
|
171
147
|
```swift
|
|
172
148
|
import Photos
|
|
173
149
|
|
|
174
|
-
func
|
|
175
|
-
let
|
|
150
|
+
func libraryAccess() async -> PHAuthorizationStatus {
|
|
151
|
+
let current = PHPhotoLibrary.authorizationStatus(for: .readWrite)
|
|
152
|
+
guard current == .notDetermined else { return current }
|
|
153
|
+
let answer = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
|
|
154
|
+
return answer
|
|
155
|
+
}
|
|
176
156
|
|
|
157
|
+
func canBrowseGallery(_ status: PHAuthorizationStatus) -> Bool {
|
|
177
158
|
switch status {
|
|
178
|
-
case .
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return status
|
|
182
|
-
case .denied, .restricted:
|
|
183
|
-
return status
|
|
184
|
-
@unknown default:
|
|
185
|
-
return status
|
|
159
|
+
case .authorized, .limited: true
|
|
160
|
+
case .denied, .restricted, .notDetermined: false
|
|
161
|
+
@unknown default: false
|
|
186
162
|
}
|
|
187
163
|
}
|
|
188
164
|
```
|
|
189
165
|
|
|
190
|
-
### Camera
|
|
166
|
+
### Camera access
|
|
191
167
|
|
|
192
|
-
|
|
168
|
+
The camera needs `NSCameraUsageDescription`. Check, and request if needed,
|
|
169
|
+
before any capture session is configured:
|
|
193
170
|
|
|
194
171
|
```swift
|
|
195
172
|
import AVFoundation
|
|
196
173
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
174
|
+
enum CameraAccess {
|
|
175
|
+
static func isGranted() async -> Bool {
|
|
176
|
+
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
177
|
+
case .authorized:
|
|
178
|
+
return true
|
|
179
|
+
case .notDetermined:
|
|
180
|
+
let allowed = await AVCaptureDevice.requestAccess(for: .video)
|
|
181
|
+
return allowed
|
|
182
|
+
case .denied, .restricted:
|
|
183
|
+
return false
|
|
184
|
+
@unknown default:
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
209
187
|
}
|
|
210
188
|
}
|
|
211
189
|
```
|
|
212
190
|
|
|
213
|
-
###
|
|
191
|
+
### When the answer is no
|
|
214
192
|
|
|
215
|
-
|
|
193
|
+
Send the person to Settings. Do not keep re-prompting and do not quietly drop
|
|
194
|
+
the feature.
|
|
216
195
|
|
|
217
196
|
```swift
|
|
218
|
-
struct
|
|
219
|
-
let message: String
|
|
197
|
+
struct CameraBlockedView: View {
|
|
220
198
|
@Environment(\.openURL) private var openURL
|
|
221
199
|
|
|
222
200
|
var body: some View {
|
|
223
201
|
ContentUnavailableView {
|
|
224
|
-
Label("
|
|
202
|
+
Label("Camera Is Off", systemImage: "lock.shield")
|
|
225
203
|
} description: {
|
|
226
|
-
Text(
|
|
204
|
+
Text("Allow camera access in Settings to scan receipts.")
|
|
227
205
|
} actions: {
|
|
228
206
|
Button("Open Settings") {
|
|
229
|
-
if let
|
|
230
|
-
openURL(
|
|
207
|
+
if let target = URL(string: UIApplication.openSettingsURLString) {
|
|
208
|
+
openURL(target)
|
|
231
209
|
}
|
|
232
210
|
}
|
|
233
211
|
}
|
|
@@ -235,266 +213,239 @@ struct PermissionDeniedView: View {
|
|
|
235
213
|
}
|
|
236
214
|
```
|
|
237
215
|
|
|
238
|
-
###
|
|
239
|
-
|
|
240
|
-
| Key | When Required |
|
|
241
|
-
|-----|--------------|
|
|
242
|
-
| `NSPhotoLibraryUsageDescription` | Reading photos from the library |
|
|
243
|
-
| `NSPhotoLibraryAddUsageDescription` | Saving photos/videos to the library |
|
|
244
|
-
| `NSCameraUsageDescription` | Accessing the camera |
|
|
245
|
-
| `NSMicrophoneUsageDescription` | Recording audio (video with sound) |
|
|
216
|
+
### Usage strings
|
|
246
217
|
|
|
247
|
-
|
|
218
|
+
| Key | Needed for |
|
|
219
|
+
|---|---|
|
|
220
|
+
| `NSPhotoLibraryUsageDescription` | reading existing library assets |
|
|
221
|
+
| `NSPhotoLibraryAddUsageDescription` | saving photos or videos into the library |
|
|
222
|
+
| `NSCameraUsageDescription` | any camera use |
|
|
223
|
+
| `NSMicrophoneUsageDescription` | recording audio, including video with sound |
|
|
248
224
|
|
|
249
|
-
|
|
225
|
+
A missing key is not a warning: the app crashes at the moment the system would
|
|
226
|
+
have shown the prompt.
|
|
250
227
|
|
|
251
|
-
|
|
228
|
+
## Camera capture basics
|
|
252
229
|
|
|
253
|
-
|
|
230
|
+
Put the `AVCaptureSession` in its own `@Observable` model. The representable
|
|
231
|
+
view only draws the preview. The session itself is not `Sendable`, and
|
|
232
|
+
`startRunning()` / `stopRunning()` are synchronous calls that block until the
|
|
233
|
+
hardware settles, so all session work runs on one private serial queue that
|
|
234
|
+
the model calls into.
|
|
254
235
|
|
|
255
236
|
```swift
|
|
256
237
|
import AVFoundation
|
|
238
|
+
import Observation
|
|
257
239
|
|
|
258
|
-
@
|
|
259
|
-
@Observable
|
|
260
|
-
@MainActor
|
|
261
|
-
final class CameraManager {
|
|
240
|
+
final class CaptureEngine: @unchecked Sendable {
|
|
262
241
|
let session = AVCaptureSession()
|
|
263
|
-
private let
|
|
264
|
-
private
|
|
265
|
-
|
|
266
|
-
var isRunning = false
|
|
267
|
-
var capturedImage: Data?
|
|
242
|
+
private let stills = AVCapturePhotoOutput()
|
|
243
|
+
private let queue = DispatchQueue(label: "capture.engine")
|
|
268
244
|
|
|
269
|
-
func
|
|
270
|
-
|
|
245
|
+
func run<T: Sendable>(_ work: @escaping @Sendable () -> T) async -> T {
|
|
246
|
+
await withCheckedContinuation { done in
|
|
247
|
+
queue.async { done.resume(returning: work()) }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
271
250
|
|
|
251
|
+
func buildStillGraph() -> Bool {
|
|
272
252
|
session.beginConfiguration()
|
|
253
|
+
defer { session.commitConfiguration() }
|
|
273
254
|
session.sessionPreset = .photo
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
position: .back) else { return }
|
|
279
|
-
currentDevice = device
|
|
280
|
-
|
|
281
|
-
guard let input = try? AVCaptureDeviceInput(device: device),
|
|
282
|
-
session.canAddInput(input) else { return }
|
|
255
|
+
let wide = AVCaptureDevice.DeviceType.builtInWideAngleCamera
|
|
256
|
+
guard let rear = AVCaptureDevice.default(wide, for: .video, position: .back),
|
|
257
|
+
let input = try? AVCaptureDeviceInput(device: rear),
|
|
258
|
+
session.canAddInput(input) else { return false }
|
|
283
259
|
session.addInput(input)
|
|
260
|
+
guard session.canAddOutput(stills) else { return false }
|
|
261
|
+
session.addOutput(stills)
|
|
262
|
+
return true
|
|
263
|
+
}
|
|
264
|
+
}
|
|
284
265
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
266
|
+
@available(iOS 17.0, *)
|
|
267
|
+
@Observable @MainActor
|
|
268
|
+
final class ReceiptCamera {
|
|
269
|
+
private(set) var isRunning = false
|
|
270
|
+
@ObservationIgnored private let engine = CaptureEngine()
|
|
271
|
+
var session: AVCaptureSession { engine.session }
|
|
288
272
|
|
|
289
|
-
|
|
273
|
+
func configure() async {
|
|
274
|
+
guard await CameraAccess.isGranted() else { return }
|
|
275
|
+
_ = await engine.run { [engine] in engine.buildStillGraph() }
|
|
290
276
|
}
|
|
291
277
|
|
|
292
|
-
func start() {
|
|
293
|
-
guard !
|
|
294
|
-
|
|
295
|
-
session.startRunning()
|
|
296
|
-
}
|
|
278
|
+
func start() async {
|
|
279
|
+
guard !isRunning else { return }
|
|
280
|
+
await engine.run { [engine] in engine.session.startRunning() }
|
|
297
281
|
isRunning = true
|
|
298
282
|
}
|
|
299
283
|
|
|
300
|
-
func stop() {
|
|
301
|
-
guard
|
|
302
|
-
|
|
303
|
-
session.stopRunning()
|
|
304
|
-
}
|
|
284
|
+
func stop() async {
|
|
285
|
+
guard isRunning else { return }
|
|
286
|
+
await engine.run { [engine] in engine.session.stopRunning() }
|
|
305
287
|
isRunning = false
|
|
306
288
|
}
|
|
307
|
-
|
|
308
|
-
private func requestCameraAccess() async -> Bool {
|
|
309
|
-
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
|
310
|
-
if status == .notDetermined {
|
|
311
|
-
return await AVCaptureDevice.requestAccess(for: .video)
|
|
312
|
-
}
|
|
313
|
-
return status == .authorized
|
|
314
|
-
}
|
|
315
289
|
}
|
|
316
290
|
```
|
|
317
291
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
292
|
+
Keep every configuration change between `beginConfiguration()` and
|
|
293
|
+
`commitConfiguration()`, and guard each `addInput` / `addOutput` with the
|
|
294
|
+
matching `canAdd` check.
|
|
321
295
|
|
|
322
|
-
|
|
296
|
+
### Preview layer in SwiftUI
|
|
323
297
|
|
|
324
298
|
```swift
|
|
325
|
-
|
|
326
|
-
import AVFoundation
|
|
327
|
-
|
|
328
|
-
struct CameraPreview: UIViewRepresentable {
|
|
299
|
+
struct ViewfinderView: UIViewRepresentable {
|
|
329
300
|
let session: AVCaptureSession
|
|
330
301
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
view.previewLayer.videoGravity = .resizeAspectFill
|
|
335
|
-
return view
|
|
302
|
+
final class LayerHost: UIView {
|
|
303
|
+
override static var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
|
|
304
|
+
var preview: AVCaptureVideoPreviewLayer { unsafeDowncast(layer, to: AVCaptureVideoPreviewLayer.self) }
|
|
336
305
|
}
|
|
337
306
|
|
|
338
|
-
func
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
307
|
+
func makeUIView(context: Context) -> LayerHost {
|
|
308
|
+
let host = LayerHost()
|
|
309
|
+
host.preview.session = session
|
|
310
|
+
host.preview.videoGravity = .resizeAspectFill
|
|
311
|
+
return host
|
|
342
312
|
}
|
|
343
|
-
}
|
|
344
313
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
314
|
+
func updateUIView(_ host: LayerHost, context: Context) {
|
|
315
|
+
if host.preview.session !== session { host.preview.session = session }
|
|
316
|
+
}
|
|
348
317
|
}
|
|
349
318
|
```
|
|
350
319
|
|
|
351
|
-
|
|
320
|
+
Overriding `layerClass` makes the preview layer the view's backing layer, so
|
|
321
|
+
it follows the view's size with no manual frame code.
|
|
322
|
+
|
|
323
|
+
### A capture screen
|
|
352
324
|
|
|
353
325
|
```swift
|
|
354
|
-
|
|
355
|
-
|
|
326
|
+
@available(iOS 17.0, *)
|
|
327
|
+
struct ReceiptCaptureScreen: View {
|
|
328
|
+
@State private var camera = ReceiptCamera()
|
|
356
329
|
|
|
357
330
|
var body: some View {
|
|
358
331
|
ZStack(alignment: .bottom) {
|
|
359
|
-
|
|
360
|
-
.ignoresSafeArea()
|
|
361
|
-
|
|
332
|
+
ViewfinderView(session: camera.session).ignoresSafeArea()
|
|
362
333
|
Button {
|
|
363
|
-
//
|
|
334
|
+
// capture, see references/camera-capture.md
|
|
364
335
|
} label: {
|
|
365
336
|
Circle()
|
|
366
337
|
.fill(.white)
|
|
367
338
|
.frame(width: 72, height: 72)
|
|
368
|
-
.overlay
|
|
339
|
+
.overlay { Circle().strokeBorder(.gray, lineWidth: 3) }
|
|
369
340
|
}
|
|
370
|
-
.padding(.bottom)
|
|
341
|
+
.padding(.bottom, 32)
|
|
371
342
|
}
|
|
372
343
|
.task {
|
|
373
|
-
await
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
.onDisappear {
|
|
377
|
-
cameraManager.stop()
|
|
344
|
+
await camera.configure()
|
|
345
|
+
await camera.start()
|
|
378
346
|
}
|
|
347
|
+
.onDisappear { Task { await camera.stop() } }
|
|
379
348
|
}
|
|
380
349
|
}
|
|
381
350
|
```
|
|
382
351
|
|
|
383
|
-
Always
|
|
352
|
+
Always stop the session when the screen goes away. A running session keeps the
|
|
353
|
+
camera to itself and burns battery.
|
|
384
354
|
|
|
385
|
-
##
|
|
355
|
+
## Loading and showing images
|
|
386
356
|
|
|
387
|
-
###
|
|
357
|
+
### Remote images with AsyncImage
|
|
388
358
|
|
|
389
359
|
```swift
|
|
390
|
-
AsyncImage(url:
|
|
360
|
+
AsyncImage(url: posterURL) { phase in
|
|
391
361
|
switch phase {
|
|
392
|
-
case .empty:
|
|
393
|
-
|
|
394
|
-
case .
|
|
395
|
-
|
|
396
|
-
.resizable()
|
|
397
|
-
.scaledToFill()
|
|
398
|
-
case .failure:
|
|
399
|
-
Image(systemName: "photo")
|
|
400
|
-
.foregroundStyle(.secondary)
|
|
401
|
-
@unknown default:
|
|
402
|
-
EmptyView()
|
|
362
|
+
case .empty: ProgressView()
|
|
363
|
+
case .success(let poster): poster.resizable().scaledToFit()
|
|
364
|
+
case .failure: Image(systemName: "photo")
|
|
365
|
+
@unknown default: EmptyView()
|
|
403
366
|
}
|
|
404
367
|
}
|
|
405
|
-
.frame(width: 200, height: 200)
|
|
406
|
-
.clipShape(.rect(cornerRadius: 12))
|
|
407
368
|
```
|
|
408
369
|
|
|
409
|
-
`AsyncImage`
|
|
370
|
+
`AsyncImage` keeps no cache between redraws. An app that shows many remote
|
|
371
|
+
images needs a real loader or `URLCache`-backed caching; the
|
|
372
|
+
[loading and caching guide](references/image-loading-caching.md) builds one.
|
|
410
373
|
|
|
411
|
-
###
|
|
374
|
+
### Downsample before display
|
|
412
375
|
|
|
413
|
-
|
|
376
|
+
Decode library photos straight to the size they will be drawn at. A 48 MP
|
|
377
|
+
photo takes more than 200 MB once decoded.
|
|
414
378
|
|
|
415
379
|
```swift
|
|
416
380
|
import ImageIO
|
|
417
|
-
import UIKit
|
|
418
381
|
|
|
419
|
-
func
|
|
420
|
-
let
|
|
421
|
-
|
|
422
|
-
let
|
|
382
|
+
func thumbnail(from bytes: Data, fitting points: CGSize, scale: CGFloat) -> UIImage? {
|
|
383
|
+
let longestEdge = max(points.width, points.height) * scale
|
|
384
|
+
guard let reader = CGImageSourceCreateWithData(bytes as CFData, nil) else { return nil }
|
|
385
|
+
let opts: [CFString: Any] = [
|
|
386
|
+
kCGImageSourceThumbnailMaxPixelSize: longestEdge,
|
|
387
|
+
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
423
388
|
kCGImageSourceCreateThumbnailFromImageAlways: true,
|
|
424
389
|
kCGImageSourceShouldCacheImmediately: true,
|
|
425
|
-
kCGImageSourceCreateThumbnailWithTransform: true,
|
|
426
|
-
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
|
|
427
390
|
]
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {
|
|
431
|
-
return nil
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return UIImage(cgImage: cgImage)
|
|
391
|
+
let scaled = CGImageSourceCreateThumbnailAtIndex(reader, 0, opts as CFDictionary)
|
|
392
|
+
return scaled.map { UIImage(cgImage: $0) }
|
|
435
393
|
}
|
|
436
394
|
```
|
|
437
395
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
## Review Checklist
|
|
485
|
-
|
|
486
|
-
- [ ] `PhotosPicker` used instead of deprecated `UIImagePickerController`
|
|
487
|
-
- [ ] Privacy descriptions in Info.plist for camera/photo library
|
|
488
|
-
- [ ] Loading states handled for async image/video loading
|
|
489
|
-
- [ ] Large images downsampled with `CGImageSource` before display
|
|
490
|
-
- [ ] Camera session started on background thread; stopped in `onDisappear`
|
|
491
|
-
- [ ] Permission denial handled with Settings deep link
|
|
492
|
-
- [ ] `AVCaptureSession` owned by model, not created inside `UIViewRepresentable`
|
|
493
|
-
- [ ] Media asset types and picker results are `Sendable` across concurrency boundaries
|
|
396
|
+
Pass the screen scale in (`UITraitCollection.current.displayScale` or the
|
|
397
|
+
SwiftUI `displayScale` environment value). Use this for every list, grid or
|
|
398
|
+
thumbnail of picked photos, and feed the raw `Data` from `PhotosPickerItem`
|
|
399
|
+
directly into it before any `UIImage` exists.
|
|
400
|
+
|
|
401
|
+
### Rendering modes
|
|
402
|
+
|
|
403
|
+
- `.renderingMode(.original)` keeps the image's own colors. Use it for photos
|
|
404
|
+
and artwork.
|
|
405
|
+
- `.renderingMode(.template)` turns the image into a mask filled with the
|
|
406
|
+
current `foregroundStyle`. Use it for icons that should follow the tint.
|
|
407
|
+
|
|
408
|
+
## Common mistakes
|
|
409
|
+
|
|
410
|
+
1. Picking through `UIImagePickerController`. Its library source is legacy and
|
|
411
|
+
limited; `PhotosPicker` (SwiftUI) or `PHPickerViewController` (UIKit) runs
|
|
412
|
+
out of process, supports multi-select and needs no permission to browse.
|
|
413
|
+
2. Asking for full library access just so someone can pick a photo. Use
|
|
414
|
+
`PhotosPicker`, or ask for `.readWrite` so the system can offer limited access.
|
|
415
|
+
Pick-and-use never needs the whole library.
|
|
416
|
+
3. Decoding full-size images for thumbnails. Downsample with `CGImageSource`
|
|
417
|
+
and `kCGImageSourceThumbnailMaxPixelSize`.
|
|
418
|
+
4. Loading `PhotosPickerItem` data on the main thread. Call the async
|
|
419
|
+
`loadTransferable(type:)` inside a `Task`.
|
|
420
|
+
5. Leaving the `AVCaptureSession` running after the view leaves. Stop it in
|
|
421
|
+
`onDisappear` or `dismantleUIView`.
|
|
422
|
+
6. Assuming the camera is authorized. Read the current video authorization
|
|
423
|
+
status first; `.denied` and `.restricted` both need a path.
|
|
424
|
+
7. Calling `startRunning()` on the main thread. It blocks for hundreds of
|
|
425
|
+
milliseconds while the hardware starts; run it on a dedicated serial queue.
|
|
426
|
+
8. Creating the session inside a `UIViewRepresentable`. Own it in an
|
|
427
|
+
`@Observable` model.
|
|
428
|
+
|
|
429
|
+
## Review checklist
|
|
430
|
+
|
|
431
|
+
- [ ] `PhotosPicker` or `PHPickerViewController` is used, not the legacy
|
|
432
|
+
`UIImagePickerController` library picker
|
|
433
|
+
- [ ] Camera and photo library usage strings are in Info.plist
|
|
434
|
+
- [ ] Async image and video loads show a loading state
|
|
435
|
+
- [ ] Large images go through `CGImageSource` downsampling before display
|
|
436
|
+
- [ ] The capture session starts off the main thread and stops in `onDisappear`
|
|
437
|
+
- [ ] Denied permission leads to a Settings link
|
|
438
|
+
- [ ] The capture session belongs to a model, not to a `UIViewRepresentable`
|
|
439
|
+
- [ ] Media values and picker results that cross concurrency boundaries are
|
|
440
|
+
`Sendable`
|
|
494
441
|
|
|
495
442
|
## References
|
|
496
443
|
|
|
497
|
-
- [
|
|
498
|
-
|
|
499
|
-
- [
|
|
500
|
-
|
|
444
|
+
- [Picker recipes](references/photokit-patterns.md): loading states, video and
|
|
445
|
+
Live Photos, saving, HEIC
|
|
446
|
+
- [Capture recipes](references/camera-capture.md): the full capture model,
|
|
447
|
+
stills and movies, barcode and QR scanning
|
|
448
|
+
- [Loading and caching](references/image-loading-caching.md): the
|
|
449
|
+
AsyncImage limits, memory and disk caches, downsampling
|
|
450
|
+
- [Playback](references/av-playback.md): the audio session, HLS streaming,
|
|
451
|
+
AVPlayer
|