简介

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

点击前往GitHub

概述

上一篇文章主要介绍了登录与注册页面的设计。这一篇文章将会介绍相册的设计与实现。

相册设计与实现

交互设计

相册的主界面如下。

点击Add按钮可以添加一个相册文件夹,通过弹出的AlertView来命名,界面如下。

长按一个已有的相册可以进行删除操作,界面如下。

文件结构

相册使用了MVC设计模式,为了方便排布,使用了UICollectionView,文件结构如下。

模型类设计

模型类为SGAlbum,每个模型对应一个相册,存储相册的名字、存储路径,封面图路径,注意到前面的页面设计中添加相册的按钮也作为一个Cell存在,因此在模型类中有一个属性描述是否是添加相册按钮。综上所述,设计如下。

typedef NS_OPTIONS(NSInteger, SGAlbumType) {
SGAlbumButtonTypeCommon = 0,
SGAlbumButtonTypeAddButton
}; @interface SGAlbum : NSObject @property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSString *coverImageURL;
@property (nonatomic, assign) SGAlbumType type; @end

Cell设计

每个模型被传递到collectionView的Cell并且显示出来,每个Cell都是一个UICollectionCell的子类,它的子视图包括了背景图、相册封面图、相册名称标签三个部分,如下。

@interface SGHomeViewCell ()

@property (nonatomic, weak) UIImageView *backgroundImageView;
@property (nonatomic, weak) UIImageView *thumbImageView;
@property (nonatomic, weak) UILabel *nameLabel; @end

为了处理长按删除这一事件,为Cell的contentView添加一个LongPress事件,而最后该事件会交给collectionView处理,collectionView接收到消息后显示UIActionSheet来让用户确认操作,操作被确认后选择的相册文件夹将会被删除,同时消息会继续传递到控制器,来重新加载文件,这两次消息传递均通过block完成,在Cell上提供了一个block来回调到collectionView,由于直接对block赋值无法获得智能补全提示,因此将block作为私有属性,写一个单独的setter来设置回调,代码如下。

// block setter
- (void)setAction:(SGHomeViewCellActionBlock)actionBlock; // private block property
@property (nonatomic, copy) SGHomeViewCellActionBlock actionBlock; // add gesture
UILongPressGestureRecognizer *press = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(press:)];
press.minimumPressDuration = 0.5f;
[self.contentView addGestureRecognizer:press]; // gesture handler
- (void)press:(UILongPressGestureRecognizer *)ges {
// 第一个相册为添加按钮,不用处理长按删除事件
if (ges.state != UIGestureRecognizerStateBegan) return;
if (self.album.type == SGAlbumButtonTypeCommon) {
if (self.actionBlock) {
self.actionBlock();
}
}
}

通过重写setter来监听模型的传递,当模型被传递到Cell时,使用模型中的数据去渲染视图,如果没有提供封面图,则使用默认封面,代码如下。

- (void)setAlbum:(SGAlbum *)album {
_album = album;
if (album.type == SGAlbumButtonTypeAddButton) {
self.thumbImageView.image = [UIImage imageNamed:@"AlbumAddButton"];
self.nameLabel.text = @"Add";
} else {
UIImage *thumb = [UIImage imageNamed:album.coverImageURL ?: @"AlbumCover_placeholder"];
self.thumbImageView.image = thumb;
self.nameLabel.text = album.name;
}
}

为了减少collectionView的代码量,将Cell的创建和复用的逻辑放到Cell中。代码如下。

+ (instancetype)cellWithCollectionView:(UICollectionView *)collectionView forIndexPath:(NSIndexPath *)indexPath {
static NSString *ID = @"SGHomeViewCell";
// register方法保证了dequeue方法无可复用时创建一个新的Cell
[collectionView registerClass:[SGHomeViewCell class] forCellWithReuseIdentifier:ID];
SGHomeViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
return cell;
}

CollectionView设计

collectionView以自身作为数据源,同时负责处理Cell长按事件,删除相册并将事件继续传递到控制器来重新加载文件,数据源需要的数据由控制器加载沙盒文件来提供,综上所述,collectionView需要的属性如下。

@interface SGHomeView : UICollectionView
// 数据源需要的数据,由控制器负责加载
@property (nonatomic, strong) NSArray<SGAlbum *> *albums;
// 用于向控制器二次传递Cell的长按事件,处理方式与Cell设计中一样
- (void)setAction:(SGHomeViewNeedReloadActionBlock)actionBlock;
@end

collectionView在数据源获取Cell时来设计block回调,代码如下。

- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
SGHomeViewCell *cell = [SGHomeViewCell cellWithCollectionView:collectionView forIndexPath:indexPath];
SGAlbum *album = self.albums[indexPath.row];
cell.album = album;
WS(); // 创建weakSelf的宏,防止循环引用
[cell setAction:^{
UIActionSheet *ac = [[UIActionSheet alloc] initWithTitle:@"Operation" delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Delete" otherButtonTitles:nil];
[ac showInView:self.superview];
// currentSelectAlbum用于记录当前相册模型,以便actionSheet的回调里删除对应的相册。
weakSelf.currentSelectAlbum = album;
}];
return cell;
}

由于相册模型已经提供了存储路径,因此删除十分方便,删除后通过block将事件继续会传到控制器,代码如下。

- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex {
if(buttonIndex == 0) {
[[NSFileManager defaultManager] removeItemAtPath:self.currentSelectAlbum.path error:nil];
if (self.actionBlock) {
self.actionBlock();
}
}
}

控制器设计

文件系统设计

控制器主要负责处理文件的加载,相册在沙盒中的存储如下所述。

相册的根目录为Documents目录,此目录的文件可以被iCloud备份以防止数据丢失。 为了区分不同密码对应的不同账户,以每个密码加密后的名称作为文件夹名称,这些文件夹作为不同账户的相册根目录。每个账户下创建的相册都会以实体文件夹的形式存在于各自的根目录下。同时每个相册中分Photo和Thumb目录来存储原图和缩略图,一个文件结构的例子如下图所示。

被高亮的行是某个账户的根目录

由于涉及了较多的文件路径,因此使用一个工具类来管理这些路径,称为SGFileUtil,该类根据账户对象来初始化一个账户对应的根目录,本节中用到的属性和方法如下。

@interface SGFileUtil : NSObject

@property (nonatomic, strong) SGAccount *account;
@property (nonatomic, copy, readonly) NSString *rootPath; + (instancetype)sharedUtil; @end

通过重写account的setter实现rootPath根据当前account来变化,如果rootPath这一文件夹不存在,则创建出来,代码如下。

- (void)setAccount:(SGAccount *)account {
_account = account;
_rootPath = [DocumentPath stringByAppendingPathComponent:account.password];
NSFileManager *mgr = [NSFileManager defaultManager];
if (![mgr fileExistsAtPath:_rootPath isDirectory:nil]) {
[mgr createDirectoryAtPath:_rootPath withIntermediateDirectories:NO attributes:nil error:nil];
}
}

控制器的业务逻辑

在视图加载时,首先将collectionView添加到控制器视图上,并且设置collectionView的block回调,用于处理Cell长按事件回传。接下来要处理文件的加载。

- (void)viewDidLoad {
[super viewDidLoad];
[self setupView];
[self loadFiles];
} - (void)setupView {
self.title = @"Agony";
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
[layout setScrollDirection:UICollectionViewScrollDirectionVertical];
SGHomeView *view = [[SGHomeView alloc] initWithFrame:(CGRect){0, 0, [UIScreen mainScreen].bounds.size} collectionViewLayout:layout];
view.alwaysBounceVertical = YES;
view.delegate = self;
WS(); // 用于创建weakSelf的宏,防止循环引用
[view setAction:^{
[weakSelf loadFiles];
}];
self.homeView = view;
[self.view addSubview:view];
} - (void)loadFiles {
SGFileUtil *util = [SGFileUtil sharedUtil];
NSString *rootPath = util.rootPath;
NSFileManager *mgr = [NSFileManager defaultManager];
NSMutableArray<SGAlbum *> *albums = @[].mutableCopy;
// 第一个相册为添加相册按钮,手动添加
SGAlbum *addBtnAlbum = [SGAlbum new];
addBtnAlbum.type = SGAlbumButtonTypeAddButton;
[albums addObject:addBtnAlbum];
// 其他相册从当前账户的根目录搜索
NSArray *fileNames = [mgr contentsOfDirectoryAtPath:rootPath error:nil];
for (NSUInteger i = 0; i < fileNames.count; i++) {
NSString *fileName = fileNames[i];
SGAlbum *album = [SGAlbum new];
album.name = fileName;
album.path = [[SGFileUtil sharedUtil].rootPath stringByAppendingPathComponent:fileName];
[albums addObject:album];
}
self.homeView.albums = albums;
[self.homeView reloadData];
}

控制器同时作为collectionView的代理,处理尺寸与点击事件,代码如下。

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(100, 100);
}
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(5, 10, 5, 10);
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
SGAlbum *album = self.homeView.albums[indexPath.row];
// 如果是添加相册按钮,则弹出alertView来处理新建相册事件
// 否则启动照片浏览器,来显示相册内容,照片浏览器在下一篇文章介绍
if (album.type == SGAlbumButtonTypeAddButton) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"New Folder" message:@"Please enter folder name" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
alertView.alertViewStyle = UIAlertViewStylePlainTextInput;
[alertView show];
} else {
SGPhotoBrowserViewController *browser = [SGPhotoBrowserViewController new];
browser.rootPath = album.path;
[self.navigationController pushViewController:browser animated:YES];
}
}

在alertView的回调中处理文件夹创建,文件夹名不能为空,并且不能重复,检查完毕后创建文件夹,并且重新加载文件。

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 1) {
NSString *folderName = [alertView textFieldAtIndex:0].text;
if (!folderName.length) {
[MBProgressHUD showError:@"Folder Name Cannot be Empty"];
return;
}
NSFileManager *mgr = [NSFileManager defaultManager];
NSString *folderPath = [[SGFileUtil sharedUtil].rootPath stringByAppendingPathComponent:folderName];
if ([mgr fileExistsAtPath:folderPath isDirectory:nil]) {
[MBProgressHUD showError:@"Folder Exists"];
return;
}
[mgr createDirectoryAtPath:folderPath withIntermediateDirectories:NO attributes:nil error:nil];
[self loadFiles];
}
}

总结

本文主要介绍了相册页面的设计与实现,目前项目已经完成了图片浏览器,欢迎关注项目后续,项目的下载地址可以在文章开头找到。

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开源项目(一~三)

    结束了GitHub平台上“最受欢迎的Android开源项目”系列盘点之后,我们正式迎来了“GitHub上最受欢迎的iOS开源项目”系列盘点.今天,我们将介绍20个在GitHub上非常受开发者欢迎的iO ...

  9. 直接拿来用!最火的iOS开源项目(三)

    相比Android,GitHub上的iOS开源项目更可谓是姹紫嫣红.尽管效果各异,但究其根源,却都是因为开发者本身对于某种效果的需求以及热爱.在“直接拿来用!最火的iOS开源项目”系列文章(一).(二 ...

随机推荐

  1. python操作mongodb

    # python操作mongodb # 首先,引入第三方模块pymongo,该模块是python用来操作mongodb的 import pymongo # 第二步,设置ip地址,以及表格名称,表格名字 ...

  2. ajax处理跨域有几种方式

    一.什么是跨域 同源策略是由Netscape提出的著名安全策略,是浏览器最核心.基本的安全功能,它限制了一个源(origin)中加载文本或者脚本与来自其他源(origin)中资源的交互方式,所谓的同源 ...

  3. Paper藐小之处明察秋毫故时有物外之趣

    暂且针对第一篇叶脉提取的paper 插入图像的"图 N": 英 Times New Roman, 中 宋体, 10磅. 文末的引文: 两端对齐. 流程图框格内文字换行时, 忌: 将 ...

  4. 学习HTML的第一次课

    1.母板中的标签的含义 1.1< Doctype html/ > 声明文档类型为HTML 1.2< meta charset="UTF-8"/ > 设当前网 ...

  5. C#之冒泡排序

    以前在学校的时候看过冒泡排序,看的时候挺明白的,但是自己写的时候就写不出来,在网上搜索了一下,发现网上的冒泡排序算法几乎都不符合冒泡排序的原理,虽然也能实现,但是不正宗. 冒泡排序从字面意思理解:应该 ...

  6. Java基础知识回顾之三 ----- 封装、继承和多态

    前言 在上一篇中回顾了java的修饰符和String类,这篇就来回顾下Java的三大特性:封装.继承.多态. 封装 什么是封装 在面向对象程式设计方法中,封装是指一种将抽象性函式接口的实现细节部份包装 ...

  7. [ZJOI 2007]时态同步

    Description 小Q在电子工艺实习课上学习焊接电路板.一块电路板由若干个元件组成,我们不妨称之为节点,并将其用数字1,2,3….进行标号.电路板的各个节点由若干不相交的导线相连接,且对于电路板 ...

  8. [HNOI2016]大数

    题目描述 小 B 有一个很大的数 S,长度达到了 N 位:这个数可以看成是一个串,它可能有前导 0,例如00009312345.小B还有一个素数P.现在,小 B 提出了 M 个询问,每个询问求 S 的 ...

  9. [Ahoi2005]LANE 航线规划

    题目描述 对Samuel星球的探险已经取得了非常巨大的成就,于是科学家们将目光投向了Samuel星球所在的星系——一个巨大的由千百万星球构成的Samuel星系. 星际空间站的Samuel II巨型计算 ...

  10. TopCoder SRM 560 Div 1 - Problem 1000 BoundedOptimization & Codeforces 839 E

    传送门:https://284914869.github.io/AEoj/560.html 题目简述: 定义"项"为两个不同变量相乘. 求一个由多个不同"项"相 ...