【OC】PHPickerViewController

PHPickerViewController

文章目录

前言

在做 3GShare 仿写项目的上传页时,我需要实现一个多图选择功能,最开始我用的是UIImagePickerController,但很快发现它只能选一张图片,而且每次都会弹出相册权限请求

因此在这里我要介绍的是iOS 14 推出的 PHPickerViewController,不仅支持多选,还不需要申请相册权限

UIImagePickerController VS PHPickerViewController

我们先看看旧的API有哪些问题以至于要换掉它

UIImagePickerController 的三个痛点:

  1. 必须申请相册权限
  2. 只能选一张图片,想选多张只能多次调用,逻辑复杂
  3. iOS 14 已经软废弃,苹果明确推荐迁移到 PHPickerViewController

相比之下,PHPickerViewController 解决了所有这些问题:

PHPickerViewController 的优势:

  1. 不需要相册权限(系统进程处理,App 拿不到其他图片)
  2. 原生支持多选
  3. 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 涉及三个核心类

  1. PHPickerConfiguration - 配置选择器(选几张、选什么类型)
  2. PHPickerViewController - 选择器本身
  3. 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];
                });
            }
        }];
    }
}

‼️但是这会导致两个问题

  1. 顺序问题

由于loadObjectOfClass是异步加载,所以每一张照片加载结束的时间不确定,先加载结束的会先保存到数组,这样就不会按照用户选择的顺序保存

  1. 不知道什么时候全部加载完毕

每一张都异步加载,无法知道"所有图片都加载完了"这个时机,那么这会有什么问题呢?

由于我们不知道全部加载完的时间然后一次性刷新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 解决多任务同步

  1. 顺序问题

对于这个问题其实我们加上一个索引值就可以解决,在加载的开头规定索引,后续在保存在数组的时候不采用addObject:在末尾插入,而是用索引添加

  1. 不知道什么时候全部加载完毕

既然一张一张加载有问题,那我们在全部加载完再刷新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
    });
}
相关推荐
AI行业学习2 小时前
CC-Switch 下载、安装windows\macOS \Linux 安装
linux·运维·macos
一个人旅程~4 小时前
完整精华版macbookHighSierra 至Montere-Ventyra版本跨越评估与避坑指南(含证书有效期)
windows·经验分享·macos·电脑
秋雨梧桐叶落莳5 小时前
iOS——抽屉视图详解
开发语言·macos·ui·ios·objective-c·cocoa
库奇噜啦呼6 小时前
【iOS】源码学习-方法交换
学习·ios·cocoa
pumpkin8451416 小时前
Mac Studio M4 Max 纯本地化部署 Qwen 3.6 并桥接 Claude Code 实践指南
macos
hurrycry_小亦18 小时前
苹果WWDC 2026前瞻:Ferret-Pro端侧大模型即将亮相|小亦之闻|AI 编程三日速递!(5月26日~5月28日)
macos·ios·wwdc
搬砖的小码农_Sky21 小时前
macOS Sequoia OpenClaw + Ollama 本地离线部署(免API、Apple Silicon金属加速)
人工智能·macos·ai·人机交互
稚枭天卓1 天前
mac 安装 redis
redis·macos
AugustRed1 天前
MacOS 运维常用命令大全(超全速查表)
运维·macos