ImagePickerController(圖片選擇器)是iOS開(kāi)發(fā)中一個(gè)很常用的UI控件。作為攝影愛(ài)好者,而Google出品的攝影后期App,Snapseed中那個(gè)圖片選擇器,好看又實(shí)用,便嘗試著仿寫(xiě)了一個(gè)。集成后一行代碼即可實(shí)現(xiàn)下圖效果。

實(shí)現(xiàn)原理從界面說(shuō)起。
界面可以分解為一個(gè)背景圖層用于點(diǎn)擊關(guān)閉(空白處點(diǎn)擊消失),一個(gè)橫向可滾動(dòng)的collectionview用于顯示圖庫(kù)里的圖片,一個(gè)tableview用于存放功能按鈕。
1. 功能按鈕的tableview
先從簡(jiǎn)單的tableview說(shuō)起,從小模塊到大模塊應(yīng)該是以下方面。
- 自定義數(shù)組數(shù)據(jù)對(duì)象item方便調(diào)整按鈕的文字圖片數(shù)量功能
- 自定義tableview的cell方便樣式調(diào)整,cell中屬性根據(jù)對(duì)應(yīng)item設(shè)置
- 自定義tableview的datasourece在之中設(shè)置cell和item的對(duì)應(yīng)關(guān)系
- tableview的deleagte,設(shè)置點(diǎn)擊事件
- 點(diǎn)擊功能實(shí)現(xiàn),設(shè)置UIImagePickerController
- tableview的view初始化,設(shè)置datasource,view繪制
自定義item對(duì)象,item對(duì)象定義了action的顯示名字,類型,圖片
@property(nonatomic, copy) NSString *actionTitle;
@property(nonatomic, strong) UIImage *actionImage;
@property(nonatomic, assign) CDZImagePickerActionType actionType;
其中CDZImagePickerActionType可用來(lái)定義按鈕動(dòng)作類型,可以用枚舉來(lái)定義
typedef NS_ENUM(NSInteger, CDZImagePickerActionType) {
CDZImagePickerCameraAction,
CDZImagePickerLibraryAction,
CDZImagePickerRecentAction,
CDZImagePickerCloseAction
};
定義一個(gè)init方法用來(lái)快捷初始化item對(duì)象
- (instancetype)initWithTitle:(NSString *)titele withActionType:(CDZImagePickerActionType)type withImage:(UIImage *)image{
self = [super init];
if (self) {
_actionTitle = titele;
_actionType = type;
_actionImage = image;
}
return self;
}
自定義cell對(duì)象,封裝一個(gè)方法來(lái)對(duì)應(yīng)item和cell的關(guān)系
- (void)setCellFromItem:(CDZImagePickerActionsItem *)item{
self.textLabel.text = item.actionTitle;
self.imageView.image = item.actionImage;
}
可以重寫(xiě)layoutSubviews方法達(dá)到cell中文字,圖片樣式自定義的效果,比如可以寫(xiě)有圖片和無(wú)圖片的樣式布局
- (void)layoutSubviews{
[super layoutSubviews];
self.selectionStyle = UITableViewCellSelectionStyleNone;//點(diǎn)擊不變色
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,這一步可以根據(jù)個(gè)人選擇,個(gè)人習(xí)慣是把datasource從viewcontroller里抽離,可以讓controller更少代碼,datasource有數(shù)組對(duì)象用于存放item
@property (nonatomic, strong) NSMutableArray *itemArray;
實(shí)現(xiàn)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對(duì)應(yīng)起來(lái)
return cell;
}
實(shí)現(xiàn)delegate和點(diǎn)擊事件
#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(@"打開(kāi)相機(jī)");
}
}
- (void)openLibrary{
UIImagePickerController *pickerController = [[UIImagePickerController alloc]init];
pickerController.delegate = self;
pickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
[self presentViewController:pickerController animated:NO completion:nil];
NSLog(@"打開(kāi)圖庫(kù)");
}
- (void)closeAction{
[self dismissViewControllerAnimated:YES completion:nil];
NSLog(@"關(guān)閉按鈕");
}
- (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(@"打開(kāi)最新圖片");
}];
}
實(shí)現(xiàn)imagepicker的delegate(記得實(shí)現(xiàn)UINavigationControllerDelegate,不可只實(shí)現(xiàn)UIImagePickerControllerDelegate,會(huì)報(bào)錯(cuò))
#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(@"從相機(jī)或圖庫(kù)獲取圖片");
}
iOS10還要檢查一下相機(jī)和圖庫(kù)的權(quán)限,記得在info.plist中添加兩行,不然會(huì)崩潰

最后在是tableview初始化與繪制,在datasource初始化自己需要的按鈕item數(shù)組(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; //不需要滑動(dòng)
_actionView.separatorStyle = UITableViewCellSeparatorStyleNone; //分割線去除
_actionView.delegate = self;
_actionView.dataSource = self.actionsDataSource;
}
return _actionView;
}
2.展示照片的collecionview
collecionview的實(shí)現(xiàn)思路和tableview類似
- 自定義collectionview的cell實(shí)現(xiàn)樣式調(diào)整
- 自定義Datasource解析并設(shè)置當(dāng)前的cell對(duì)應(yīng)的圖片
- 用Photokit的方法抓取圖庫(kù)的照片
- 實(shí)現(xiàn)collectionviewflowlayout的delegate實(shí)時(shí)調(diào)整cell的大小,邊距
- 實(shí)現(xiàn)collecitonview的delegate,設(shè)置點(diǎn)擊事件
- 實(shí)現(xiàn)collectionview的初始化和視圖繪制
cell的自定義主要是重寫(xiě)其initWithFrame方法添加imageview(注意collectionviewcell只用initWithFrame方式初始化,tableviewcell初始化方法有多種),并在layoutSubviews里改變imageview的大小,和tableview類似寫(xiě)一個(gè)setCellFromItem的方法解析item數(shù)據(jù)。而Photokit獲取的圖片對(duì)象為phasset,用PHImageMange的方法根據(jù)將其解析為size為cell的大小uimage,加快加載速度,按需加載(關(guān)于Photokit的優(yōu)點(diǎn)不再贅述,iOS8以后蘋(píng)果只保留了Photokit用于獲取系統(tǒng)圖片)
photokit獲取時(shí),注意兩個(gè)地方,targetSize傳入的是pixel,而不是平時(shí)用的size,且option里的resizeMode默認(rèn)為None(不縮放),不重寫(xiě)resizeMode屬性那么只會(huì)抓取不縮放的圖,在意質(zhì)量可以設(shè)置resizeMode為Exact(精確),追求速度可以設(shè)置為Fast(這樣不會(huì)完全按照設(shè)置的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也類似,定義一個(gè)itemArray用于存放圖片對(duì)象phasset
@property (nonatomic, strong) NSMutableArray *itemArray;
實(shí)現(xiàn)datasource方法,和tableview類似(不用判斷cell是nil是因?yàn)閏ollectionviewcell的初始化只有一種)
#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中獲取全部照片
再用photokit的方法把所有圖片獲取到數(shù)組里
- (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的大小排列和間距等,都是由collectionviewlayout決定的,布局是線性的話,推薦用官方的子類collectionviewflowlayout,其deleagate是collectionviewdeleagateflowlayout,是collectionviewdelegate的子delegate。
- (UICollectionViewFlowLayout *)photosFlowLayout{
if (!_photosFlowLayout) {
_photosFlowLayout = [UICollectionViewFlowLayout new];
_photosFlowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; //水平滾動(dòng)
}
return _photosFlowLayout;
}
#pragma mark - collectionViewDelegateFlowLayout
//根據(jù)asset的長(zhǎng)寬設(shè)定每個(gè)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;
}
//設(shè)置整個(gè)collectionview上下左右間距
- (UIEdgeInsets) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section{
return UIEdgeInsetsMake(photosViewInset, photosViewInSet, photosViewInset, photosViewInset);
}
//設(shè)置cell之間的間距
- (CGFloat) collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section{
return photosViewInset;
}
設(shè)置collecionview的delegate和點(diǎn)擊事件
#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類似,多了一步注冊(cè)collectionview的cell(collectionviewcell必須讓collecionview注冊(cè)重用標(biāo)識(shí),而tableviewcell可以在自己init方法里注冊(cè))
- (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.透明圖層(用于實(shí)現(xiàn)空白處點(diǎn)擊消失)
比較簡(jiǎn)單,增加一個(gè)tap手勢(shì)即可,直接上代碼(記得實(shí)現(xiàn)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;//設(shè)置為透明
_backgroundView.userInteractionEnabled = YES;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(dissPicker:)];
[_backgroundView addGestureRecognizer:tap];
}
return _backgroundView;
}
4.收尾&封裝
定義一個(gè)block用于回調(diào)傳照片
typedef void (^CDZImageResultBlock) (UIImage *image);
一個(gè)內(nèi)部block屬性
@property (nonatomic ,copy) CDZImageResultBlock block;
封裝方法
- (void)openPickerInController:(UIViewController *)controller withImageBlock:(CDZImageResultBlock)imageBlock{
self.modalPresentationStyle = UIModalPresentationOverCurrentContext;//iOS8上默認(rèn)presentviewcontroller不透明,需設(shè)置style
self.block = imageBlock;
[controller presentViewController:self animated:YES completion:nil];
}
在Dealloc方法里調(diào)用block回調(diào)
- (void)dealloc{
self.block(self.resultImage);
NSLog(@"ImagePicker已銷(xiāo)毀");
}
5.使用方法
將CDZImagePicker文件夾拖入項(xiàng)目
CDZImagePickerViewController *imagePickerController = [[CDZImagePickerViewController alloc]init];
[imagePickerController openPickerInController:self withImageBlock:^(UIImage *image) {
if (image) { //如果沒(méi)選照片會(huì)回調(diào)nil,若想讓之前照片不變就加上判斷
//yourcode
}
// yourcode
}];
也可以自定義CDZActionsItem自定義文字和圖片
imagePickerController.actionArray = [NSMutableArray arrayWithObjects:
[[CDZImagePickerActionsItem alloc]initWithTitle:@"打開(kāi)設(shè)備上的圖片" withActionType:CDZImagePickerLibraryAction withImage:[UIImage imageNamed:@"phone-icon.png"]],
[[CDZImagePickerActionsItem alloc]initWithTitle:@"相機(jī)" withActionType:CDZImagePickerCameraAction withImage:[UIImage imageNamed:@"camera-icon.png"]],
[[CDZImagePickerActionsItem alloc]initWithTitle:@"打開(kāi)最新圖片" withActionType:CDZImagePickerRecentAction withImage:[UIImage imageNamed:@"clock-icon.png"]],
nil];
效果就和snapseed很像啦!

最后
所有源碼和Demo
雖然不是什么很難的輪子,但希望能分享給有需要的人。
關(guān)于后續(xù)的處理照片變更和獲取相冊(cè)權(quán)限另寫(xiě)了一篇簡(jiǎn)書(shū)文章,歡迎閱讀。
如果您覺(jué)得有幫助,不妨給個(gè)star鼓勵(lì)一下,歡迎關(guān)注&交流
有任何問(wèn)題歡迎評(píng)論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz