Photokit

dpearson2699/swift-ios-skills/skills/photokit

作者 dpearson26998d90fd121a26無授權條款1.1K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫2 個月前更新

Implement, 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, video recording, or media permissions. Also use when selecting photos from the library, taking pictures, recording video, processing images, or handling photo/camera privacy permissions in Swift apps.

AI 產生的概覽

指導 iOS 應用程式使用 PhotoKit 與 AVFoundation 實作照片挑選、相機拍攝、影像載入與媒體權限。

功能
提供 Swift 模式,涵蓋 PhotosPicker 與 PHPickerViewController 挑選、AVCaptureSession 相機拍攝、影像載入與降取樣,以及照片與相機權限處理。包含程式碼範例、常見錯誤和審查清單,並附上挑選器模式、相機拍攝、影像快取和 AV 播放的參考檔案。
適用情境
適用於在 iOS 應用程式中實作、審查或改善照片挑選、相機拍攝、影片錄製、影像顯示或媒體權限時。也適合在 Swift 中處理挑選照片、拍照或照片與相機隱私權限的情境。
執行需求
無指令碼,僅包含說明與參考文件。面向 iOS 16+ 與 Swift 6.3,需要 Xcode、SwiftUI、PhotosUI、Photos、AVFoundation 和 ImageIO。應用程式需在 Info.plist 中提供照片圖庫、相機和麥克風的使用說明。

PhotoKit

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 [blocked] for complete picker recipes and references/camera-capture.md [blocked] for AVCaptureSession patterns.

Contents

PhotosPicker (SwiftUI, iOS 16+)

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.

Single Selection

swift
import SwiftUIimport PhotosUI
struct SinglePhotoPicker: View {    @State private var selectedItem: PhotosPickerItem?    @State private var selectedImage: Image?
    var body: some View {        VStack {            if let selectedImage {                selectedImage                    .resizable()                    .scaledToFit()                    .frame(maxHeight: 300)            }
            PhotosPicker("Select Photo", selection: $selectedItem, matching: .images)        }        .onChange(of: selectedItem) { _, newItem in            Task {                if let data = try? await newItem?.loadTransferable(type: Data.self),                   let uiImage = UIImage(data: data) {                    selectedImage = Image(uiImage: uiImage)                }            }        }    }}

Multi-Selection

swift
struct MultiPhotoPicker: View {    @State private var selectedItems: [PhotosPickerItem] = []    @State private var selectedImages: [Image] = []
    var body: some View {        VStack {            ScrollView(.horizontal) {                HStack {                    ForEach(selectedImages.indices, id: \.self) { index in                        selectedImages[index]                            .resizable()                            .scaledToFill()                            .frame(width: 100, height: 100)                            .clipShape(.rect(cornerRadius: 8))                    }                }            }
            PhotosPicker(                "Select Photos",                selection: $selectedItems,                maxSelectionCount: 5,                matching: .images            )        }        .onChange(of: selectedItems) { _, newItems in            Task {                selectedImages = []                for item in newItems {                    if let data = try? await item.loadTransferable(type: Data.self),                       let uiImage = UIImage(data: data) {                        selectedImages.append(Image(uiImage: uiImage))                    }                }            }        }    }}

Media Type Filtering

Filter with PHPickerFilter composites to restrict selectable media:

swift
// Images onlyPhotosPicker(selection: $items, matching: .images)
// Videos onlyPhotosPicker(selection: $items, matching: .videos)
// Live Photos onlyPhotosPicker(selection: $items, matching: .livePhotos)
// Screenshots onlyPhotosPicker(selection: $items, matching: .screenshots)
// Images and videos combinedPhotosPicker(selection: $items, matching: .any(of: [.images, .videos]))
// Images excluding screenshotsPhotosPicker(selection: $items, matching: .all(of: [.images, .not(.screenshots)]))

Loading Selected Items with Transferable

PhotosPickerItem loads content asynchronously via loadTransferable(type:). Define a Transferable type for automatic decoding:

swift
struct PickedImage: Transferable {    let data: Data    let image: Image
    static var transferRepresentation: some TransferRepresentation {        DataRepresentation(importedContentType: .image) { data in            guard let uiImage = UIImage(data: data) else {                throw TransferError.importFailed            }            return PickedImage(data: data, image: Image(uiImage: uiImage))        }    }}
enum TransferError: Error {    case importFailed}
// Usageif let picked = try? await item.loadTransferable(type: PickedImage.self) {    selectedImage = picked.image}

Always load in a Task to avoid blocking the main thread. Handle nil returns and thrown errors -- the user may select a format that cannot be decoded.

Privacy and Permissions

Photo Library Access Levels

iOS provides two access levels for the photo library. The system automatically presents the limited-library picker when an app requests .readWrite access -- users choose which photos to share.

Access LevelDescriptionInfo.plist Key
Add-onlyWrite photos to the library without readingNSPhotoLibraryAddUsageDescription
Read-writeFull or limited read access plus writeNSPhotoLibraryUsageDescription

PhotosPicker requires no permission to browse -- it runs out-of-process and only grants access to selected items. Request explicit permission only when you need to read the full library (e.g., a custom gallery) or save photos.

Checking and Requesting Photo Library Permission

swift
import Photos
func requestPhotoLibraryAccess() async -> PHAuthorizationStatus {    let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
    switch status {    case .notDetermined:        return await PHPhotoLibrary.requestAuthorization(for: .readWrite)    case .authorized, .limited:        return status    case .denied, .restricted:        return status    @unknown default:        return status    }}

Camera Permission

Add NSCameraUsageDescription to Info.plist. Check and request access before configuring a capture session:

swift
import AVFoundation
func requestCameraAccess() async -> Bool {    let status = AVCaptureDevice.authorizationStatus(for: .video)
    switch status {    case .notDetermined:        return await AVCaptureDevice.requestAccess(for: .video)    case .authorized:        return true    case .denied, .restricted:        return false    @unknown default:        return false    }}

Handling Denied Permissions

When the user denies access, guide them to Settings. Never repeatedly prompt or hide functionality silently.

swift
struct PermissionDeniedView: View {    let message: String    @Environment(\.openURL) private var openURL
    var body: some View {        ContentUnavailableView {            Label("Access Denied", systemImage: "lock.shield")        } description: {            Text(message)        } actions: {            Button("Open Settings") {                if let url = URL(string: UIApplication.openSettingsURLString) {                    openURL(url)                }            }        }    }}

Required Info.plist Keys

KeyWhen Required
NSPhotoLibraryUsageDescriptionReading photos from the library
NSPhotoLibraryAddUsageDescriptionSaving photos/videos to the library
NSCameraUsageDescriptionAccessing the camera
NSMicrophoneUsageDescriptionRecording audio (video with sound)

Omitting a required key causes a runtime crash when the permission dialog would appear.

Camera Capture Basics

Own each capture session in a dedicated controller and serialize configuration, startRunning(), and stopRunning() on the same non-main executor. Never mix main-actor configuration with detached start/stop tasks: beginConfiguration()/commitConfiguration() and session state changes must not race. The representable view only displays the preview.

Minimal Camera Manager

Load Camera Capture [blocked] for the serialized-controller pattern, photo/video delegates, focus, torch, orientation, and scanning. The critical lifecycle is:

  1. Request access outside the session configuration transaction.
  2. On the capture executor, call beginConfiguration() and immediately install defer { commitConfiguration() } so every early exit balances the transaction.
  3. Add inputs and outputs only after canAddInput/canAddOutput checks.
  4. Start or stop on that same executor, then publish UI state on the main actor only after the synchronous call returns.
  5. On failure, stop, restore a fresh session fixture, fix the configuration, and rerun authorization, background/foreground, interruption, and capture checks.

Camera Preview in SwiftUI

Wrap AVCaptureVideoPreviewLayer in a UIViewRepresentable. Override layerClass for automatic resizing:

swift
import SwiftUIimport AVFoundation
struct CameraPreview: UIViewRepresentable {    let session: AVCaptureSession
    func makeUIView(context: Context) -> PreviewView {        let view = PreviewView()        view.previewLayer.session = session        view.previewLayer.videoGravity = .resizeAspectFill        return view    }
    func updateUIView(_ uiView: PreviewView, context: Context) {        if uiView.previewLayer.session !== session {            uiView.previewLayer.session = session        }    }}
final class PreviewView: UIView {    override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }    var previewLayer: AVCaptureVideoPreviewLayer { layer as! AVCaptureVideoPreviewLayer }}

Using the Camera in a View

swift
struct CameraScreen: View {    @State private var cameraManager = CameraManager()
    var body: some View {        ZStack(alignment: .bottom) {            CameraPreview(session: cameraManager.session)                .ignoresSafeArea()
            Button {                // Capture photo -- see references/camera-capture.md            } label: {                Circle()                    .fill(.white)                    .frame(width: 72, height: 72)                    .overlay(Circle().stroke(.gray, lineWidth: 3))            }            .padding(.bottom)        }        .task {            await cameraManager.configure()            cameraManager.start()        }        .onDisappear {            cameraManager.stop()        }    }}

Always call stop() in onDisappear. A running capture session holds the camera exclusively and drains battery.

Image Loading and Display

AsyncImage for Remote Images

swift
AsyncImage(url: imageURL) { phase in    switch phase {    case .empty:        ProgressView()    case .success(let image):        image            .resizable()            .scaledToFill()    case .failure:        Image(systemName: "photo")            .foregroundStyle(.secondary)    @unknown default:        EmptyView()    }}.frame(width: 200, height: 200).clipShape(.rect(cornerRadius: 12))

AsyncImage does not cache images across view redraws. For production apps with many images, use a dedicated image loading library or URLCache-based caching.

Downsampling Large Images

Load full-resolution photos from the library into a display-sized CGImage to avoid memory spikes. A 48MP photo can consume over 200 MB uncompressed.

swift
import ImageIOimport UIKit
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UITraitCollection.current.displayScale) -> UIImage? {    let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
    let options: [CFString: Any] = [        kCGImageSourceCreateThumbnailFromImageAlways: true,        kCGImageSourceShouldCacheImmediately: true,        kCGImageSourceCreateThumbnailWithTransform: true,        kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels    ]
    guard let source = CGImageSourceCreateWithData(data as CFData, nil),          let cgImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary) else {        return nil    }
    return UIImage(cgImage: cgImage)}

Use this whenever displaying user-selected photos in lists, grids, or thumbnails. Pass the raw Data from PhotosPickerItem directly to the downsampler before creating a UIImage.

Image Rendering Modes

swift
// Original: display the image as-is with its original colorsImage("photo")    .renderingMode(.original)
// Template: treat the image as a mask, colored by foregroundStyleImage(systemName: "heart.fill")    .renderingMode(.template)    .foregroundStyle(.red)

Use .original for photos and artwork. Use .template for icons that should adopt the current tint color.

Common Mistakes

DON'T: Use UIImagePickerController for photo picking. DO: Use PhotosPicker (SwiftUI) or PHPickerViewController (UIKit). Why: UIImagePickerController is legacy API with limited functionality. PhotosPicker runs out-of-process, supports multi-selection, and requires no library permission for browsing.

DON'T: Request full photo library access when you only need the user to pick photos. DO: Use PhotosPicker which requires no permission, or request .readWrite and let the system handle limited access. Why: Full access is unnecessary for most pick-and-use workflows. The system's limited-library picker respects user privacy and still grants access to selected items.

DON'T: Load full-resolution images into memory for thumbnails. DO: Use CGImageSource with kCGImageSourceThumbnailMaxPixelSize to downsample. A 48MP image is over 200 MB uncompressed.

DON'T: Block the main thread loading PhotosPickerItem data. DO: Use async loadTransferable(type:) in a Task.

DON'T: Forget to stop AVCaptureSession when the view disappears. DO: Call session.stopRunning() in onDisappear or dismantleUIView.

DON'T: Assume camera access is granted without checking. DO: Check AVCaptureDevice.authorizationStatus(for: .video) and handle .denied/.restricted.

DON'T: Call session.startRunning() on the main thread. DO: Run it on the same dedicated serial executor that owns configuration and stop operations. Why: startRunning() is a synchronous blocking call that can take hundreds of milliseconds while the hardware initializes.

DON'T: Create AVCaptureSession inside a UIViewRepresentable. DO: Own the session in a separate @Observable model.

Review Checklist

  • PhotosPicker used instead of deprecated UIImagePickerController
  • Privacy descriptions in Info.plist for camera/photo library
  • Loading states handled for async image/video loading
  • Large images downsampled with CGImageSource before display
  • Camera session started on background thread; stopped in onDisappear
  • Permission denial handled with Settings deep link
  • AVCaptureSession owned by model, not created inside UIViewRepresentable
  • Media asset types and picker results are Sendable across concurrency boundaries

References

  • references/photokit-patterns.md [blocked] — Picker patterns, media loading, HEIC handling
  • references/camera-capture.md [blocked] — AVCaptureSession, photo/video capture, QR scanning
  • references/image-loading-caching.md [blocked] — AsyncImage, caching, downsampling
  • references/av-playback.md [blocked] — AVPlayer, streaming, audio

來源與署名

來源:dpearson2699/swift-ios-skills位於skills/photokit提交8d90fd1

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架

更多來自 dpearson2699/swift-ios-skills 的技能

Widgetkit

dpearson2699

指導實作、審查與改進 iOS、iPadOS、watchOS 與 CarPlay 上的 WidgetKit 小工具與控制項。

Software Development1.1K2 個月前更新

Weatherkit

dpearson2699

指導 iOS 開發者使用 WeatherService 取得 WeatherKit 預報、警報與署名資訊。

Software Development1.1K2 個月前更新

Vision Framework

dpearson2699

Implement computer vision features including text recognition (OCR), face detection, barcode scanning, image segmentation, object tracking, and document scanning in iOS apps. Covers both the modern Swift-native Vision API (iOS 18+) and legacy VNRequest patterns, VisionKit DataScannerViewController for live camera scanning, and CoreMLRequest/VNCoreMLRequest for custom model inference. Use when adding OCR, barcode scanning, face detection, or custom Core ML model inference with Vision.

待分類1.1K2 個月前更新

Tipkit

dpearson2699

Implement and review Apple TipKit feature-discovery UI for iOS 17+ apps. Use when adding or auditing in-app tips, contextual help, coach marks, Tip, TipView, popoverTip, rules, events, actions, display frequency, testing overrides, reusable tip identifiers, or iOS 18+ TipGroup and CloudKit tip sync; avoid for generic SwiftUI navigation or layout outside tip presentation.

待分類1.1K2 個月前更新

Tabletopkit

dpearson2699

指導使用 TabletopKit 在 visionOS 上打造多人空間桌遊,涵蓋棋具、座位、動作與 RealityKit 算繪。

Software Development1.1K2 個月前更新

Swiftui Webkit

dpearson2699

指導在 iOS 26 及更新版本的 SwiftUI App 中使用 WebKit for SwiftUI 嵌入與控制網頁內容。

Software Development1.1K2 個月前更新