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