最近重新开始练习iOS开发,找感觉。
先做个简单的图片浏览选择功能。不用管是难还是简单,先实现一下。
一、步骤概述
包含三个步骤:
- 创建页面:图片选择页(collectionView),图片预览页
- 处理数据:在页面间传递图片数据集或者单张图片,用系统的Photo库API获取图片
- 实现动画:各个手势(GestureRecognizer)结合数值的计算
练习重点:
- 各种手势(GestureRecognizer)的运用
二、具体实现
创建页面
首先是创建相应的页面。这个作为最基础的部分还是在此略过。总共有三个页面:
- 入口页
- 选择页
- 预览页
- 选择图片后的入口页
处理数据
然后是处理图片数据,在页面间传递图片数据。
给图片加上点击事件,由于UIImageView默认不能交互,所以需要把交互打开:
在入口页导入系统的
if (status == PHAuthorizationStatusNotDetermined) {
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
if (status == PHAuthorizationStatusAuthorized) {
// 有权限读取照片
[self performLoadPhotoAssets]; // 自定义方法
}
}];
} else if (status == PHAuthorizationStatusAuthorized) {
// 有权限读取照片
[self performLoadPhotoAssets]; // 自定义方法
}
复制代码
当判断到有权限查看相册时,则执行获取图片的方法[self performLoadPhotoAssets]
:
- 先从系统相册获取图片
assetsArray
NSMutableArray *assetsArray = [NSMutableArray array];
PHFetchOptions *options = [[PHFetchOptions alloc] init];
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:NO]];
PHFetchResult *fetchResult = [PHAsset fetchAssetsWithOptions:options];
[fetchResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[assetsArray addObject:obj];
}];
复制代码
- 然后传递到下一个UICollectionView界面
UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
layout.itemSize = CGSizeMake(100, 100);
CGFloat space = (self.view.bounds.size.width - (4 * 100)) / 5;
if (space < 5) space = 5;
layout.minimumInteritemSpacing = space;
layout.minimumLineSpacing = 15;
layout.sectionInset = UIEdgeInsetsMake(15, space, 15, space);
YLPhotoSelectionViewController *photoVC = [[YLPhotoSelectionViewController alloc] initWithCollectionViewLayout:layout];
photoVC.assetsArray = [assetsArray copy];
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:photoVC];
[self presentViewController:nav animated:YES completion:nil];
复制代码
在这个UICollectionView界面,也就是图片选择页,在接口处自然是定义好属性:
@interface YLPhotoSelectionViewController : UICollectionViewController
@property(nonatomic, copy) NSArray * _Nullable assetsArray;
@end
复制代码
这个collectionView没什么好多说的,实现collectionView的dataSource和delegate即可,唯一要注意的是在CollectionViewCell里继续引入并调用Photo库解码PHAseet:
- cell里接受数据的属性
@interface YLPhotoCollectionViewCell : UICollectionViewCell
@property(nonatomic, strong) PHAsset *asset;
@end
复制代码
- 在
asset
属性的set里解码PHAsset
- (void)setAsset:(PHAsset *)asset {
__weak __typeof(self) weakSelf = self;
[[PHImageManager defaultManager] requestImageForAsset:asset
targetSize:self.photoImageView.bounds.size
contentMode:PHImageContentModeAspectFill
options:NULL
resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
if (result) {
// 给cell里的图片视图ImageView赋值
weakSelf.photoImageView.image = result;
}
}];
}
复制代码
然后接着在collectionView的点击代理里,把整个图片数组和被选择图片的索引值传递给图片预览页:
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
NSInteger row = indexPath.row;
// 图片预览页
YLPhotoPreviewViewController *previewVC = [[YLPhotoPreviewViewController alloc] init];
// 传递图片和图片的索引值
previewVC.photoAssetsArray = self.assetsArray;
previewVC.photoIndex = row;
[self.navigationController pushViewController:previewVC animated:YES];
}
复制代码
传递整个图片数组和被选择图片的索引值,而不是只传递被选择的图片,这样做目的是给轻扫手势切换图片提供相应的数据,通过对索引值的加和减来实现。
同样地,在图片预览页面的接口定义接收数据的属性:
@interface YLPhotoPreviewViewController : UIViewController
@property(nonatomic, copy) NSArray * _Nullable photoAssetsArray;
@property(nonatomic, assign) NSInteger photoIndex;
@end
复制代码
图片的呈现很简单,一张铺满屏幕的UIImageView,同样用在cell里的Photo库的解码方法,把数据赋值给UIImageView,注意这里的图片是通过传递过来的两个属性组合取出:
PHAsset *asset = self.photoAssetsArray[self.photoIndex];
复制代码
实现动画
最后是这次的重点,实现各种手势操作:
- 长按,UILongPressGestureRecognizer
- 轻扫,UISwipeGestureRecognizer
- 缩放,UIPinchGestureRecognizer
- 旋转,UIRotationGestureRecognizer
- 滑动,UIPanGestureRecognizer
- 屏幕边缘滑动,UIScreenEdgePanGestureRecognizer
这里重点记录一下缩放、旋转和平移这三个手势实现时,都需要有处理的数据,全部代码在GitHub里。
首先它们都需要记录一个最终数值:
// 最终照片的比例
@property (assign, nonatomic) CGFloat finalPinchScale;
// 最终旋转角度
@property (assign, nonatomic) CGFloat finalRotaionAngle;
// 最终平移的位置
@property (assign, nonatomic) CGPoint finalPanTranslastion;
复制代码
其次都需要记录上次缩放、旋转或平移的值:
// 上次缩放的比例
static CGFloat _lastPinchScale = 1;
// 上一次旋转的角度
static CGFloat _lastRotaionAngle = 0;
// 上次平移的位置
static CGPoint _lastTranslation = {.x = 0, .y = 0};
复制代码
然后是根据操作的状态state来计算数值,以缩放的代码为例:
if (sender.state == UIGestureRecognizerStateChanged) {
// 改变照片的缩放比例
self.finalPinchScale = _lastPinchScale * currentPinchScale;
// 将finalPinchScaley应用在照片上
[self transformForImage]; // 自定义方法
} else if (sender.state == UIGestureRecognizerStateEnded) {
// 更新lastPinchScale的数值到本次缩放的比例
_lastPinchScale = _lastPinchScale *currentPinchScale;
}
复制代码
然后就是执行关键的自定义方法[self transformForImage]
:
- (void)transformForImage {
// 把最终值传入相应的transform类型
CGAffineTransform scaleTransform = CGAffineTransformMakeScale(self.finalPinchScale, self.finalPinchScale);
CGAffineTransform rotationTransform = CGAffineTransformRotate(scaleTransform, self.finalRotaionAngle);
CGAffineTransform translationTransform = CGAffineTransformTranslate(rotationTransform, _finalPanTranslastion.x, _finalPanTranslastion.y);
self.previewImageView.transform = translationTransform;
}
复制代码
这里是最终的自定义方法,可以看到不同类型的transform是需要叠加在一起,然后最后一个transform类型赋值给视图的transform属性。
最后要主要,当有多个容易冲突的手势操作时,这些手势需要实现相应的代理方法:
// 缩放、旋转、平移手势各自签署协议
pinchGesture.delegate = self;
rotationGesture.delegate = self;
panGesture.delegate = self;
// “同时生效”的代理方法
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
return YES;
}
复制代码
然后就可以操作手势来实现缩放、旋转等功能了:
三、总结
iOS开发很大一部分工作是熟悉使用系统提供的强大API,然后做适度的封装。
至于编程的另一部分——算法,数据结构,编程范式等,不分后端还是前端,都是编程者需要结合理论和实践不断提高的东西。