简介

虽然目前市面上有一些不错的加密相册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. 使用javaMail实现简单邮件发送

    一.首先你要用来发送邮件的qq邮箱需要开通pop3/smtp服务,这个可以百度一下就知道了 二.导入所需要的jar包,我使用的是maven添加依赖 <dependency> <gro ...

  2. CentOS在线安装Mysql5.7

    一.通过Yum命令安装 1.下载rpm安装源 官方地址:https://dev.mysql.com/downloads/repo/yum/ rpm文件地址:https://dev.mysql.com/ ...

  3. SQL将一个数据库中的数据复制到另一个数据库中

    复制表结构 首先,打开并连接Sql Server,在源数据库Source_db(源数据库名称)上右键,然后依次点击"编写表脚本为"→"CREATE到"→&quo ...

  4. Java 接口基础详解

    目录 Java接口示例 实现一个接口 接口实例 实现多个接口 方法签名重叠 接口变量 接口方法 接口默认方法 接口与继承 继承与默认方法 接口与多态性 在Java中,接口是一个抽象类型,有点类似于类, ...

  5. [ZJOI 2006]书架

    Description 小T有一个很大的书柜.这个书柜的构造有些独特,即书柜里的书是从上至下堆放成一列.她用1到n的正整数给每本书都编了号. 小T在看书的时候,每次取出一本书,看完后放回书柜然后再拿下 ...

  6. bzoj 5289: [Hnoi2018]排列

    Description Solution 首先注意到实际上约束关系构成了一棵树 考虑这个排列 \(p\),编号为 \(a[i]\) 的出现了,\(i\) 才可以出现 那么如果连边 \((a[i],i) ...

  7. 51 nod 1766 树上的最远点对(线段树+lca)

    1766 树上的最远点对 基准时间限制:3 秒 空间限制:524288 KB 分值: 80 难度:5级算法题   n个点被n-1条边连接成了一颗树,给出a~b和c~d两个区间,表示点的标号请你求出两个 ...

  8. ●BZOJ 4361 isn

    题链: http://www.lydsy.com/JudgeOnline/problem.php?id=4361 题解: 容斥,DP,树状数组 注意题意:一旦变成了非降序列,就停止操作.即对非降序列进 ...

  9. hdu5631 BestCoder Round #73 (div.2)

    Rikka with Graph  Accepts: 123  Submissions: 525  Time Limit: 2000/1000 MS (Java/Others)  Memory Lim ...

  10. C++ 实参和形参

    形参:在函数没有调用的时候,函数的形参并不占据实际的内存空间,也没有实质的值,--正如字面意思那样,"形式"参数,只是一个"形式. 实参:当函数被调用的时候,系统会为形式 ...