WWDC Quick Look 💓 By SwiftGGTeam
Improve access to Photos in your app

Improve access to Photos in your app

观看原视频

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 的映射
  • 映射可能失败,需要处理 identifierNotFoundmultipleIdentifiersFound 两种错误
  • 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 返回用户新增的照片标识符,不是全部已选照片
  • 适合在”添加更多照片”的按钮点击时调用

核心启发

  1. 社交 App 的多图有序发布

    • 做什么:让用户在选择多张照片时控制发布顺序,第一张是封面,后面的按序号排列
    • 为什么值得做selection = .ordered 一行代码就能实现,用户选图时直接看到 1、2、3 的序号
    • 怎么开始:设置 config.selection = .ordered,读取 results 数组的顺序即为用户指定的顺序
  2. 跨设备照片日记同步

    • 做什么:用户在 iPhone 上创建的照片日记,在 iPad 和 Mac 上打开时自动加载相同的照片
    • 为什么值得做PHCloudIdentifier 处理了 iCloud 同步的复杂性,你只需要保存标识符并同步到 CloudKit
    • 怎么开始:用 cloudIdentifierMappings 获取 cloud ID,同步到 CloudKit 数据库,目标设备用 localIdentifierMappings 反查
  3. 照片编辑项目的跨设备续作

    • 做什么:照片编辑 App 让用户在 iPhone 上开始编辑,在 iPad 上继续完成
    • 为什么值得做:编辑项目的元数据(滤镜参数、裁剪框)很小,通过 CloudKit 同步;照片本身通过 PHCloudIdentifier 定位
    • 怎么开始:保存编辑参数 + 照片的 cloud identifier 到 CloudKit,目标设备反查后恢复编辑状态
  4. 受限模式下的智能相册管理

    • 做什么:在受限照片库模式下,App 自动创建相册保存自己生成的内容(比如拼图结果、滤镜照片)
    • 为什么值得做:iOS 15 解除了受限模式下无法创建相册的限制,用户不需要授予完整权限也能正常使用
    • 怎么开始:检测当前权限状态,受限模式下正常调用 creationRequestForAssetCollection,创建后自动加入用户的选择集
  5. 带进度条的照片选择体验

    • 做什么:选择 iCloud 照片时显示真实的下载进度,而不是无限转圈
    • 为什么值得做loadObject 返回的 NSProgress 可以直接绑定到 UIProgressView,体验提升明显
    • 怎么开始:监听 fractionCompleted 属性,更新进度条,加载完成后隐藏

关联 Session

评论

GitHub Issues · utterances