$npx -y skills add dpearson2699/swift-ios-skills --skill photokitImplement, review, or improve photo picking, camera capture, and media handling in iOS apps using PhotoKit and AVFoundation. Use when working with PhotosPicker, PHPickerViewController, camera capture sessions (AVCaptureSession), photo library access, image loading and display, vi
| 1 | # PhotoKit |
| 2 | |
| 3 | 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. |
| 4 | |
| 5 | ## Contents |
| 6 | |
| 7 | - [PhotosPicker (SwiftUI, iOS 16+)](#photospicker-swiftui-ios-16) |
| 8 | - [Privacy and Permissions](#privacy-and-permissions) |
| 9 | - [Camera Capture Basics](#camera-capture-basics) |
| 10 | - [Image Loading and Display](#image-loading-and-display) |
| 11 | - [Common Mistakes](#common-mistakes) |
| 12 | - [Review Checklist](#review-checklist) |
| 13 | - [References](#references) |
| 14 | |
| 15 | ## PhotosPicker (SwiftUI, iOS 16+) |
| 16 | |
| 17 | `PhotosPicker` is the native SwiftUI replacement for `UIImagePickerController`. It runs out-of-process, requires no photo library permission for browsing, and supports single or multi-selection with media type filtering. |
| 18 | |
| 19 | ### Single Selection |
| 20 | |
| 21 | ```swift |
| 22 | import SwiftUI |
| 23 | import PhotosUI |
| 24 | |
| 25 | struct SinglePhotoPicker: View { |
| 26 | @State private var selectedItem: PhotosPickerItem? |
| 27 | @State private var selectedImage: Image? |
| 28 | |
| 29 | var body: some View { |
| 30 | VStack { |
| 31 | if let selectedImage { |
| 32 | selectedImage |
| 33 | .resizable() |
| 34 | .scaledToFit() |
| 35 | .frame(maxHeight: 300) |
| 36 | } |
| 37 | |
| 38 | PhotosPicker("Select Photo", selection: $selectedItem, matching: .images) |
| 39 | } |
| 40 | .onChange(of: selectedItem) { _, newItem in |
| 41 | Task { |
| 42 | if let data = try? await newItem?.loadTransferable(type: Data.self), |
| 43 | let uiImage = UIImage(data: data) { |
| 44 | selectedImage = Image(uiImage: uiImage) |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | } |
| 49 | } |
| 50 | ``` |
| 51 | |
| 52 | ### Multi-Selection |
| 53 | |
| 54 | ```swift |
| 55 | struct MultiPhotoPicker: View { |
| 56 | @State private var selectedItems: [PhotosPickerItem] = [] |
| 57 | @State private var selectedImages: [Image] = [] |
| 58 | |
| 59 | var body: some View { |
| 60 | VStack { |
| 61 | ScrollView(.horizontal) { |
| 62 | HStack { |
| 63 | ForEach(selectedImages.indices, id: \.self) { index in |
| 64 | selectedImages[index] |
| 65 | .resizable() |
| 66 | .scaledToFill() |
| 67 | .frame(width: 100, height: 100) |
| 68 | .clipShape(.rect(cornerRadius: 8)) |
| 69 | } |
| 70 | } |
| 71 | } |
| 72 | |
| 73 | PhotosPicker( |
| 74 | "Select Photos", |
| 75 | selection: $selectedItems, |
| 76 | maxSelectionCount: 5, |
| 77 | matching: .images |
| 78 | ) |
| 79 | } |
| 80 | .onChange(of: selectedItems) { _, newItems in |
| 81 | Task { |
| 82 | selectedImages = [] |
| 83 | for item in newItems { |
| 84 | if let data = try? await item.loadTransferable(type: Data.self), |
| 85 | let uiImage = UIImage(data: data) { |
| 86 | selectedImages.append(Image(uiImage: uiImage)) |
| 87 | } |
| 88 | } |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | ### Media Type Filtering |
| 96 | |
| 97 | Filter with `PHPickerFilter` composites to restrict selectable media: |
| 98 | |
| 99 | ```swift |
| 100 | // Images only |
| 101 | PhotosPicker(selection: $items, matching: .images) |
| 102 | |
| 103 | // Videos only |
| 104 | PhotosPicker(selection: $items, matching: .videos) |
| 105 | |
| 106 | // Live Photos only |
| 107 | PhotosPicker(selection: $items, matching: .livePhotos) |
| 108 | |
| 109 | // Screenshots only |
| 110 | PhotosPicker(selection: $items, matching: .screenshots) |
| 111 | |
| 112 | // Images and videos combined |
| 113 | PhotosPicker(selection: $items, matching: .any(of: [.images, .videos])) |
| 114 | |
| 115 | // Images excluding screenshots |
| 116 | PhotosPicker(selection: $items, matching: .all(of: [.images, .not(.screenshots)])) |
| 117 | ``` |
| 118 | |
| 119 | ### Loading Selected Items with Transferable |
| 120 | |
| 121 | `PhotosPickerItem` loads content asynchronously via `loadTransferable(type:)`. Define a `Transferable` type for automatic decoding: |
| 122 | |
| 123 | ```swift |
| 124 | struct PickedImage: Transferable { |
| 125 | let data: Data |
| 126 | let image: Image |
| 127 | |
| 128 | static var transferRepresentation: some TransferRepresentation { |
| 129 | DataRepresentation(importedContentType: .image) { data in |
| 130 | guard let uiImage = UIImage(data: data) else { |
| 131 | throw TransferError.importFailed |
| 132 | } |
| 133 | return PickedImage(data: data, image: Image(uiImage: uiImage)) |
| 134 | } |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | enum TransferError: Error { |
| 139 | case importFailed |
| 140 | } |
| 141 | |
| 142 | // Usage |
| 143 | if let picked = try? await item.loadTr |