ImagePickerController(图片选择器)是 iOS 开发中一个很常用的 UI 控件。作为摄影爱好者,而 Google 出品的摄影后期 App,Snapseed 中那个图片选择器,好看又实用,便尝试着仿写了一个。
集成后一行代码即可实现下图效果。 实现原理从界面说起。 界面可以分解为一个背景图层用于点击关闭(空白处点击消失),一个横向可滚动的 Collectionview 用于显示图库里的图片,一个 Tableview 用于存放功能按钮。
1.功能按钮的 Tableview
先从简单的 Tableview说起,从小模块到大模块应该是以下方面。
- 自定义数组数据对象 item 方便调整按钮的文字图片数量功能
- 自定义 Tableview 的 cell 方便样式调整,cell 中属性根据对应 item 设置
- 自定义 Tableview 的 datasourece 在之中设置 cell 和 item 的对应关系
- Tableview 的 deleagte,设置点击事件
- 点击功能实现,设置
UIImagePickerController
- Tableview 的 View 初始化,设置 datasource,view 绘制
自定义 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 类似
- 自定义 Collectionview 的 cell 实现样式调整
- 自定义 Datasource 解析并设置当前的 cell 对应的图片
- 用 PhotosKit 的方法抓取图库的照片
- 实现 Collectionviewflowlayout 的 Delegate 实时调整 cell 的大小,边距
- 实现 Collecitonview 的 Delegate,设置点击事件
- 实现 Collectionview 的初始化和视图绘制
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 鼓励一下,欢迎关注&交流