photos-camera-media
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePhotos, Camera & Media
照片、相机与媒体
Modern patterns for photo picking, camera capture, image loading, and media permissions targeting iOS 26+ with Swift 6.2. Patterns are backward-compatible to iOS 16 unless noted.
See for complete picker recipes and for AVCaptureSession patterns.
references/photospicker-patterns.mdreferences/camera-capture.md基于Swift 6.2、面向iOS 26+版本的照片选择、相机拍摄、图片加载和媒体权限的现代实现模式。除非另有说明,所有模式都向下兼容到iOS 16。
完整的选择器使用方案请查看,AVCaptureSession相关模式请查看。
references/photospicker-patterns.mdreferences/camera-capture.mdPhotosPicker (SwiftUI, iOS 16+)
PhotosPicker (SwiftUI, iOS 16+)
PhotosPickerUIImagePickerControllerPhotosPickerUIImagePickerControllerSingle Selection
单选
swift
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)
}
}
}
}
}swift
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)
}
}
}
}
}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(RoundedRectangle(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))
}
}
}
}
}
}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(RoundedRectangle(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 composites to restrict selectable media:
PHPickerFilterswift
// 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)]))通过组合进行过滤,限制可选择的媒体类型:
PHPickerFilterswift
// 仅图片
PhotosPicker(selection: $items, matching: .images)
// 仅视频
PhotosPicker(selection: $items, matching: .videos)
// 仅实况照片
PhotosPicker(selection: $items, matching: .livePhotos)
// 仅截图
PhotosPicker(selection: $items, matching: .screenshots)
// 图片+视频
PhotosPicker(selection: $items, matching: .any(of: [.images, .videos]))
// 图片(排除截图)
PhotosPicker(selection: $items, matching: .all(of: [.images, .not(.screenshots)]))Loading Selected Items with Transferable
通过Transferable加载选中项
PhotosPickerItemloadTransferable(type:)Transferableswift
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
}Always load in a to avoid blocking the main thread. Handle returns and thrown errors -- the user may select a format that cannot be decoded.
TasknilPhotosPickerItemloadTransferable(type:)Transferableswift
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
}
// 使用示例
if let picked = try? await item.loadTransferable(type: PickedImage.self) {
selectedImage = picked.image
}请始终在中执行加载操作避免阻塞主线程,同时处理返回和抛出的错误——用户可能选择了无法解码的媒体格式。
TasknilPrivacy 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 access -- users choose which photos to share.
.readWrite| Access Level | Description | Info.plist Key |
|---|---|---|
| Add-only | Write photos to the library without reading | |
| Read-write | Full or limited read access plus write | |
PhotosPickeriOS为相册提供了两种访问级别。当应用申请权限时,系统会自动弹出受限相册选择器,用户可自主选择要共享的照片。
.readWrite| 访问级别 | 描述 | Info.plist键 |
|---|---|---|
| 仅添加 | 仅可写入相册,无读取权限 | |
| 读写 | 完整/受限读取权限+写入权限 | |
PhotosPickerChecking 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
}
}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 to Info.plist. Check and request access before configuring a capture session:
NSCameraUsageDescriptionswift
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
}
}在Info.plist中添加配置。配置拍摄会话前请先检查并申请访问权限:
NSCameraUsageDescriptionswift
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
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) {
UIApplication.shared.open(url)
}
}
}
}
}当用户拒绝权限申请时,引导用户前往系统设置开启权限,请勿重复弹窗或静默隐藏功能。
swift
struct PermissionDeniedView: View {
let message: String
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) {
UIApplication.shared.open(url)
}
}
}
}
}Required Info.plist Keys
必备的Info.plist键
| Key | When Required |
|---|---|
| Reading photos from the library |
| Saving photos/videos to the library |
| Accessing the camera |
| Recording audio (video with sound) |
Omitting a required key causes a runtime crash when the permission dialog would appear.
| 键 | 适用场景 |
|---|---|
| 从相册读取照片 |
| 保存照片/视频到相册 |
| 访问相机 |
| 录制音频(含声音的视频) |
如果遗漏了必填的配置键,系统弹出权限弹窗时应用会直接发生运行时崩溃。
Camera Capture Basics
相机拍摄基础
Manage camera sessions in a dedicated model. The representable view only displays the preview. See for complete patterns.
@Observablereferences/camera-capture.md请在独立的模型中管理相机会话,代表视图仅负责展示预览。完整的实现模式请查看。
@Observablereferences/camera-capture.mdMinimal Camera Manager
极简相机管理器
swift
import AVFoundation
@available(iOS 17.0, *)
@Observable
@MainActor
final class CameraManager {
let session = AVCaptureSession()
private let photoOutput = AVCapturePhotoOutput()
private var currentDevice: AVCaptureDevice?
var isRunning = false
var capturedImage: Data?
func configure() async {
guard await requestCameraAccess() else { return }
session.beginConfiguration()
session.sessionPreset = .photo
// Add camera input
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: .back) else { return }
currentDevice = device
guard let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) else { return }
session.addInput(input)
// Add photo output
guard session.canAddOutput(photoOutput) else { return }
session.addOutput(photoOutput)
session.commitConfiguration()
}
func start() {
guard !session.isRunning else { return }
Task.detached { [session] in
session.startRunning()
}
isRunning = true
}
func stop() {
guard session.isRunning else { return }
Task.detached { [session] in
session.stopRunning()
}
isRunning = false
}
private func requestCameraAccess() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
if status == .notDetermined {
return await AVCaptureDevice.requestAccess(for: .video)
}
return status == .authorized
}
}Start and stop on a background queue. The and methods are synchronous and block the calling thread.
AVCaptureSessionstartRunning()stopRunning()swift
import AVFoundation
@available(iOS 17.0, *)
@Observable
@MainActor
final class CameraManager {
let session = AVCaptureSession()
private let photoOutput = AVCapturePhotoOutput()
private var currentDevice: AVCaptureDevice?
var isRunning = false
var capturedImage: Data?
func configure() async {
guard await requestCameraAccess() else { return }
session.beginConfiguration()
session.sessionPreset = .photo
// 添加相机输入
guard let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: .back) else { return }
currentDevice = device
guard let input = try? AVCaptureDeviceInput(device: device),
session.canAddInput(input) else { return }
session.addInput(input)
// 添加照片输出
guard session.canAddOutput(photoOutput) else { return }
session.addOutput(photoOutput)
session.commitConfiguration()
}
func start() {
guard !session.isRunning else { return }
Task.detached { [session] in
session.startRunning()
}
isRunning = true
}
func stop() {
guard session.isRunning else { return }
Task.detached { [session] in
session.stopRunning()
}
isRunning = false
}
private func requestCameraAccess() async -> Bool {
let status = AVCaptureDevice.authorizationStatus(for: .video)
if status == .notDetermined {
return await AVCaptureDevice.requestAccess(for: .video)
}
return status == .authorized
}
}请在后台队列中启动和停止,和是同步方法,会阻塞调用线程。
AVCaptureSessionstartRunning()stopRunning()Camera Preview in SwiftUI
SwiftUI中的相机预览
Wrap in a . Override for automatic resizing:
AVCaptureVideoPreviewLayerUIViewRepresentablelayerClassswift
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 }
}将封装在中,重写实现自动适配尺寸:
AVCaptureVideoPreviewLayerUIViewRepresentablelayerClassswift
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 }
}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, 32)
}
.task {
await cameraManager.configure()
cameraManager.start()
}
.onDisappear {
cameraManager.stop()
}
}
}Always call in . A running capture session holds the camera exclusively and drains battery.
stop()onDisappearswift
struct CameraScreen: View {
@State private var cameraManager = CameraManager()
var body: some View {
ZStack(alignment: .bottom) {
CameraPreview(session: cameraManager.session)
.ignoresSafeArea()
Button {
// 拍摄照片 -- 参考 references/camera-capture.md
} label: {
Circle()
.fill(.white)
.frame(width: 72, height: 72)
.overlay(Circle().stroke(.gray, lineWidth: 3))
}
.padding(.bottom, 32)
}
.task {
await cameraManager.configure()
cameraManager.start()
}
.onDisappear {
cameraManager.stop()
}
}
}请务必在中调用,运行中的拍摄会话会独占相机资源并持续消耗电量。
onDisappearstop()Image Loading and Display
图片加载与展示
AsyncImage for Remote Images
加载远程图片的AsyncImage
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(RoundedRectangle(cornerRadius: 12))AsyncImageURLCacheswift
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(RoundedRectangle(cornerRadius: 12))AsyncImageURLCacheDownsampling Large Images
大图片下采样
Load full-resolution photos from the library into a display-sized to avoid memory spikes. A 48MP photo can consume over 200 MB uncompressed.
CGImageswift
import ImageIO
import UIKit
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UIScreen.main.scale) -> 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 from directly to the downsampler before creating a .
DataPhotosPickerItemUIImage将相册中的全分辨率照片加载为适配展示尺寸的,避免内存峰值。一张4800万像素的照片未压缩时会占用超过200MB内存。
CGImageswift
import ImageIO
import UIKit
func downsample(data: Data, to pointSize: CGSize, scale: CGFloat = UIScreen.main.scale) -> 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)
}在列表、网格或缩略图场景展示用户选中的照片时请使用该方法,将返回的原始直接传入下采样方法,再创建。
PhotosPickerItemDataUIImageImage Rendering Modes
图片渲染模式
swift
// 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)Use for photos and artwork. Use for icons that should adopt the current tint color.
.original.templateswift
// 原始模式:按照片原始颜色展示
Image("photo")
.renderingMode(.original)
// 模板模式:将图片视为遮罩,使用foregroundStyle设置的颜色渲染
Image(systemName: "heart.fill")
.renderingMode(.template)
.foregroundStyle(.red)照片和美术资源使用模式,需要适配当前主题色的图标使用模式。
.original.templateCommon Mistakes
常见错误
DON'T: Use for photo picking.
DO: Use (SwiftUI) or (UIKit).
Why: is legacy API with limited functionality. runs out-of-process, supports multi-selection, and requires no library permission for browsing.
UIImagePickerControllerPhotosPickerPHPickerViewControllerUIImagePickerControllerPhotosPickerDON'T: Request full photo library access when you only need the user to pick photos.
DO: Use which requires no permission, or request 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.
PhotosPicker.readWriteDON'T: Load full-resolution images into memory for thumbnails.
DO: Use with to downsample.
Why: A 48MP image occupies over 200 MB uncompressed. Loading multiple at full resolution causes memory pressure warnings and termination.
CGImageSourcekCGImageSourceThumbnailMaxPixelSizeDON'T: Block the main thread loading data.
DO: Use in a .
Why: Photo data loading involves disk I/O and potential format conversion. Blocking the main thread causes UI hangs and watchdog kills.
PhotosPickerItemasync loadTransferable(type:)TaskDON'T: Forget to stop when the view disappears.
DO: Call in or .
Why: A running session holds the camera exclusively, preventing other apps from using it, and drains battery continuously.
AVCaptureSessionsession.stopRunning()onDisappeardismantleUIViewDON'T: Assume camera access is granted without checking.
DO: Check and handle and with appropriate UI.
Why: Attempting to add a camera input without authorization silently fails. The user sees a blank preview with no explanation.
AVCaptureDevice.authorizationStatus(for: .video).denied.restrictedDON'T: Call on the main thread.
DO: Dispatch to a background thread with or a dedicated serial queue.
Why: is a synchronous blocking call that can take hundreds of milliseconds while the hardware initializes.
session.startRunning()Task.detachedstartRunning()DON'T: Create inside a .
DO: Own the session in a separate model and pass it to the representable.
Why: runs on every state change. Creating a session there destroys and recreates the capture pipeline repeatedly.
AVCaptureSessionUIViewRepresentable@ObservableupdateUIView不要: 使用实现照片选择功能。
推荐: 使用(SwiftUI)或(UIKit)。
原因: 是功能受限的老旧API,运行在进程外,支持多选,浏览时无需申请相册权限。
UIImagePickerControllerPhotosPickerPHPickerViewControllerUIImagePickerControllerPhotosPicker不要: 仅需要用户选择照片时就申请完整相册权限。
推荐: 使用无需权限的,或申请权限让系统处理受限访问逻辑。
原因: 大多数选图使用场景不需要全量相册访问权限,系统的受限相册选择器既尊重用户隐私,也能保证应用获取选中项的访问权限。
PhotosPicker.readWrite不要: 加载全分辨率图片作为缩略图。
推荐: 使用配合进行下采样。
原因: 4800万像素的图片未压缩时占用超过200MB内存,同时加载多张全分辨率图片会触发内存压力警告甚至应用被系统杀死。
CGImageSourcekCGImageSourceThumbnailMaxPixelSize不要: 阻塞主线程加载数据。
推荐: 在中使用加载。
原因: 照片数据加载涉及磁盘I/O和格式转换,阻塞主线程会导致UI卡顿甚至被看门狗机制杀死。
PhotosPickerItemTaskasync loadTransferable(type:)不要: 视图消失时忘记停止。
推荐: 在或中调用。
原因: 运行中的会话会独占相机,阻止其他应用使用相机,同时持续消耗电量。
AVCaptureSessiononDisappeardismantleUIViewsession.stopRunning()不要: 不检查权限就默认相机访问已授权。
推荐: 检查,针对和状态展示对应的提示UI。
原因: 未授权时添加相机输入会静默失败,用户只会看到空白预览而没有任何提示。
AVCaptureDevice.authorizationStatus(for: .video).denied.restricted不要: 在主线程调用。
推荐: 通过或专门的串行队列调度到后台线程执行。
原因: 是同步阻塞调用,硬件初始化过程可能耗时数百毫秒。
session.startRunning()Task.detachedstartRunning()不要: 在中创建。
推荐: 在独立的模型中持有会话,再传递给代表视图。
原因: 会在每次状态变化时执行,在其中创建会话会导致拍摄管道被反复销毁重建。
UIViewRepresentableAVCaptureSession@ObservableupdateUIViewReview Checklist
评审检查清单
- used instead of deprecated
PhotosPickerUIImagePickerController - Privacy description strings set in Info.plist for camera and/or photo library
- Loading states handled for async image/video loading from
PhotosPickerItem - Large images downsampled with before display
CGImageSource - Camera session properly started on background thread and stopped in
onDisappear - Permission denial handled with Settings deep link and
ContentUnavailableView - Memory pressure considered for multi-photo selection (sequential loading, downsampling)
- owned by a model, not created inside
AVCaptureSessionUIViewRepresentable - Camera preview uses override for automatic resizing
layerClass - included if recording video with audio
NSMicrophoneUsageDescription - Media asset types and picker results are when passed across concurrency boundaries
Sendable
- 使用替代已废弃的
PhotosPickerUIImagePickerController - Info.plist中已配置相机和/或相册的隐私描述字符串
- 已处理异步加载图片/视频的加载态
PhotosPickerItem - 大图片展示前已通过下采样
CGImageSource - 相机会话正确在后台线程启动,并在中停止
onDisappear - 权限被拒绝时已提供跳转设置的入口和提示
ContentUnavailableView - 多选照片场景已考虑内存压力(顺序加载、下采样)
- 由模型持有,而非在
AVCaptureSession中创建UIViewRepresentable - 相机预览通过重写实现自动适配尺寸
layerClass - 如需录制带音频的视频,已添加配置
NSMicrophoneUsageDescription - 跨并发边界传递的媒体资源类型和选择器结果遵循协议
Sendable
References
参考资料
- — Picker patterns, media loading, thumbnail generation, HEIC handling.
references/photospicker-patterns.md - — AVCaptureSession setup, photo/video capture, QR scanning, orientation.
references/camera-capture.md - apple-docs MCP: ,
/documentation/PhotosUI/PhotosPicker/documentation/AVFoundation/AVCaptureSession
- — 选择器模式、媒体加载、缩略图生成、HEIC处理
references/photospicker-patterns.md - — AVCaptureSession配置、照片/视频拍摄、二维码扫描、方向适配
references/camera-capture.md - 苹果官方文档 MCP: ,
/documentation/PhotosUI/PhotosPicker/documentation/AVFoundation/AVCaptureSession