PHPickerViewController
文章目录
- PHPickerViewController
前言
在做 3GShare 仿写项目的上传页时,我需要实现一个多图选择功能,最开始我用的是UIImagePickerController,但很快发现它只能选一张图片,而且每次都会弹出相册权限请求
因此在这里我要介绍的是iOS 14 推出的 PHPickerViewController,不仅支持多选,还不需要申请相册权限
UIImagePickerController VS PHPickerViewController
我们先看看旧的API有哪些问题以至于要换掉它
UIImagePickerController 的三个痛点:
- 必须申请相册权限
- 只能选一张图片,想选多张只能多次调用,逻辑复杂
- iOS 14 已经软废弃,苹果明确推荐迁移到 PHPickerViewController
相比之下,PHPickerViewController 解决了所有这些问题:
PHPickerViewController 的优势:
- 不需要相册权限(系统进程处理,App 拿不到其他图片)
- 原生支持多选
- UI 就是系统相册,用户熟悉
基本用法
一、 引入框架
objc
#import <PhotosUI/PhotosUI.h>
// 遵守协议
@interface UploadVC () <PHPickerViewControllerDelegate>
二、 配置并展示选择器
objc
- (void)selectPhoto {
// 配置选择器
PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
// 只显示图片
config.filter = [PHPickerFilter imagesFilter];
// 最多选9张,0表示不限制
config.selectionLimit = 9;
// 展示图片选择器
PHPickerViewController *picker = [[PHPickerViewController alloc]
initWithConfiguration:config];
picker.delegate = self;
[self presentViewController:picker animated:YES completion:nil];
}
三、 实现代理方法
objc
// 用户完成选择后调用
- (void)picker:(PHPickerViewController *)picker
didFinishPicking:(NSArray<PHPickerResult *> *)results {
// 先关闭选择器
[picker dismissViewControllerAnimated:YES completion:nil];
// 在这里处理选择结果
}
基础的使用过程就是这些,其中涉及到三个重要的核心类,我会一一介绍
三个核心类
PHPickerViewController 涉及三个核心类
- PHPickerConfiguration - 配置选择器(选几张、选什么类型)
- PHPickerViewController - 选择器本身
- PHPickerResult - 选择结果(每张图片对应一个)
我们在使用的时候用 Configuration 配置好参数,创建 ViewController 展示给用户,用户选完后在代理方法里拿到 Result 数组
PHPickerConfiguration
objc
// 配置选择器
PHPickerConfiguration *config = [[PHPickerConfiguration alloc] init];
// 1. 限制选择数量
config.selectionLimit = 0; // 0 = 不限制
config.selectionLimit = 1; // 只能选1张
config.selectionLimit = 9; // 最多选9张
// 2. 过滤类型
config.filter = [PHPickerFilter imagesFilter]; // 只显示图片
config.filter = [PHPickerFilter videosFilter]; // 只显示视频
config.filter = [PHPickerFilter livePhotosFilter]; // 只显示 Live Photo
// 组合过滤(图片+视频都显示)
config.filter = [PHPickerFilter anyFilterMatchingSubfilters:@[
[PHPickerFilter imagesFilter],
[PHPickerFilter videosFilter]
]];
PHPickerResult
每张选中的图片对应一个 PHPickerResult:
objc
// PHPickerResult 有两个重要属性
result.itemProvider // NSItemProvider,用来加载实际图片数据
result.assetIdentifier // 图片在相册里的唯一标识符(可能为 nil)
result数组里并不会直接存储UIImage,而是图片的"引用/票据"
我们必须调用itemProvider.loadObjectOfClass才能拿到真正的图片数据
所以我们在选完图片之后还需要"加载"图片
理解了这一点,我们就会想那直接一张一张加载就可以了,比如
objc
- (void)picker:(PHPickerViewController *)picker
didFinishPicking:(NSArray<PHPickerResult *> *)results {
[picker dismissViewControllerAnimated:YES completion:nil];
for (PHPickerResult *result in results) {
// 加载图片
[result.itemProvider loadObjectOfClass:[UIImage class]
completionHandler:^(UIImage *image, NSError *error) {
if (image) {
// 后面会讲到
dispatch_async(dispatch_get_main_queue(), ^{
// 保存到数组里
// 多线程同时追加会崩溃,加锁保护
@synchronized (newImages) {
[self.selectedImages addObject:image];
}
// 更新 UI
[self refreshPhotoView];
});
}
}];
}
}
‼️但是这会导致两个问题
- 顺序问题
由于loadObjectOfClass是异步加载,所以每一张照片加载结束的时间不确定,先加载结束的会先保存到数组,这样就不会按照用户选择的顺序保存
- 不知道什么时候全部加载完毕
每一张都异步加载,无法知道"所有图片都加载完了"这个时机,那么这会有什么问题呢?
由于我们不知道全部加载完的时间然后一次性刷新UI,所以这里我们采用一张一张加载的方法会导致加载完一张就保存一张,每保存一张就要刷新一次UI,就会导致UI一直闪烁,而且每次刷新其实也会重建之前的所有UIImageView浪费性能
异步加载
在介绍这些问题解决方案前,我们先来介绍一下上面反复提到的异步加载
相对的同步加载是指一行一行执行,比如
objc
// 同步:一行一行等着执行(会卡住)
UIImage *image = [某个耗时操作]; // 等它完成,期间 App 冻结
NSLog(@"完成了"); // 然后才执行这里
而异步加载其实就是在后台加载
objc
// 异步:发出请求,不等,完成了再通知你(不会卡住)
[某个耗时操作 完成后:^(UIImage *image) {
// 操作完成了,系统调用这里(回调)
// 但不知道是什么时候
}];
NSLog(@"我不等,直接执行"); // 这行立刻执行,不等上面完成
上面加载图片用到的loadObjectOfClass 就是异步的:你调用它,它立刻返回,然后在后台线程去加载图片,加载完了调用你的 block(回调)
objc
[result.itemProvider loadObjectOfClass:[UIImage class]
completionHandler:^(UIImage *image, NSError *error) {
// ⚠️ 由于是异步加载所以这个回调在后台线程执行!
// 如果直接在这里操作 UI → 可能崩溃或显示异常
// ✅ 必须切回主线程才能更新 UI
// dispatch_get_main_queue() 就是主线程/主队列
// dispatch_async就是把任务提交到指定队列里异步执行
dispatch_async(dispatch_get_main_queue(), ^{
[self.imageView setImage:image];
});
}];
理解了这些我们来看解决方案
dispatch_group 解决多任务同步
- 顺序问题
对于这个问题其实我们加上一个索引值就可以解决,在加载的开头规定索引,后续在保存在数组的时候不采用addObject:在末尾插入,而是用索引添加
- 不知道什么时候全部加载完毕
既然一张一张加载有问题,那我们在全部加载完再刷新UI,所以我们就要解决全部加载结束的时间问题
dispatch_group是一个计数器
GCD(Grand Central Dispatch) 中用来管理一组异步任务的工具
objc
// 1. 创建一个 group(计数器)
dispatch_group_t group = dispatch_group_create();
// 2. 任务开始前:进入group,告诉 group:"我要开始一个任务了"(计数 +1)
dispatch_group_enter(group);
// 3. 任务完成后:离开 group,告诉 group:"这个任务完成了"(计数 -1)
dispatch_group_leave(group);
// 4. 等所有任务完成(计数变0)后,在指定队列执行 block
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 全部完成后做这件事
});
group 内部计数:
enter → 计数变1
enter → 计数变2
enter → 计数变3
leave → 计数变2
leave → 计数变1
leave → 计数变0 → 触发 notify ✅
我们直接来看完整的代码实现
objc
- (void)picker:(PHPickerViewController *)picker
didFinishPicking:(NSArray<PHPickerResult *> *)results {
[picker dismissViewControllerAnimated:YES completion:nil];
if (results.count == 0) return;
// 创建计数器
dispatch_group_t group = dispatch_group_create();
// 用索引占位,保证图片顺序
NSMutableArray *newImages = [NSMutableArray arrayWithCapacity:results.count];
for (int i = 0; i < results.count; i++) {
[newImages addObject:[NSNull null]]; // 先占位
}
// 并发加载所有图片
for (int i = 0; i < results.count; i++) {
// 告诉计数器:开始一个任务(计数 +1)
dispatch_group_enter(group);
NSInteger index = i; // 保存索引,保证图片放到对应位置
[results[i].itemProvider loadObjectOfClass:[UIImage class]
completionHandler:^(UIImage *image, NSError *error) {
// 这里在后台线程执行
if (image) {
// 用索引赋值,不同线程写不同位置,不会冲突
newImages[index] = image;
}
// 告诉计数器:这个任务完成了(计数 -1)
dispatch_group_leave(group);
}];
// 注意:这里不等图片加载完,立刻去循环下一张
// 三张图片是同时开始加载的!
}
// 等所有任务完成(计数变0),在主线程执行
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 所有图片都加载完了,而且顺序正确
[self.selectedImages addObjectsFromArray:newImages];
[self refreshPhotoView]; // 刷新 UI
});
}