简介

虽然目前市面上有一些不错的加密相册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向左偏移d
CGFloat 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开源照片浏览器框架SGPhotoBrowser的设计与实现

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

  8. iOS -- 开源项目和库

    TimLiu-iOS 目录 UI 下拉刷新 模糊效果 AutoLayout 富文本 图表 表相关与Tabbar 隐藏与显示 HUD与Toast 对话框 其他UI 动画 侧滑与右滑返回手势 gif动画 ...

  9. iOS开源项目周报1222

    由OpenDigg 出品的iOS开源项目周报第二期来啦.我们的iOS开源周报集合了OpenDigg一周来新收录的优质的iOS开发方面的开源项目,方便iOS开发人员便捷的找到自己需要的项目工具等. io ...

随机推荐

  1. super函数的作用

    super函数的作用super().__init__()当子类重写父类的方法时,会覆盖父类方法,super此举是保留父类 如果属性名跟方法名相同,属性会覆盖方法 方法必须要有实例才能被调用,这叫做绑定

  2. Python的字典和JSON

    Python的字典和JSON在表现形式上非常相似 #这是Python中的一个字典 dic = { 'str': 'this is a string', 'list': [1, 2, 'a', 'b'] ...

  3. vue的入门/简介

    vue 特点 1. 响应的数据绑定/响应式编程  2. 组件化 vue优点 1. 轻量级的框架  2. 简单易学 3. 双向数据绑定 4. 组件化 5. 视图,数据,结构分离 6. 虚拟DOM 7. ...

  4. java代码优化细节

    在代码线上运行的过程中,往往会出现很多我们意想不到的错误,不少错误定位到最后往往是一个非常小的原因导致的.然而因为线上环境和开发环境是非常不同的,为了解决一个错误,我们需要先查找错误原因.修改验证.打 ...

  5. [LeetCode] Design Excel Sum Formula 设计Excel表格求和公式

    Your task is to design the basic function of Excel and implement the function of sum formula. Specif ...

  6. Python系列之 - 面向对象(2)

    类的三大特性 类的三大特性包括: 封装.继承.多态 一 封装 封装就是将类所用到的所有字段.属性.方法都包含在类代码段里面,当实例调用直接调用类中的方法即可. class People(object) ...

  7. 实验吧_貌似有点难(php代码审计)&头有点大

    二话不说先贴代码 <?php function GetIP(){ if(!empty($_SERVER["HTTP_CLIENT_IP"])) $cip = $_SERVER ...

  8. (MariaDB/MySQL)之DML(1):数据插入

    本文目录: 1.insert和replace插入数据 1.1 insert into values() 1.2 insert into set 1.3 insert into select_state ...

  9. [TJOI 2013]单词

    Description 题库链接 给出一篇文章的所有单词,询问每个单词出现的次数. 单词总长 \(\leq 10^6\) Solution 算是 \(AC\) 自动机的板子,注意拼成文章的时候要在单词 ...

  10. [COGS 2258][HZOI 2015]复仇的序幕曲

    Description 你还梦不梦痛不痛,回忆这么重你怎么背得动 ----序言 当年的战火硝烟已经渐渐远去,可仇恨却在阿凯蒂王子的心中越来越深 他的叔父三年前谋权篡位,逼宫杀死了他的父王,用铁血手腕平 ...