ImagePickerController(图片选择器)是 iOS 开发中一个很常用的 UI 控件。作为摄影爱好者,而 Google 出品的摄影后期 App,Snapseed 中那个图片选择器,好看又实用,便尝试着仿写了一个。

集成后一行代码即可实现下图效果。 实现原理从界面说起。 界面可以分解为一个背景图层用于点击关闭(空白处点击消失),一个横向可滚动的 Collectionview 用于显示图库里的图片,一个 Tableview 用于存放功能按钮。

1.功能按钮的 Tableview

先从简单的 Tableview说起,从小模块到大模块应该是以下方面。

自定义 item 对象,item 对象定义了 Action 的显示名字,类型,图片

@property(nonatomic, copy) NSString *actionTitle;
@property(nonatomic, strong) UIImage *actionImage;
@property(nonatomic, assign) CDZImagePickerActionType actionType;

其中 CDZImagePickerActionType 可用来定义按钮动作类型,可以用枚举来定义

typedef NS_ENUM(NSInteger, CDZImagePickerActionType) {
    CDZImagePickerCameraAction,
    CDZImagePickerLibraryAction,
    CDZImagePickerRecentAction,
    CDZImagePickerCloseAction
};

定义一个 init 方法用来快捷初始化 item 对象。

- (instancetype)initWithTitle:(NSString *)titele withActionType:(CDZImagePickerActionType)type withImage:(UIImage *)image{
    self = [super init];
    if (self) {
        _actionTitle = titele;
        _actionType = type;
        _actionImage = image;
    }
    return self;
}

自定义 cell 对象,封装一个方法来对应 item 和 cell 的关系

- (void)setCellFromItem:(CDZImagePickerActionsItem *)item{
    self.textLabel.text = item.actionTitle;
    self.imageView.image = item.actionImage;
}

可以重写 layoutSubviews 方法达到 cell 中文字,图片样式自定义的效果,比如可以写有图片和无图片的样式布局

- (void)layoutSubviews{
    [super layoutSubviews];
    self.selectionStyle = UITableViewCellSelectionStyleNone;//点击不变色
    self.imageView.frame = CGRectMake(20, 15, 22, 22);
    self.imageView.contentMode = UIViewContentModeScaleAspectFit;
    self.textLabel.font = [UIFont systemFontOfSize:16.0f];
    self.textLabel.textColor = [UIColor colorWithRed:0.30 green:0.30 blue:0.30 alpha:1.00];
    if (self.imageView.image){
        self.textLabel.frame = CGRectMake(60, 2, 200, 48);
        self.textLabel.textAlignment = NSTextAlignmentLeft;
    }
    else{
        self.textLabel.textAlignment = NSTextAlignmentCenter;
    }
}

自定义 datasource,这一步可以根据个人选择,个人习惯是把 datasource 从 Viewcontroller 里抽离,可以让 Controller更少代码,datasource 有数组对象用于存放 item @property (nonatomic, strong) NSMutableArray *itemArray;

实现 datasource 方法。

#pragma mark - tableViewDataSourceRequried
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.itemArray.count;
}
		

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    CDZImagePickerActionsItem *item = self.itemArray[indexPath.row];
    CDZImagePickerActionsCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([CDZImagePickerActionsCell class])];
    if (!cell) {
        cell = [[CDZImagePickerActionsCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass([CDZImagePickerActionsCell class])];
    }
    [cell setCellFromItem:item];//将 cell 和 item 对应起来
    return cell;
}

实现 delegate 和点击事件

#pragma mark - tableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return actionsViewCellHeight;
}


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    CDZImagePickerActionsItem *item = self.actionArray[indexPath.row];
    [self doActionsWithType:item.actionType];
}
#pragma mark - actions
- (void)doActionsWithType:(CDZImagePickerActionType)type{
    switch (type) {
        case  CDZImagePickerCameraAction:
            [self openCamera];
            break;
        case   CDZImagePickerRecentAction:
            [self openRecentImage];
            break;
        case CDZImagePickerLibraryAction:
            [self openLibrary];
            break;
        case CDZImagePickerCloseAction:
            [self closeAction];
            break;
    }   
}

- (void)openCamera{
    if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]){
        UIImagePickerController *pickerController = [[UIImagePickerController alloc]init];
        pickerController.delegate = self;
        pickerController.sourceType = UIImagePickerControllerSourceTypeCamera;
        [self presentViewController:pickerController animated:NO completion:nil];
        NSLog(@"打开相机");
    }
}

- (void)openLibrary{
    UIImagePickerController *pickerController = [[UIImagePickerController alloc]init];
    pickerController.delegate = self;
    pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    [self presentViewController:pickerController animated:NO completion:nil];
    NSLog(@"打开图库");
    
}

- (void)closeAction{
    [self dismissViewControllerAnimated:YES completion:nil];
    NSLog(@"关闭按钮");
}


- (void)openRecentImage{
    [[PHImageManager defaultManager]requestImageForAsset:self.photosDataSource.itemArray[0] targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFill options:nil resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        self.resultImage = result;
        [self dismissViewControllerAnimated:YES completion:nil];
        NSLog(@"打开最新图片");
    }];
}

实现 imagepicker 的 delegate(记得实现 UINavigationControllerDelegate,不可只实现 UIImagePickerControllerDelegate,会报错

#pragma mark - imagePickerController delegate

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(nonnull NSDictionary<NSString *,id> *)info{
    UIImage *image = info[UIImagePickerControllerOriginalImage];
    if(picker.sourceType == UIImagePickerControllerSourceTypeCamera){
        UIImageWriteToSavedPhotosAlbum(image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);
    }
    self.resultImage = image;
    [picker dismissViewControllerAnimated:NO completion:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
    NSLog(@"从相机或图库获取图片");
}

iOS 10 还要检查一下相机和图库的权限,记得在 info.plist 中添加两行,不然会崩溃 最后在是 Tableview 初始化与绘制,在 datasource 初始化自己需要的按钮 item 数组(acctionArray)

- (CDZImagePickerActionsDataSource *)actionsDataSource{
    if (!_actionsDataSource) {
        _actionsDataSource = [[CDZImagePickerActionsDataSource alloc]init];
        _actionsDataSource.itemArray = self.actionArray;
    }
    return _actionsDataSource;
}
- (UITableView *)actionView{
    if (!_actionView) {
        CGFloat actionsViewHeight = actionsViewCellHeight * self.actionArray.count;
        _actionView = [[UITableView alloc]initWithFrame:CGRectMake(0,SCREEN_HEIGHT - actionsViewHeight ,SCREEN_WIDTH, actionsViewHeight) style:UITableViewStylePlain];
        _actionView.scrollEnabled = NO; //不需要滑动
        _actionView.separatorStyle = UITableViewCellSeparatorStyleNone; //分割线去除
        _actionView.delegate = self;
        _actionView.dataSource = self.actionsDataSource;
    }
    return _actionView;
}

2.展示照片的 Collecionview

Collecionview 的实现思路和 Tableview 类似

cell 的自定义主要是重写其 initWithFrame 方法添加 imageview(注意 Collectionviewcell 只用 initWithFrame 方式初始化,Tableviewcell 初始化方法有多种),并在 layoutSubviews 里改变 Imageview 的大小,和 Tableview 类似写一个 setCellFromItem 的方法解析 item 数据。而 PhotosKit 获取的图片对象为 PHAsset,用 PHImageManage 的方法根据将其解析为 size 为 cell 的大小 UIImage,加快加载速度,按需加载(关于 PhotosKit 的优点不再赘述,iOS 8 以后苹果只保留了 PhotosKit 用于获取系统图片)

PhotosKit 获取时,注意两个地方,targetSize 传入的是 pixel,而不是平时用的 size,且 option 里的 resizeMode 默认为 None(不缩放),不重写 resizeMode 属性那么只会抓取不缩放的图,在意质量可以设置 resizeMode 为 Exact(精确),追求速度可以设置为 Fast(这样不会完全按照设置的 targetSize 去获取图片)

- (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
        [self.contentView addSubview:self.photoImageView];
    }
    return self;
}

- (void)layoutSubviews{
    [super layoutSubviews];
    self.photoImageView.frame = self.contentView.bounds;
}

- (void)setCellFromItem:(PHAsset *)asset{
  PHImageRequestOptions *options = [[PHImageRequestOptions alloc]init];
    options.resizeMode = PHImageRequestOptionsResizeModeFast;
    [[PHImageManager defaultManager]requestImageForAsset:asset targetSize:[UIScreen mainScreen].bounds.size contentMode:PHImageContentModeAspectFill options:options resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        self.photoImageView.image = result;
    }];
}

- (UIImageView *)photoImageView{
    if (!_photoImageView) {
        _photoImageView = [[UIImageView alloc]init];
        _photoImageView.contentMode = UIViewContentModeScaleAspectFill;
    }
    return _photoImageView;
}

Datasource 也类似,定义一个 itemArray 用于存放图片对象 PHAsset @property (nonatomic, strong) NSMutableArray *itemArray; 实现 Datasource 方法,和 Tableview 类似(不用判断 cell 是 nil 是因为 Collectionviewcell 的初始化只有一种

#pragma mark - collectionViewDataSourceRequried

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return self.itemArray.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    PHAsset *item = self.itemArray[indexPath.row];
    CDZImagePickerPhotosCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([CDZImagePickerPhotosCell class]) forIndexPath:indexPath];
    [cell setCellFromItem:item];
    return cell;
}

在 Controller 中获取全部照片 再用 PhotosKit 的方法把所有图片获取到数组里

- (NSMutableArray *)getImageAssets{
    self.imageAssetsResult = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:nil];
    NSMutableArray *assets = [NSMutableArray new];
    for (PHAsset *asset in self.imageAssetsResult){
        [assets insertObject:asset atIndex:0];
    }
    return assets;
}

Collectionview 的布局,cell 的大小排列和间距等,都是由 UICollectionViewLayout 决定的,布局是线性的话,推荐用官方的子类 UICollectionViewFlowLayout,其 Deleagate 是 UICollectionviewDeleagateFlowLayout,是 UICollectionViewDelegate 的子 Delegate。

- (UICollectionViewFlowLayout *)photosFlowLayout{
    if (!_photosFlowLayout) {
        _photosFlowLayout = [UICollectionViewFlowLayout new];
        _photosFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; //水平滚动
    }
    return _photosFlowLayout;
}
#pragma mark - collectionViewDelegateFlowLayout
//根据 asset 的长宽设定每个 cell 的 size
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
    PHAsset *asset = self.photosDataSource.itemArray[indexPath.row];
    CGFloat height = photosViewHeight - 2 * photosViewInset;
    CGFloat aspectRatio = asset.pixelWidth / (CGFloat)asset.pixelHeight;
    CGFloat width = height * aspectRatio;
    CGSize size = CGSizeMake(width, height);
    return size;
}

//设置整个 collectionview 上下左右间距
- (UIEdgeInsets) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section{
    return UIEdgeInsetsMake(photosViewInset, photosViewInSet, photosViewInset, photosViewInset);
}

//设置 cell 之间的间距
- (CGFloat) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section{
    return photosViewInset;
}

设置 Collecionview 的 Delegate 和点击事件

#pragma mark - collectionViewDelegate

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
    [[PHImageManager defaultManager]requestImageForAsset:self.photosArray[indexPath.row] targetSize:PHImageManagerMaximumSize contentMode:PHImageContentModeAspectFill options:nil resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
        self.resultImage = result;
        [self dismissViewControllerAnimated:YES completion:nil];
        NSLog(@"已选择图片");
    }];
}

最后就是 Collectionview 的初始化和绘制和 Tableview 类似,多了一步注册 Collectionview 的 cell(Collectionviewcell 必须让 Collecionview 注册重用标识,而 Tableviewcell 可以在自己 init 方法里注册

- (UICollectionView *)photosView{
    if (!_photosView){
        CGFloat actionsViewHeight = actionsViewCellHeight * self.actionArray.count;
        _photosView = [[UICollectionView alloc]initWithFrame:CGRectMake(0, SCREEN_HEIGHT - actionsViewHeight - photosViewHeight , SCREEN_WIDTH, photosViewHeight) collectionViewLayout:self.photosFlowLayout];
        _photosView.delegate = self;
        _photosView.dataSource = self.photosDataSource;
        _photosView.backgroundColor = [UIColor whiteColor];
        _photosView.showsHorizontalScrollIndicator = NO;
        [_photosView registerClass:[CDZImagePickerPhotosCell class] forCellWithReuseIdentifier:NSStringFromClass([CDZImagePickerPhotosCell class])];
    }
    return _photosView;
}
- (CDZImagePickerPhotosDataSource *)photosDataSource{
    if (!_photosDataSource){
        _photosDataSource = [[CDZImagePickerPhotosDataSource alloc]init];
        _photosDataSource.itemArray = [self getImageAssets];
    }
    return _photosDataSource;
}

3.透明图层(用于实现空白处点击消失)

比较简单,增加一个 tap 手势即可,直接上代码(记得实现 UIGestureRecognizerDelegate

- (UIView *)backgroundView{
    if (!_backgroundView){
        CGFloat actionsViewHeight = actionsViewCellHeight * self.actionArray.count;
        _backgroundView =[[UIView alloc]initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT - photosViewHeight - actionsViewHeight)];
        _backgroundView.backgroundColor.opaque = YES;//设置为透明
        _backgroundView.userInteractionEnabled = YES;
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(dissPicker:)];
        [_backgroundView addGestureRecognizer:tap];
    }
    return _backgroundView;
}

4.收尾&封装

定义一个 block 用于回调传照片 typedef void (^CDZImageResultBlock) (UIImage *image); 一个内部block属性 @property (nonatomic ,copy) CDZImageResultBlock block; 封装方法

- (void)openPickerInController:(UIViewController *)controller withImageBlock:(CDZImageResultBlock)imageBlock{
    self.modalPresentationStyle = UIModalPresentationOverCurrentContext;//iOS 8 上默认 presentviewcontroller 不透明,需设置 style
    self.block = imageBlock;
    [controller presentViewController:self animated:YES completion:nil];
}

dealloc 方法里调用 block 回调

- (void)dealloc{
    self.block(self.resultImage);
    NSLog(@"ImagePicker已销毁");
}

5.使用方法

将 CDZImagePicker 文件夹拖入项目

CDZImagePickerViewController *imagePickerController = [[CDZImagePickerViewController alloc]init];
[imagePickerController openPickerInController:self withImageBlock:^(UIImage *image) {
        if (image) { //如果没选照片会回调 nil,若想让之前照片不变就加上判断
           //yourcode
        }
       // yourcode
}];

也可以自定义 CDZActionsItem 自定义文字和图片

imagePickerController.actionArray = [NSMutableArray arrayWithObjects:
                      [[CDZImagePickerActionsItem alloc]initWithTitle:@"打开设备上的图片" withActionType:CDZImagePickerLibraryAction withImage:[UIImage imageNamed:@"phone-icon.png"]],
                      [[CDZImagePickerActionsItem alloc]initWithTitle:@"相机" withActionType:CDZImagePickerCameraAction withImage:[UIImage imageNamed:@"camera-icon.png"]],
                      [[CDZImagePickerActionsItem alloc]initWithTitle:@"打开最新图片" withActionType:CDZImagePickerRecentAction withImage:[UIImage imageNamed:@"clock-icon.png"]],
                      nil];

效果就和 Snapseed 很像啦!

最后

所有源码和 Demo 虽然不是什么很难的轮子,但希望能分享给有需要的人。

关于后续的处理照片变更和获取相册权限另写了一篇 文章,欢迎阅读。

如果您觉得有帮助,不妨给个 star 鼓励一下,欢迎关注&交流