Improve access to Photos in your app
Photos & Camera 进阶 24m

改善你的 app 对照片库的访问

Improve access to Photos in your app

2021年6月10日

在 Apple 官方观看视频

一句话判断

iOS 15 对照片权限模型做了细粒度拆分——用户可以选择只授权「选中的照片」而不是整个相册,你的 app 必须适配这个变化,否则会在权限检查处直接崩溃。

这场 Session 讲了什么

Photos 框架在 iOS 15 经历了一次重要的权限模型变更。之前的「全部照片 / 无权限」二元选择被细化为三级:全部照片、选中的照片、无权限。「选中的照片」是一个全新的权限级别——用户可以选择只把相册中的几张照片授权给你的 app。

这意味着 PHPhotoLibrary.authorizationStatus() 现在可能返回 .limited。如果你的代码只检查了 .authorized 和 .denied,在 .limited 状态下会出现问题。

Session 详细讲解了如何处理 .limited 权限。使用 PHAsset.fetchAssets 时,系统只会返回用户选中的那些照片。如果你的 app 需要访问更多照片,需要引导用户到系统设置中修改权限。

另一个重要更新是 PHPickerConfiguration 的改进。iOS 15 允许你在 picker 中设置 selection 属性来预设已选中的照片,还可以配置 selectionPolicy 来控制多选行为。

值得深挖的点

Limited 权限下的数据一致性问题

当用户授予 .limited 权限时,PHAsset.fetchAssets 返回的结果只包含选中的照片。但问题是:用户可能随时在系统设置中增减授权的照片。这意味着你之前 fetch 的结果可能已经过期。Session 建议使用 PHPhotoLibrary.register() 监听权限变化通知,在用户修改授权范围时重新 fetch。

PHPicker 与直接访问的选择

如果你的 app 只需要让用户选择几张照片(比如头像上传),用 PHPickerViewController 就够了——它不需要任何照片权限。只有当你需要浏览整个相册、读取照片元数据或者做批量操作时,才需要请求 PHPhotoLibrary 权限。

代码片段

// 正确处理所有权限级别
import Photos

class PhotoAccessManager {
    let library = PHPhotoLibrary.shared()
    
    func requestAccess() async -> Bool {
        // 使用新的 async API 请求权限
        let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
        return handleStatus(status)
    }
    
    func handleStatus(_ status: PHAuthorizationStatus) -> Bool {
        switch status {
        case .authorized:
            // 用户授权了全部照片
            return true
        case .limited:
            // 用户只授权了选中的照片
            // fetchAssets 只返回选中的照片
            return true
        case .denied, .restricted:
            // 没有权限,需要引导用户去设置
            showPermissionDeniedAlert()
            return false
        case .notDetermined:
            return false
        @unknown default:
            return false
        }
    }
    
    func showPermissionDeniedAlert() {
        guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
        if UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url)
        }
    }
}
// 使用 PHPicker 选择照片(不需要权限)
import PhotosUI

class PhotoPicker: NSObject, PHPickerViewControllerDelegate {
    func presentPicker(from viewController: UIViewController) {
        var config = PHPickerConfiguration()
        config.selectionLimit = 5  // 最多选 5 张
        config.filter = .images    // 只显示图片
        
        let picker = PHPickerViewController(configuration: config)
        picker.delegate = self
        viewController.present(picker, animated: true)
    }
    
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true)
        
        for result in results {
            let provider = result.itemProvider
            
            if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
                provider.loadObject(ofClass: UIImage.self) { image, error in
                    guard let image = image as? UIImage else { return }
                    DispatchQueue.main.async {
                        // 处理选中的图片
                    }
                }
            }
        }
    }
}
// 监听权限变化
func registerForChanges() {
    PHPhotoLibrary.shared().register(self)
}

// PHPhotoLibraryChangeObserver
func photoLibraryDidChange(_ changeInstance: PHChange) {
    // 用户可能修改了授权范围,需要重新获取照片
    DispatchQueue.main.async {
        self.reloadPhotos()
    }
}

最佳实践

如果你的 app 之前只检查 .authorized,现在必须加上 .limited 的处理。在 .limited 状态下,PHAsset.fetchAssets 不会返回错误,只是结果集变小了。最容易出问题的地方是:你的 UI 假设「相册为空」意味着用户没有照片,但实际上可能是用户只授权了部分照片。

推荐使用 PHPickerViewController 作为默认的图片选择方案。只有当你需要创建自定义的照片浏览器、需要访问照片元数据(位置、日期、EXIF)或者需要编辑照片时,才请求完整的照片权限。

还有什么值得关注

  • iOS 15 允许用户在「设置 > 隐私 > 照片」中为每个 app 单独管理授权的照片列表。
  • PHPickerConfiguration 新增了 selection 属性,可以预选照片。
  • PHAsset.creationDate 和 PHAsset.modificationDate 现在有更精确的子秒级时间戳。
WWDC 2021