iOS中寫(xiě)一個(gè)仿Snapseed的ImagePickerController(照片選擇器)

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

謝謝觀看

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容