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 应用中使用 WebKit for SwiftUI 嵌入和控制网页内容。

Software Development1.1K2个月前更新