WWDC Quick Look 💓 By SwiftGGTeam
Meet the new Photos picker

Meet the new Photos picker

观看原视频

Highlight

iOS 14 引入了全新的 Photos picker(PHPickerViewController),替代了之前的 UIImagePickerController 和手动请求相册权限然后用 PHAsset 筛选的方式。

核心内容

很多 App 只想完成一个很小的任务:换头像、给聊天消息附一张图、把几张照片导入编辑器。过去常见做法是请求完整照片库权限,然后用 PhotoKit 或旧的 UIImagePickerController 把照片拿进来。这个权限范围太大。用户只是点了一次“选照片”,App 却可能获得整座照片库的访问入口。

iOS 14 的 PHPickerViewController 把这个流程改成系统代办。选择器由系统提供,运行在 App 之外的独立进程中。App 看不到选择器里的其他内容,也不能截取选择器界面;只有用户确认选择的照片和视频会作为结果传回 App。(01:06

这个变化还顺手解决了旧选择器的体验问题。新的 Photos picker 有和系统照片 App 一致的网格、搜索、缩放、多选和选中内容预览。开发者用配置对象限制可选类型和选择数量,再通过 delegate 接收结果。(00:24

Apple 给出的边界很清楚。大多数只需要照片或视频数据的 App,不需要直接使用 PhotoKit。只有非破坏性图片编辑、照片库整理这类必须操作 PHAsset 的 App,才应该请求照片库权限,并且要处理 iOS 14 的 Limited Photos Library。(10:15

详细内容

配置选择范围和多选数量

02:27PHPickerConfiguration 是入口。它控制两个关键决策:用户最多能选多少项,以及选择器里能出现哪些媒体类型。

import PhotosUI

var configuration = PHPickerConfiguration()

// “unlimited” selection by specifying 0, default is 1
configuration.selectionLimit = 0

// Only show images (including Live Photos)
configuration.filter = .images
// Uncomment next line for other example: Only show videos or Live Photos (for their video complement), but no images
// configuration.filter = .any(of: [.videos, .livePhotos])

关键点:

  • import PhotosUI 引入 Photos picker 所在框架。
  • PHPickerConfiguration() 创建一个可变配置对象,后续选择数量和过滤条件都写在这里。
  • selectionLimit 默认是 1;设为 0 表示无限制多选。
  • .images 会显示图片,也包含 Live Photo。
  • .any(of: [.videos, .livePhotos]) 是官方片段里的另一个过滤示例,用来显示视频和 Live Photo 的视频部分。

呈现 picker 并接收结果

03:07)配置完成后,用它初始化 PHPickerViewController。App 自己负责呈现和关闭 picker;用户确认后,delegate 会拿到 [PHPickerResult]

import UIKit
import PhotosUI

class SingleSelectionPickerViewController: UIViewController, PHPickerViewControllerDelegate {
    @IBAction func presentPicker(_ sender: Any) {
        var configuration = PHPickerConfiguration()
        // Only wants images
        configuration.filter = .images

        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self

        // The client is responsible for presentation and dismissal
        present(picker, animated: true)
    }

    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        // The client is responsible for presentation and dismissal
        picker.dismiss(animated: true)

        // Get the first item provider from the results, the configuration only allowed one image to be selected
        let itemProvider = results.first?.itemProvider

        if let itemProvider = itemProvider, itemProvider.canLoadObject(ofClass: UIImage.self) {
            itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
                // TODO: Do something with the image or handle the error
            }
        } else {
            // TODO: Handle empty results or item provider not being able load UIImage
        }
    }
}

关键点:

  • 控制器需要遵守 PHPickerViewControllerDelegate,否则收不到用户选择结果。
  • PHPickerViewController(configuration:) 把前面的配置变成系统选择器界面。
  • picker.delegate = self 指定当前控制器接收选择完成回调。
  • didFinishPickingresults 可能为空;用户取消选择时就会出现这种情况。
  • PHPickerResult 通过 itemProvider 提供数据表示,加载前先用 canLoadObject(ofClass:) 检查类型。
  • loadObject(ofClass:) 是异步 API,回调里要处理错误,并把 UI 更新切回主线程。

多选时按需加载图片

07:34)多选并不要求一次性把所有图片加载进内存。演示 App 只保存返回的 NSItemProvider,再用迭代器按需加载下一张图片。

import UIKit
import PhotosUI

class ViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!

    var itemProviders: [NSItemProvider] = []
    var iterator: IndexingIterator<[NSItemProvider]>?

    @IBAction func presentPicker(_ sender: Any) {
        var configuration = PHPickerConfiguration()
        configuration.filter = .images
        configuration.selectionLimit = 0

        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        present(picker, animated: true)
    }

    func displayNextImage() {
        if let itemProvider = iterator?.next(), itemProvider.canLoadObject(ofClass: UIImage.self) {
            let previousImage = imageView.image
            itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, error in
                DispatchQueue.main.async {
                    guard let self = self, let image = image as? UIImage, self.imageView.image == previousImage else { return }
                    self.imageView.image = image
                }
            }
        }
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        displayNextImage()
    }

}

extension ViewController: PHPickerViewControllerDelegate {

    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        dismiss(animated: true)

        itemProviders = results.map(\.itemProvider)
        iterator = itemProviders.makeIterator()
        displayNextImage()
    }

}

关键点:

  • selectionLimit = 0 打开无限制多选,picker 会显示适合多选的界面和 Add 按钮。
  • itemProviders 只保存数据提供者引用,不提前解码所有图片。
  • iterator 记录当前显示进度,displayNextImage() 每次只处理下一项。
  • previousImageguard 用来避免异步回调覆盖已经变化的 imageView
  • touchesEnded 在演示里用于切到下一张图,真实 App 可以把同样逻辑接到按钮、分页或缩略图列表。
  • results.map(\.itemProvider) 把所有选择结果转换成后续可加载的数据源。

需要 PHAsset 时接回 PhotoKit

11:13)少数 App 需要继续使用 PhotoKit,例如非破坏性编辑或照片库整理。此时可以用 PHPhotoLibrary 初始化配置,并在结果里读取 assetIdentifier,再用标准 PhotoKit API fetch PHAsset

import UIKit
import PhotosUI

class PhotoKitPickerViewController: UIViewController, PHPickerViewControllerDelegate {
    @IBAction func presentPicker(_ sender: Any) {
        let photoLibrary = PHPhotoLibrary.shared()
        let configuration = PHPickerConfiguration(photoLibrary: photoLibrary)
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        present(picker, animated: true)
    }

    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true)

        let identifiers = results.compactMap(\.assetIdentifier)
        let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil)

        // TODO: Do something with the fetch result if you have Photos Library access
    }
}

关键点:

  • PHPhotoLibrary.shared() 取到当前照片库对象,配置对象用它和 PhotoKit 对齐。
  • PHPickerConfiguration(photoLibrary:) 是需要 PHAsset 标识符时使用的初始化方式。
  • results.compactMap(\.assetIdentifier) 过滤掉没有本地资产标识符的结果。
  • PHAsset.fetchAssets(withLocalIdentifiers:options:) 把标识符转换成 PHAsset 查询结果。
  • 注释强调了权限前提:只有已经拥有 Photos Library access 的 App 才能对 fetch 结果做后续处理。
  • Limited Photos Library 不会因为用户在 picker 中选择了更多照片就自动扩大 PHAsset 可访问范围;需要额外授权时,要使用专门的 limited library 管理流程。

核心启发

  1. 做什么:把头像上传改成一次性 Photos picker

    为什么值得做:头像上传只需要一张图片,不该先请求完整照片库权限。PHPickerViewController 可以让用户直接选择图片,App 只拿到这一次选择的结果。

    怎么开始:创建 PHPickerConfiguration(),设置 configuration.filter = .images,保持默认 selectionLimit = 1,在 didFinishPicking 里通过 itemProvider.loadObject(ofClass: UIImage.self) 读取图片。

  2. 做什么:给聊天或笔记 App 增加多图插入

    为什么值得做:session 展示了 picker 的多选、选区预览和 Add 按钮,用户可以一次挑好一组图片。

    怎么开始:把 configuration.selectionLimit 设为 0 或产品允许的数量;回调里保存 results.map(\.itemProvider),按消息发送顺序或插入顺序逐个加载。

  3. 做什么:为图片编辑器建立低权限导入入口

    为什么值得做:很多编辑器只需要用户当前选中的图片数据,不需要浏览整座照片库。picker 返回的 NSItemProvider 已经能加载 UIImage

    怎么开始:先用 PHPickerViewController 做默认导入;如果某个高级功能确实要追踪 PHAsset,再在该功能入口请求照片库访问。

  4. 做什么:给照片库整理类 App 做 PhotoKit 分支

    为什么值得做:整理相册、非破坏性编辑这类 App 需要 PHAsset 和照片库访问状态。session 给出从 picker 结果取 assetIdentifier 再 fetch PHAsset 的路径。

    怎么开始:用 PHPickerConfiguration(photoLibrary:) 初始化选择器;在回调里收集 assetIdentifier;检测用户是否授予 Photos Library access;需要扩大 PHAsset 访问范围时,再接入 session 提到的额外授权流程。

关联 Session

评论

GitHub Issues · utterances