在 iOS 應用程式中使用 PhotoKit 與 AVFoundation 實作、審查或改善相片選取、相機拍攝與媒體處理功能。適用於使用 PhotosPicker、PHPickerViewController、相機擷取會話(AVCaptureSession)、圖庫存取、圖片載入與顯示、影片錄製或媒體權限等情境。亦適用於在 Swift 應用程式中從圖庫選取照片、拍照、錄製影片、處理圖片或處理相片/相機隱私權限。
PhotoKit
針對 iOS 16+ 與 Swift 6.3 提供相片選取、相機拍攝、圖片載入以及媒體權限的現代化設計模式。除非另有說明,否則相關模式皆向下相容至 iOS 16。完整選取器範例請參閱 references/photokit-patterns.md,AVCaptureSession 模式請參閱 references/camera-capture.md。
目錄
PhotosPicker (SwiftUI, iOS 16+)
PhotosPicker 是 SwiftUI 用來替代 UIImagePickerController 的原生元件。它在獨立程序(out-of-process)中執行,瀏覽照片時不需要相片圖庫權限,並支援搭配媒體類型篩選進行單選或多選。
單選
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("選擇相片", 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(
"選擇相片",
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 組合進行篩選,限制可選取的媒體類型:
// 僅限圖片
PhotosPicker(selection: $items, matching: .images)
// 僅限影片
PhotosPicker(selection: $items, matching: .videos)
// 僅限原況照片 (Live Photos)
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)]))
使用 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
}
// 使用方式
if let picked = try? await item.loadTransferable(type: PickedImage.self) {
selectedImage = picked.image
}
請務必在 Task 中進行載入,避免阻塞主執行緒。請妥善處理回傳 nil 以及拋出的錯誤 —— 使用者可能會選取無法解碼的格式。
隱私與權限
相片圖庫存取層級
iOS 針對相片圖庫提供兩種存取層級。當應用程式要求 .readWrite 存取權限時,系統會自動顯示有限圖庫選取器(limited-library picker)—— 由使用者決定要分享哪些相片。
| 存取層級 | 說明 | 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
}
}
相機權限
在 Info.plist 中新增 NSCameraUsageDescription。在設定擷取會話(capture session)之前,先檢查並要求存取權限:
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("存取遭拒", systemImage: "lock.shield")
} description: {
Text(message)
} actions: {
Button("開啟設定") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
}
}
}
必要的 Info.plist 鍵值
| 鍵值 | 必要時機 |
|---|---|
NSPhotoLibraryUsageDescription |
從圖庫讀取相片 |
NSPhotoLibraryAddUsageDescription |
儲存相片/影片至圖庫 |
NSCameraUsageDescription |
存取相機 |
NSMicrophoneUsageDescription |
錄製音訊(含聲音的影片) |
若漏掉必要的鍵值,當權限對話框預期要顯示時會導致應用程式崩潰(runtime crash)。
相機拍攝基礎
請在專用的控制器中管理各個擷取會話(capture session),並將設定、startRunning() 以及 stopRunning() 序列化(serialize)在同一個非主執行緒執行器(non-main executor)上。切勿混合主 Actor(main-actor)設定與分離的 start/stop 任務:beginConfiguration()/commitConfiguration() 與會話狀態變更絕不能發生競態條件(race condition)。Representable 視圖僅用於顯示預覽畫面。
最小化相機管理器
完整序列化控制器模式、相片/影片代理(delegates)、對焦、閃光燈/手電筒、方向與掃描功能,請載入 相機拍攝。關鍵生命週期如下:
- 在擷取會話設定事務(transaction)之外要求授權。
- 在擷取執行器(capture executor)上,呼叫
beginConfiguration()並立即加入defer { commitConfiguration() },確保每次提前退出都能平衡事務。 - 務必在
canAddInput/canAddOutput檢查通過後才新增輸入與輸出。 - 在同一個執行器上啟動或停止,且僅在同步呼叫回傳後,才於主 Actor 發布 UI 狀態。
- 發生失敗時,停止執行、還原全新的會話實例(fixture)、修正設定,並重新執行授權、背景/前景、中斷以及拍攝檢查。
在 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 }
}
在 View 中使用相機
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)
}
.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 不會在 View 重新繪製時快取圖片。對於包含大量圖片的正式版應用程式,請使用專用的圖片載入套件,或是基於 URLCache 的快取機制。
降採樣大尺寸圖片
從圖庫載入全解析度相片時,請將其降採樣(downsample)為顯示尺寸的 CGImage,以避免記憶體暴增。一張 48MP 的未壓縮相片可能會消耗超過 200 MB 記憶體。
import ImageIO
import UIKit
func downsample(data: D
<!-- truncated for translation batch; full body continues in source -->




