简介

虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制。本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目)、WiFi传图、照片文件加密等功能。目前项目和文章会同时前进,项目的源代码可以在github上下载。
点击前往GitHub

概述

上一篇文章主要介绍了图片浏览器原图浏览、缩放和滑动切换图片的实现细节。本文主要介绍原图浏览实现的技术细节,其中包括了对内存占用的优化。

回顾

上节介绍了用于处理图片缩放的SGZoomingImageView,其实质是ScrollView+ImageView,scrollView的contentSize随着imageView的尺寸而变化,并且scrollView自带了对捏合手势缩放图片的支持。
图片切换是UIScrollView+SGZoomingScrollView,对scrollView进行分页,每一页都是一个SGZoomingScrollView,显示一张图片。

原图浏览控制器SGPhotoViewController

调用顺序

当点击了一张缩略图,就会push出原图浏览控制器SGPhotoViewController从而进入原图浏览状态,代码如下。
该方法属于缩略图浏览控制器SGPhotoBrowser,具体讲解可以在第四篇文章中找到

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {// 如果处于编辑状态(批处理照片),则不进入原图浏览,而是通过didUnhighlightItemAtIndexPath:方法处理图片的选中与反选if (self.toolBar.isEditing) {return;}SGPhotoViewController *vc = [SGPhotoViewController new];vc.browser = self;vc.index = indexPath.row;[self.navigationController pushViewController:vc animated:YES];
}

SGPhotoViewController中包含了一个SGPhotoView,SGPhotoView继承了UIScrollView,用于放置多个SGZoomingImageView来处理图片切换。它获取数据的方式仍然是通过browser的数据源(block回调),因此上面的代码中将browser传递了进来,同时需要当前图片的索引,以确定要查看哪一张图片。

SGPhotoViewController负责处理SGPhotoView的添加,单击事件(隐藏和显示导航栏、工具栏)和工具栏的动作(删除、导出照片),具体代码如下。

* 声明代码 *

@interface SGPhotoViewController : UIViewController
// 用于通过数据源block获取数据
@property (nonatomic, weak) SGPhotoBrowser *browser;
// 当前照片的模型索引
@property (nonatomic, assign) NSInteger index;@end
// 拓展
@interface SGPhotoViewController ()
// 用于记录是否隐藏了导航栏与工具栏
@property (nonatomic, assign) BOOL isBarHidden;
// 用于显示原图的视图,继承自UIScrollView
@property (nonatomic, weak) SGPhotoView *photoView;
// 底部工具条,能够提供删除、导出操作
@property (nonatomic, weak) SGPhotoToolBar *toolBar;@end

* 实现代码 *

- (void)viewDidLoad {[super viewDidLoad];// 添加SGPhotoView,通过addSubView:而不是loadView的原因在下面讲解[self setupView];// 防止scrollView的原点跟随导航栏自己变动self.automaticallyAdjustsScrollViewInsets = NO;// 为了防止循环引用,定义weakSelf的宏WS();// 控制器处理photoView的单击事件,用于翻转导航栏和工具栏的显示隐藏状态[self.photoView setSingleTapHandlerBlock:^{[weakSelf toggleBarState];}];
}
/*通过addSubView:而不是loadView加载SGPhotoView是因为SGPhotoView需要向左偏移-d的距离,来保持每一张图片之间的间隔(上一篇文章有讲),如果通过loadView将控制器视图指定为SGPhotoView,则需要到viewWillAppear:才能调整view的坐标。
*/
- (void)setupView {SGPhotoView *photoView = [SGPhotoView new];self.photoView = photoView;// photoView需要弱引用控制器,以便更改导航栏标题(显示当前是第几张)self.photoView.controller = self;// photoView需要browser以通过数据源获取数据self.photoView.browser = self.browser;// photoView需要知道当前是第几张照片self.photoView.index = self.index;[self.view addSubview:photoView];// photoView的左侧有一个宽为d的黑边,因此需要将photoView向左偏移dCGFloat x = -PhotoGutt;CGFloat y = 0;CGFloat w = self.view.bounds.size.width + 2 * PhotoGutt;CGFloat h = self.view.bounds.size.height;self.photoView.frame = CGRectMake(x, y, w, h);CGFloat barW = self.view.bounds.size.width;CGFloat barH = 44;CGFloat barX = 0;CGFloat barY = self.view.bounds.size.height - barH;// 底部工具条,用于进行导出和删除操作,工具条和第五篇提到的一样,继承SGBlockToolBar,通过block回调SGPhotoToolBar *tooBar = [[SGPhotoToolBar alloc] initWithFrame:CGRectMake(barX, barY, barW, barH)];self.toolBar = tooBar;[self.view addSubview:tooBar];WS();// 处理工具栏的动作,tag在SGPhotoToolBar中定义[self.toolBar setButtonActionHandlerBlock:^(UIBarButtonItem *sender) {switch (sender.tag) {case SGPhotoToolBarTrashTag:[weakSelf trashAction];break;case SGPhotoToolBarExportTag:[weakSelf exportAction];break;default:break;}}];
}

到这里为止,就完成了photoView和工具栏的加载,接下来就是一些细节了。

导航栏与工具栏的显示与隐藏

导航栏在隐藏时,应该同时把状态栏隐藏,而状态栏的隐藏在iOS7以后是默认通过控制器管理的,通过控制器的prefersStatusBarHidden方法返回是否显示,如果要更新状态栏状态,则使用setNeedsStatusBarAppearanceUpdate方法,如下。

- (void)toggleBarState {self.isBarHidden = !self.isBarHidden;[self setNeedsStatusBarAppearanceUpdate];
}
- (BOOL)prefersStatusBarHidden {return self.isBarHidden;
}

当然也可以通过UIApplication单例来操作,但在iOS7之后其优先级比上面的方式要低,实现如下。

- (void)toggleBarState {self.isBarHidden = !self.isBarHidden;[[UIApplication sharedApplication] setStatusBarHidden:self.isBarHidden withAnimation:YES];
}

如果想只是用下面的方法来调整工具栏,则需要在info.plist中设置View controller-based status bar appearance为NO。
除此之外还需要处理对导航栏和工具栏的隐藏,其完整实现如下。

- (void)toggleBarState {self.isBarHidden = !self.isBarHidden;[[UIApplication sharedApplication] setStatusBarHidden:self.isBarHidden withAnimation:NO];[self.navigationController setNavigationBarHidden:self.isBarHidden animated:YES];[UIView animateWithDuration:0.35 animations:^{self.toolBar.alpha = self.isBarHidden ? 0 : 1.0f;}];
}

删除图片动作

工具条的删除动作通过block回调到原图控制器,并且执行trashAction方法,该方法先通过ActionSheet来让用户确认是否真的要删除,如果确认,则根据当前照片模型数据删除文件,并且通过browser的数据源去请求重新加载数据。

- (void)trashAction {// block回调的ActionSheet[[[SGBlockActionSheet alloc] initWithTitle:@"Please Confirm Delete" callback:^(UIActionSheet *actionSheet, NSInteger buttonIndex) {if (buttonIndex == 0) {// photoView的currentPhoto为展示中的图片,下文讲解细节[[NSFileManager defaultManager] removeItemAtPath:self.photoView.currentPhoto.photoURL.path error:nil];[[NSFileManager defaultManager] removeItemAtPath:self.photoView.currentPhoto.thumbURL.path error:nil];[self.navigationController popViewControllerAnimated:YES];// browser的子类必须实现reload数据源block来通知其重新从文件系统中加载数据,以便显示删除后的效果NSAssert(self.browser.reloadHandler != nil, @"you must implement 'reloadHandler' block to reload files while delete");self.browser.reloadHandler();// 重新加载文件只是加载了模型数据,还需要collectionView重新加载数据[self.browser reloadData];}} cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete" otherButtonTitlesArray:nil] showInView:self.view];
}

导出图片动作

使用ALAssetsLibrary的writeImageToSavedPhotosAlbum:::方法即可向系统相册写入数据,注意该方法为异步,具体实现如下。

- (void)exportAction {[[[SGBlockActionSheet alloc] initWithTitle:@"Save To Where" callback:^(UIActionSheet *actionSheet, NSInteger buttonIndex) {if (buttonIndex == 1) {ALAssetsLibrary *lib = [ALAssetsLibrary new];// currentImageView是photoView正在显示的照片的SGZoomingImageView对象,下文详细讲解UIImage *image = self.photoView.currentImageView.innerImageView.image;[MBProgressHUD showMessage:@"Saving"];[lib writeImageToSavedPhotosAlbum:image.CGImage metadata:nil completionBlock:^(NSURL *assetURL, NSError *error) {[MBProgressHUD hideHUD];[MBProgressHUD showSuccess:@"Succeeded"];}];}} cancelButtonTitle:@"Cancel" destructiveButtonTitle:nil otherButtonTitlesArray:@[@"Photo Library"]] showInView:self.view];
}

原图浏览视图SGPhotoView

类结构

用于原图浏览的核心即为SGPhotoView,它是一个scrollView,上面分页排布着SGZoomingImageView对象,其结构如下。

// 照片的间距
#define PhotoGutt 20
// 单击事件的回调block定义,用于交给控制器处理
typedef void(^SGPhotoViewTapHandlerBlcok)(void);@interface SGPhotoView : UIScrollView@property (nonatomic, weak) SGPhotoViewController *controller;
@property (nonatomic, weak) SGPhotoBrowser *browser;
// 当前浏览的图片索引,每次从缩略图进入原图浏览时需要传入以初始化,在左右滑动时自动更新
@property (nonatomic, assign) NSInteger index;
// 用于控制器获取当前模型与当前SGZoomingImageView对象
@property (nonatomic, strong) SGPhotoModel *currentPhoto;
@property (nonatomic, weak) SGZoomingImageView *currentImageView;
// 单击事件的block回调setter
- (void)setSingleTapHandlerBlock:(SGPhotoViewTapHandlerBlcok)handler;@end
// 拓展
@interface SGPhotoView () <UIScrollViewDelegate> {// 每页的宽度,由于左右切图时根据偏移量计算当前图片索引CGFloat _pageW;
}@property (nonatomic, copy) SGPhotoViewTapHandlerBlcok singleTapHandler;
// 用于存储显示每一张图片的对象
@property (nonatomic, strong) NSArray<SGZoomingImageView *> *imageViews;@end

初始化

首先是类初始化时的参数初始化,包括背景色、分页、UIScrollView的代理。

- (instancetype)initWithFrame:(CGRect)frame {if (self = [super initWithFrame:frame]) {[self commonInit];}return self;
}
- (void)commonInit {self.backgroundColor = [UIColor blackColor];self.pagingEnabled = YES;self.delegate = self;
}

接下来是通过browser和index的setter来初始化每一张图片。
* 通过Browser的setter来初始化每一张图片对象 *
由于涉及到在分页的scrollView上实现间距,计算较为复杂,关于间距计算的讲解可以参考第六篇文章。

// 简言之就是将每张图片对象放到photoView的正确位置,并将这些图片对象引用
- (void)setBrowser:(SGPhotoBrowser *)browser {_browser = browser;NSInteger count = browser.numberOfPhotosHandler();CGSize visibleSize = [UIScreen mainScreen].bounds.size;NSMutableArray *imageViews = @[].mutableCopy;CGFloat imageViewWidth = visibleSize.width + PhotoGutt * 2;_pageW = imageViewWidth;self.contentSize = CGSizeMake(count * imageViewWidth, 0);for (NSUInteger i = 0; i < count; i++) {SGZoomingImageView *imageView = [SGZoomingImageView new];SGPhotoModel *model = self.browser.photoAtIndexHandler(i);[imageView.innerImageView sg_setImageWithURL:model.thumbURL];imageView.isOrigin = NO;CGRect frame = (CGRect){imageViewWidth * i, 0, imageViewWidth, visibleSize.height};imageView.frame = CGRectInset(frame, PhotoGutt, 0);[imageViews addObject:imageView];[self addSubview:imageView];[imageView scaleToFitAnimated:NO];}self.imageViews = imageViews;
}

内存优化

* 通过index的setter来使得photoView滚动到特定位置并加载高清图 *
为了优化内存,除去正在展示的图片和与其相邻的图片,加载的都是缩略图,在切换过程中会动态的计算应该显示原图的位置,并将不相邻的原图全部置为缩略图,具体实现如下。

- (void)setIndex:(NSInteger)index {_index = index;CGSize visibleSize = [UIScreen mainScreen].bounds.size;// 根据index翻到特定的页self.contentOffset = CGPointMake(index * _pageW, 0);[self loadImageAtIndex:index];
}
- (void)loadImageAtIndex:(NSInteger)index {// 更新控制器标题为当前图片索引,例如一共九张,当前是第三张,则是"3 Of 9"[self updateNavBarTitleWithIndex:index];// 通过browser的数据源获取模型总数NSInteger count = self.browser.numberOfPhotosHandler();// 遍历所有的照片对象for (NSInteger i = 0; i < count; i++) {通过browser的数据源获取模型数据SGPhotoModel *model = self.browser.photoAtIndexHandler(i);SGZoomingImageView *imageView = self.imageViews[i];// 对于当前显示的图片进行引用,其他图片都缩放到适应屏幕(图片缩放在第六篇文章有讲解)if (i == index) {self.currentImageView = imageView;} else {[imageView scaleToFitIfNeededAnimated:NO];}NSURL *photoURL = model.photoURL;NSURL *thumbURL = model.thumbURL;// 对于当前图片以及相邻图片,如果没有加载原图,则去加载原图替换缩略图,并且变换到适应屏幕大小if (i >= index - 1 && i <= index + 1) {if (imageView.isOrigin) continue;[imageView.innerImageView sg_setImageWithURL:photoURL];imageView.isOrigin = YES;[imageView scaleToFitAnimated:NO];} else {// 对于其他图片,如果仍然持有原图,则用缩略图替换之,以节约内存if (!imageView.isOrigin) continue;[imageView.innerImageView sg_setImageWithURL:thumbURL];imageView.isOrigin = NO;[imageView scaleToFitAnimated:NO];}}
}

其他细节

* 设置当前图片单击手势的回调 *
在第六篇文章中讲到,单击和双击由照片对象SGZoomingImageView捕获,双击在类内处理,而单击传递到类外的photoView,再传递到控制器以翻转bar的显示状态,因此应该在设置当前图片对象时先清除已经不在屏幕上显示的图片对象的block回调,并且将当前显示的图片对象的block进行设置,这些可以在currentImageView的setter中实现,具体如下。

- (void)setCurrentImageView:(SGZoomingImageView *)currentImageView {// 如果赋值前不为空说明之前有其他图片对象被展示,先清空其回调再赋值if (_currentImageView != nil) {[_currentImageView setSingleTapHandler:nil];}_currentImageView = currentImageView;WS(); // 定义weakSelf,防止循环引用[_currentImageView setSingleTapHandler:^{// 通过block继续向控制器传递单击事件if (weakSelf.singleTapHandler) {weakSelf.singleTapHandler();}}];
}

* 在滑动切换图片时更新index并处理原图的装载与卸载 *
为了防止左右滚动时卡顿,在滚动结束后才进行处理,通过UIScrollView的scrollViewDidEndDecelerating:代理回调,该方法在scrollView减速完毕后调用。

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {CGFloat offsetX = scrollView.contentOffset.x;// 根据当前偏移和页宽计算索引,与scrollView的页面切换规则一致,偏移超过50%的页宽就切换到下一页NSInteger index = (offsetX + _pageW * 0.5f) / _pageW;if (_index != index) {_index = index;[self loadImageAtIndex:_index];}
}

总结

本文主要讲了完成原图浏览与图片切换的细节,并对内存占用进行了优化,到这里为止,加密相册的Demo就基本介绍完毕了,由于代码较多,文中只能挑重点讲解,项目的源代码在文首的地址中可以找到,欢迎关注项目后续。

iOS开源加密相册Agony的实现(七)相关推荐

  1. iOS开源加密相册Agony的实现(二)

    简介 虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制.本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目).Wi ...

  2. iOS开源加密相册Agony的实现(三)

    简介 虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制.本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目).Wi ...

  3. iOS开源加密相册Agony的实现(一)

    简介 虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制.本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目).Wi ...

  4. iOS开源加密相册Agony的实现(五)

    简介 虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制.本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目).Wi ...

  5. iOS开源加密相册Agony的实现(四)

    简介 虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制.本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目).Wi ...

  6. iOS开源加密相册Agony的实现(六)

    简介 虽然目前市面上有一些不错的加密相册App,但不是内置广告,就是对上传的张数有所限制.本文介绍了一个加密相册的制作过程,该加密相册将包括多密码(输入不同的密码即可访问不同的空间,可掩人耳目).Wi ...

  7. 最好用的iOS iPhone加密相册app上线了,功能强大,完全免费

    功能强大,易于使用,完全免费的iOS加密相册app上线了,AppStore下载:https://itunes.apple.com/app/id1458351572?mt=8 (有任何问题不要删除app ...

  8. iOS开源照片浏览器框架SGPhotoBrowser的设计与实现

    简介 近日在制作一个开源加密相册时附带着设计了一个照片浏览器,在进一步优化后发布到了GitHub供大家使用,该框架虽然没有MWPhotoBrowser那么强大,但是使用起来更为方便,操作更符合常规相册 ...

  9. iOS开源框架和项目总结

    github上关于iOS的各种开源项目集合(转) .entry-header UI 下拉刷新 EGOTableViewPullRefresh - 最早的下拉刷新控件. SVPullToRefresh  ...

最新文章

  1. python保存内容到文件(text、json、csv)
  2. freemarker 读取字符串模板,(非文件)
  3. django安装mysql驱动
  4. BigData:绘制2018年福布斯中国富豪榜人名坐标地图(解决多个人名显示在同一个家乡地点)
  5. create 2021 | 一图读懂汽车智能化分论坛
  6. 绝了!Pandas绘图功能
  7. 将tomcat添加到服务中
  8. linux之使用md5sum命令比较两个文件是否一样
  9. Android性能优化之APK瘦身最全总结
  10. ReactNative 基于rmc-datepicker的日期选择的使用
  11. PHP-php://(类型)访问各个输入/输出流以及全局变量$HTTP_RAW_POST_DATA讲解
  12. Tomcat修改端口号(7.0 version)
  13. Oracle 11g安装步骤(超详细)
  14. zfs文件服务器上传失败,解决 ZFS 文件系统问题
  15. echarts 柱状图 柱顶部显示数字
  16. [SSL_CHX][2021-8-19]空瓶换饮料
  17. xamppmysql访问被拒绝_如何解决Xampp MySQL错误#2002-无法建立连接,因为目标计算机主动拒绝了它...
  18. 网络原理练习题(含答案)
  19. 解决NVIDIA显卡驱动 图形驱动程序安装失败 问题
  20. 皕杰基础平台的登录方式

热门文章

  1. element ui+vue实现打印
  2. Java中什么是引用变量
  3. ysscloud怎么用_红米游戏加速怎么添加到桌面
  4. 有趣的自然语言处理资源集锦
  5. php 8 jit,了解PHP 8的JIT
  6. redis的rdb文件在不同服务器之间的迁移 导入阿里云redis
  7. 集中式数据库性能优化
  8. java开源框架有哪些_常用的Java微服务开源框架有哪些呢
  9. GoJS使用以及去除水印办法
  10. 淘宝店铺发布API接口(新),淘宝oAuth2.0店铺商品API接口,淘宝商品发布API接口,淘宝商品上架API接口,一整套发布上架店铺接口对接分享