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

简介

虽然目前市面上有一些不错的加密相册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就基本介绍完毕了,由于代码较多,文中只能挑重点讲解,项目的源代码在文首的地址中可以找到,欢迎关注项目后续。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容