关于 photokit
PhotoKit 是面向 Swift 开发者的 iOS 照片、相机与图像处理模式指南,涵盖 SwiftUI 原生选图、隐私权限和 AVCaptureSession 生命周期管理。
主要功能
- 01SwiftUI 照片单选与多选
- 02异步加载所选媒体
- 03照片与相机权限处理
- 04AVCaptureSession 配置与预览
- 05图像缩小采样与显示
适用场景
- 01实现照片或视频选择器
- 02构建相机拍摄界面
- 03显示大量照片缩略图
- 04处理拒绝权限后的恢复流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill photokit安装后在 Claude Code 中输入/photokit确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/av-playback.mdcamera-capture.mdimage-loading-caching.mdphotokit-patterns.md详细介绍
PhotoKit
针对 iOS 26+ 和 Swift 6.3 的现代照片选取、相机拍摄、图像加载和媒体权限模式。除非另有说明,这些模式向后兼容至 iOS 16。完整的选取器示例请参阅 references/photokit-patterns.md,AVCaptureSession 模式请参阅 references/camera-capture.md。
目录
PhotosPicker(SwiftUI,iOS 16+)
PhotosPicker 是原生 SwiftUI 方案,用于替代 UIImagePickerController。它在进程外运行,浏览照片库无需权限,并支持按媒体类型筛选单选或多选。
单选
import SwiftUI
import 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)
}
}
}
}
}
多选
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))
}
}
}
}
}
}
媒体类型筛选
使用 PHPickerFilter 组合来限制可选媒体:
// Images only
PhotosPicker(selection: $items, matching: .images)
// Videos only
PhotosPicker(selection: $items, matching: .videos)
// Live Photos only
PhotosPicker(selection: $items, matching: .livePhotos)
// Screenshots only
PhotosPicker(selection: $items, matching: .screenshots)
// Images and videos combined
PhotosPicker(selection: $items, matching: .any(of: [.images, .videos]))
// Images excluding screenshots
PhotosPicker(selection: $items, matching: .all(of: [.images, .not(.screenshots)]))
使用 Transferable 加载选中的项目
PhotosPickerItem 通过 loadTransferable(type:) 异步加载内容。定义 Transferable 类型以自动解码:
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
}
// Usage
if let picked = try? await item.loadTransferable(type: PickedImage.self) {
selectedImage = picked.image
}
始终在 Task 中加载,避免阻塞主线程。处理返回 nil 和抛出的错误——用户可能选中了无法解码的格式。
隐私与权限
照片库访问级别
iOS 为照片库提供两个访问级别。当应用请求 .readWrite 访问权限时,系统会自动显示有限照片库选取器——用户可以选择要分享的照片。
| 访问级别 | 说明 | Info.plist 键 |
|---|---|---|
| 仅添加 | 向照片库写入照片,但不读取 | NSPhotoLibraryAddUsageDescription |
| 读写 | 完整或有限的读取权限,以及写入权限 | NSPhotoLibraryUsageDescription |
浏览照片无需 PhotosPicker 权限——它在进程外运行,且仅授予对所选项目的访问权限。只有在需要读取整个照片库(例如,自定义图库)或保存照片时,才请求显式权限。
检查并请求照片库权限
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
}
}
相机权限
将 NSCameraUsageDescription 添加到 Info.plist。在配置拍摄会话之前检查并请求访问权限:
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
}
}
处理被拒绝的权限
用户拒绝访问时,引导他们前往“设置”。切勿反复请求权限,也不要悄无声息地隐藏功能。
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)
}
}
}
}
}
必需的 Info.plist 键
| 键 | 必需场景 |
|---|---|
NSPhotoLibraryUsageDescription | 从照片库读取照片 |
NSPhotoLibraryAddUsageDescription | 将照片/视频保存到照片库 |
NSCameraUsageDescription | 访问相机 |
NSMicrophoneUsageDescription | 录制音频(带声音的视频) |
如果缺少必需的键,在本应显示权限对话框时会导致运行时崩溃。
相机拍摄基础知识
在专用控制器中管理每个拍摄会话,并在同一个非主执行器上串行执行配置、startRunning() 和 stopRunning()。切勿混用主执行器隔离的配置与分离的启动/停止任务:beginConfiguration()/commitConfiguration() 和会话状态更改不得发生竞争。可表示视图仅负责显示预览。
最小化的相机管理器
阅读 Camera Capture,了解序列化控制器模式、照片/视频代理、对焦、闪光灯、方向和扫描。关键生命周期如下:
- 在会话配置事务之外请求访问权限。
- 在采集执行器上调用
beginConfiguration(),并立即设置defer { commitConfiguration() },确保每次提前退出时都能平衡事务。 - 仅在通过
canAddInput/canAddOutput检查后添加输入和输出。 - 在同一执行器上启动或停止会话,然后仅在同步调用返回后才在主执行器上发布 UI 状态。
- 发生故障时,停止会话、恢复全新的会话配置,修正配置,然后重新运行授权、后台/前台切换、中断和采集检查。
在 SwiftUI 中预览相机画面
将 AVCaptureVideoPreviewLayer 封装在 UIViewRepresentable 中。重写 layerClass 以实现自动调整大小:
import SwiftUI
import 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 }
}
在视图中使用相机
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()
}
}
}
始终在 onDisappear 中调用 stop()。运行中的采集会话会独占相机并耗尽电池。
图像加载与显示
使用 AsyncImage 加载远程图像
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 不会在视图重绘期间缓存图像。对于包含大量图像的生产应用,请使用专用图像加载库或基于 URLCache 的缓存。
对大图像进行降采样
将图库中的全分辨率照片加载为适合显示尺寸的 CGImage,以避免内存激增。一张 48MP 的照片在未压缩时可能占用超过 200 MB。
import ImageIO
import 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)
}
在列表、网格或缩略图中显示用户选取的照片时,请始终使用此方法。在创建 UIImage 之前,将 PhotosPickerItem 返回的原始 Data 直接传给降采样器。
图像渲染模式
// Original: display the image as-is with its original colors
Image("photo")
.renderingMode(.original)
// Template: treat the image as a mask, colored by foregroundStyle
Image(systemName: "heart.fill")
.renderingMode(.template)
.foregroundStyle(.red)
照片和艺术作品使用 .original。需要采用当前色调颜色的图标使用 .template。
常见错误
不要: 使用 UIImagePickerController 选取照片。
应该: 使用 PhotosPicker(SwiftUI)或 PHPickerViewController(UIKit)。
原因: UIImagePickerController 是功能有限的旧版 API。PhotosPicker 在进程外运行,支持多选,浏览时无需图库权限。
不要: 仅在需要用户选取照片时请求完整的图库访问权限。
应该: 使用无需权限的 PhotosPicker,或请求 .readWrite 并让系统处理有限访问权限。
原因: 对大多数选取并使用的工作流而言,完整访问权限并无必要。系统的有限图库选取器会尊重用户隐私,同时仍允许访问选中的项目。
不要: 将全分辨率图像加载到内存中作为缩略图。
应该: 使用 CGImageSource 和 kCGImageSourceThumbnailMaxPixelSize 进行降采样。一张 48MP 图像在未压缩时超过 200 MB。
不要: 在主线程上阻塞加载 PhotosPickerItem 数据。
应该: 在 Task 中使用 async loadTransferable(type:)。
不要: 忘记在视图消失时停止 AVCaptureSession。
应该: 在 onDisappear 或 dismantleUIView 中调用 session.stopRunning()。
不要: 未经检查就假定相机访问权限已获授。
应该: 检查 AVCaptureDevice.authorizationStatus(for: .video) 并处理 .denied/.restricted。
不要: 在主线程上调用 session.startRunning()。
应该: 在负责配置和停止操作的同一个专用串行执行器上运行该调用。
原因: startRunning() 是同步阻塞调用,硬件初始化时可能耗时数百毫秒。
不要: 在 UIViewRepresentable 内创建 AVCaptureSession。
应该: 在单独的 @Observable 模型中持有会话。
审查清单
- 使用
PhotosPicker,而不是已弃用的UIImagePickerController - 在 Info.plist 中为相机和照片图库配置隐私说明
- 处理异步加载图像/视频时的加载状态
- 显示前使用
CGImageSource对大图像进行降采样 - 在后台线程启动相机会话;在
onDisappear中停止 - 处理权限被拒绝的情况,并提供跳转到“设置”的深层链接
-
AVCaptureSession由模型持有,而不是在UIViewRepresentable内创建 - 媒体资源类型和选择器结果在并发边界之间均为
Sendable
参考资料
- references/photokit-patterns.md — 选择器模式、媒体加载、HEIC 处理
- references/camera-capture.md — AVCaptureSession、照片/视频拍摄、二维码扫描
- references/image-loading-caching.md — AsyncImage、缓存、降采样
- references/av-playback.md — AVPlayer、流媒体、音频