Highlight
iOS 15 为 PHPicker 带来有序选择、预选照片和加载进度三大改进;同时新增 PHCloudIdentifier 实现跨设备照片同步,并放宽了受限照片库模式下对自定义相册的限制。
核心内容
你的 App 需要让用户选几张照片。以前的做法是请求完整的照片库权限,然后自己写一个照片浏览器。这在 iOS 14 被 PHPicker 改变了:系统提供选择器,App 只能拿到用户选中的照片,隐私得到保护。
iOS 15 在此基础上做了四件事。
第一件事是隐私透明化。(01:15)用户在设置里看到”仅使用系统照片选择器”的说明,知道你的 App 只能访问选中的照片。这增加了信任度,也鼓励开发者用系统选择器替代自定义实现。
第二件事是有序选择。(02:02)用户选多张照片时,可以控制顺序。选择器会显示 1、2、3 的序号,而不是简单的对勾。这对拼图、幻灯片、社交媒体多图发布等场景很有用。
第三件事是预选照片。(02:45)用户之前选了 3 张照片,再次打开选择器时,这 3 张默认处于选中状态。用户可以取消选择或追加新照片。这解决了”重新选择时不知道之前选了什么”的痛点。
第四件事是加载进度。(06:29)如果用户开启了 iCloud 照片优化存储,照片可能不在本地。以前只能显示一个转圈,现在可以通过 NSProgress 获取真实的下载进度,给用户更明确的反馈。
对于需要深度集成照片库的 App,iOS 15 还带来了 PHCloudIdentifier。(08:58)用户在 iPhone 上编辑了一组照片,打开 iPad 版 App 时,这些照片应该自动出现。问题是每个设备的本地标识符不同。PHCloudIdentifier 提供了一种跨设备查找同一照片的机制,不需要你处理 iCloud 同步的细节。
最后,受限照片库模式也有改进。(15:10)iOS 14 的受限模式下 App 无法创建或访问自定义相册,iOS 15 解除了这个限制。App 可以创建自己的相册,并在受限模式下正常读写。同时新增了 presentLimitedLibraryPicker API,可以主动弹出选择器让用户添加更多照片,并通过 completion handler 获取新增的照片标识符。
详细内容
配置有序选择和预选照片
import PhotosUI
var existingSelection: [PHPickerResult] = []
func presentPicker() {
var config = PHPickerConfiguration(photoLibrary: .shared())
config.selectionLimit = 0 // 0 表示无限制
config.selection = .ordered // 有序选择
// 预选中之前已选的照片
let previousIdentifiers = existingSelection.compactMap { $0.assetIdentifier }
config.preselectedAssetIdentifiers = previousIdentifiers
let picker = PHPickerViewController(configuration: config)
picker.delegate = self
present(picker, animated: true)
}
关键点:
selection = .ordered启用有序选择,选择器显示数字序号preselectedAssetIdentifiers传入之前选中的 asset identifier 数组- 配置必须用
PHPickerConfiguration(photoLibrary:)初始化,才能获取 asset identifier - 默认的
selection = .default只显示对勾,不显示顺序
处理选择结果并合并新旧数据
extension ViewController: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
var updatedSelection: [PHPickerResult] = []
for result in results {
if let existing = existingSelection.first(where: {
$0.assetIdentifier == result.assetIdentifier
}) {
// 预选的且未被取消的:使用旧结果(包含 item provider)
updatedSelection.append(existing)
} else {
// 新选中的:使用新结果
updatedSelection.append(result)
}
}
existingSelection = updatedSelection
// 加载图片数据
for result in updatedSelection {
result.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
// 处理图片
}
}
}
}
关键点:
- 预选照片的返回结果中,
itemProvider是空的,因为没有实际数据需要传输 - 需要用旧的 picker result 替换预选结果的 empty item provider
- 被取消选择的照片不会出现在 results 中,自然被过滤掉
- 如果用户点击取消,results 只包含预选的 asset identifier,所有 item provider 为空
显示加载进度
for result in results {
let progress = result.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
// 加载完成
}
// 用 NSProgress 更新 UI
progress.observeValue(forKeyPath: "fractionCompleted", options: .new) { _, _, _ in
DispatchQueue.main.async {
self.progressView.progress = Float(progress.fractionCompleted)
}
}
}
关键点:
loadObject返回NSProgress对象,可以观察下载进度- 照片存储在 iCloud 且本地未下载时,进度从 0 开始逐步增加
- 本地已有的照片会立即完成,进度直接跳到 1.0
用 PHCloudIdentifier 跨设备同步照片
// 源设备:获取 cloud identifier
let localIdentifiers = selectedAssets.map { $0.localIdentifier }
PHPhotoLibrary.shared().cloudIdentifierMappings(forLocalIdentifiers: localIdentifiers) { mappings in
var cloudIDs: [PHCloudIdentifier] = []
for (localID, mapping) in mappings {
if let cloudID = mapping.cloudIdentifier {
cloudIDs.append(cloudID)
} else if let error = mapping.error {
print("Mapping failed for \(localID): \(error)")
}
}
// 将 cloudIDs 序列化后通过 CloudKit 同步
let archivedData = try? NSKeyedArchiver.archivedData(withRootObject: cloudIDs, requiringSecureCoding: true)
// 上传到 CloudKit...
}
关键点:
cloudIdentifierMappings(forLocalIdentifiers:)返回本地标识符到 cloud identifier 的映射- 映射可能失败,需要处理
identifierNotFound和multipleIdentifiersFound两种错误 PHCloudIdentifier可以序列化为字符串,通过网络或 CloudKit 同步
// 目标设备:用 cloud identifier 查找本地照片
let cloudIdentifiers: [PHCloudIdentifier] = // 从 CloudKit 下载并反序列化
PHPhotoLibrary.shared().localIdentifierMappings(forCloudIdentifiers: cloudIdentifiers) { mappings in
var localIDs: [String] = []
for (cloudID, mapping) in mappings {
if let localID = mapping.localIdentifier {
localIDs.append(localID)
} else if let error = mapping.error {
if let multipleIDs = error.userInfo[PHLocalIdentifiersErrorKey] as? [String] {
// 多个匹配,让用户选择
}
}
}
let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: localIDs, options: nil)
// 显示照片
}
关键点:
localIdentifierMappings(forCloudIdentifiers:)反向查找本地标识符multipleIdentifiersFound错误时,userInfo[PHLocalIdentifiersErrorKey]包含所有匹配的本地标识符- 映射操作开销较大,建议在加载和保存时执行,日常交互使用本地标识符
受限照片库模式下管理相册
// 在受限模式下创建自定义相册
PHPhotoLibrary.shared().performChanges {
let creationRequest = PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: "My App Photos")
let placeholder = creationRequest.placeholderForCreatedAssetCollection
} completionHandler: { success, error in
// 处理结果
}
关键点:
- iOS 15 起,受限照片库模式下可以创建、获取和更新自定义相册
- App 自己创建的照片会自动加入受限选择集
- 需要深度集成的场景仍然建议引导用户授予完整访问权限
主动弹出受限照片选择器
// 让用户添加更多照片到受限选择集
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self) { identifiers in
// identifiers 是用户新添加的照片标识符
let newAssets = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil)
// 处理新增照片
}
关键点:
presentLimitedLibraryPicker主动弹出系统选择器- completion handler 返回用户新增的照片标识符,不是全部已选照片
- 适合在”添加更多照片”的按钮点击时调用
核心启发
-
社交 App 的多图有序发布
- 做什么:让用户在选择多张照片时控制发布顺序,第一张是封面,后面的按序号排列
- 为什么值得做:
selection = .ordered一行代码就能实现,用户选图时直接看到 1、2、3 的序号 - 怎么开始:设置
config.selection = .ordered,读取 results 数组的顺序即为用户指定的顺序
-
跨设备照片日记同步
- 做什么:用户在 iPhone 上创建的照片日记,在 iPad 和 Mac 上打开时自动加载相同的照片
- 为什么值得做:
PHCloudIdentifier处理了 iCloud 同步的复杂性,你只需要保存标识符并同步到 CloudKit - 怎么开始:用
cloudIdentifierMappings获取 cloud ID,同步到 CloudKit 数据库,目标设备用localIdentifierMappings反查
-
照片编辑项目的跨设备续作
- 做什么:照片编辑 App 让用户在 iPhone 上开始编辑,在 iPad 上继续完成
- 为什么值得做:编辑项目的元数据(滤镜参数、裁剪框)很小,通过 CloudKit 同步;照片本身通过 PHCloudIdentifier 定位
- 怎么开始:保存编辑参数 + 照片的 cloud identifier 到 CloudKit,目标设备反查后恢复编辑状态
-
受限模式下的智能相册管理
- 做什么:在受限照片库模式下,App 自动创建相册保存自己生成的内容(比如拼图结果、滤镜照片)
- 为什么值得做:iOS 15 解除了受限模式下无法创建相册的限制,用户不需要授予完整权限也能正常使用
- 怎么开始:检测当前权限状态,受限模式下正常调用
creationRequestForAssetCollection,创建后自动加入用户的选择集
-
带进度条的照片选择体验
- 做什么:选择 iCloud 照片时显示真实的下载进度,而不是无限转圈
- 为什么值得做:
loadObject返回的NSProgress可以直接绑定到 UIProgressView,体验提升明显 - 怎么开始:监听
fractionCompleted属性,更新进度条,加载完成后隐藏
关联 Session
- Meet the new Photos picker — WWDC 2020 引入 PHPicker 的基础介绍
- Handle the Limited Photos Library in your app — WWDC 2020 讲解受限照片库的处理方式
- What’s new in Camera Capture — 相机捕获的新功能,与照片处理紧密相关
评论
GitHub Issues · utterances