簡介
雖然目前市面上有一些不錯的加密相冊App,但不是內(nèi)置廣告,就是對上傳的張數(shù)有所限制。本文介紹了一個加密相冊的制作過程,該加密相冊將包括多密碼(輸入不同的密碼即可訪問不同的空間,可掩人耳目)、WiFi傳圖、照片文件加密等功能。目前項目和文章會同時前進,項目的源代碼可以在github上下載。
點擊前往GitHub
概述
上一篇文章主要介紹了相冊管理界面的設計與實現(xiàn)。本文主要介紹圖片瀏覽器設計的技術細節(jié)。
圖片瀏覽器設計
說明
之前嘗試了使用MWPhotoBrowser來處理多圖瀏覽與查看原圖,但有些地方不盡人意,遂自己做了一個圖片瀏覽器,為了以后將其做成框架,沒有將其耦合到工程里,而是作為一個框架跟隨工程一起開發(fā)的,目前已經(jīng)實現(xiàn)了原圖延遲加載、內(nèi)存優(yōu)化、批量刪除與保存等功能,該框架使用block來回調數(shù)據(jù)源方法,使用十分方便,目前仍在開發(fā)中,源碼在GitHub提供的工程目錄下Libs/SGPhtoBrowser可以找到。
本文主要介紹其實現(xiàn)細節(jié),限于篇幅只能介紹一部分,其余部分將在接下來的文章中一一介紹。
交互界面設計及說明
以下幾幅圖片展示了相冊的縮略圖展示、編輯與原圖查看功能。



在原圖查看頁面單擊可以隱藏導航欄和工具欄,雙擊切換原圖與適應屏幕的縮放狀態(tài),同時支持捏和手勢的縮放。為了優(yōu)化內(nèi)存,原圖查看時當前圖片以及左右兩側的圖片都加載了原圖,較遠處的圖片加載的是縮略圖,當左右滑動到縮略圖時,會去加載當前以及相鄰的原圖,并且將遠處的所有原圖替換為縮略圖。
圖片瀏覽器的總體設計
數(shù)據(jù)源模型設計
圖片瀏覽器的縮略圖瀏覽界面為collectionView,collectionView需要的數(shù)據(jù)為縮略圖,而原圖瀏覽時需要的是原圖,因此圖片瀏覽器的所需要的數(shù)據(jù)模型應該包含原圖地址與縮略圖地址,除此之外,為了標記照片的選中狀態(tài),模型中還應該有一個字段用于記錄是否選中。綜上所述,模型設計如下。
@interface SGPhotoModel : NSObject
@property (nonatomic, copy) NSURL *photoURL;
@property (nonatomic, copy) NSURL *thumbURL;
@property (nonatomic, assign) BOOL isSelected;
@end
數(shù)據(jù)源回調設計
常規(guī)的數(shù)據(jù)源回調都是通過代理方式,考慮到代理方式寫起來比較麻煩,代碼也比較分散,這里使用了block回調。數(shù)據(jù)源主要包含了三個方法,前兩個分別是去請求數(shù)據(jù)模型的數(shù)量和獲取特定位置的數(shù)據(jù)模型,第三個請求重新加載數(shù)據(jù),之所以存在第三個回調,是因為照片瀏覽器可能會刪除一些圖片,但他們沒有權限去操作模型數(shù)組(只能獲取特定位置的模型),因此需要請求數(shù)據(jù)源去刷新模型數(shù)據(jù)。具體設計如下。
typedef SGPhotoModel * (^SGPhotoBrowserDataSourcePhotoBlock)(NSInteger index);
typedef NSInteger (^SGPhotoBrowserDataSourceNumberBlock)(void);
typedef void(^SGPhotoBrowserReloadRequestBlock)(void);
@interface SGPhotoBrowser : UIViewController
@property (nonatomic, copy, readonly) SGPhotoBrowserDataSourceNumberBlock numberOfPhotosHandler;
@property (nonatomic, copy, readonly) SGPhotoBrowserDataSourcePhotoBlock photoAtIndexHandler;
@property (nonatomic, copy, readonly) SGPhotoBrowserReloadRequestBlock reloadHandler;
- (void)setNumberOfPhotosHandlerBlock:(SGPhotoBrowserDataSourceNumberBlock)handler;
- (void)setphotoAtIndexHandlerBlock:(SGPhotoBrowserDataSourcePhotoBlock)handler;
- (void)setReloadHandlerBlock:(SGPhotoBrowserReloadRequestBlock)handler;
@end
之所以設置成為readonly并手動提供setter,是因為系統(tǒng)提供的setter無法生成block的智能補全。
對外接口設計
為了方便使用瀏覽器,只需要繼承SGPhotoBrowser并實現(xiàn)數(shù)據(jù)源的block并提供數(shù)據(jù)源需要的數(shù)據(jù)模型即可,因此不需要多少額外的接口,但為了方便用戶自定義,提供了一個property來設置每行展示的照片數(shù),并且提供了reloadData方法,當模型數(shù)據(jù)變化時,要求照片瀏覽器重新加載模型刷新數(shù)據(jù),綜上所述,照片瀏覽器的完整設計如下。
typedef SGPhotoModel * (^SGPhotoBrowserDataSourcePhotoBlock)(NSInteger index);
typedef NSInteger (^SGPhotoBrowserDataSourceNumberBlock)(void);
typedef void(^SGPhotoBrowserReloadRequestBlock)(void);
@interface SGPhotoBrowser : UIViewController
@property (nonatomic, assign) NSInteger numberOfPhotosPerRow;
@property (nonatomic, copy, readonly) SGPhotoBrowserDataSourceNumberBlock numberOfPhotosHandler;
@property (nonatomic, copy, readonly) SGPhotoBrowserDataSourcePhotoBlock photoAtIndexHandler;
@property (nonatomic, copy, readonly) SGPhotoBrowserReloadRequestBlock reloadHandler;
- (void)setNumberOfPhotosHandlerBlock:(SGPhotoBrowserDataSourceNumberBlock)handler;
- (void)setphotoAtIndexHandlerBlock:(SGPhotoBrowserDataSourcePhotoBlock)handler;
- (void)setReloadHandlerBlock:(SGPhotoBrowserReloadRequestBlock)handler;
- (void)reloadData;
@end
縮略圖瀏覽實現(xiàn)
數(shù)據(jù)源處理
在圖片瀏覽器中有一個collectionView用于展示所有的圖片,圖片瀏覽器本身作為其數(shù)據(jù)源和代理,collectionView對數(shù)據(jù)源的請求通過瀏覽器向父類(由用戶繼承瀏覽器類實現(xiàn))去請求相應的block,其中numberOfItemsInSection:方法對應numberOfPhotosHandler,cellForItemAtIndexPath:方法對應photoAtIndexHandler,具體的實現(xiàn)如下。
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
NSAssert(self.numberOfPhotosHandler != nil, @"you must implement 'numberOfPhotosHandler' block to tell the browser how many photos are here");
return self.numberOfPhotosHandler();
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
NSAssert(self.photoAtIndexHandler != nil, @"you must implement 'photoAtIndexHandler' block to provide photos for the browser.");
SGPhotoModel *model = self.photoAtIndexHandler(indexPath.row);
SGPhotoCell *cell = [SGPhotoCell cellWithCollectionView:collectionView forIndexPaht:indexPath];
cell.model = model;
return cell;
}
這里通過斷言來防止用戶沒有實現(xiàn)相應的block,但這又引入了一個問題,可能在用戶設置block之前對numberOfItemsInSection:進行了回調,這就會報錯,為了防止這個問題,collectionView的數(shù)據(jù)源和代理在block被實現(xiàn)之后才會被啟用,具體的實現(xiàn)為在這兩個block的setter中檢查是否兩個block都已經(jīng)實現(xiàn),只有都實現(xiàn)了,才對collectionView的代理和數(shù)據(jù)源賦值,具體實現(xiàn)如下。
- (void)checkImplementation {
if (self.photoAtIndexHandler && self.numberOfPhotosHandler) {
self.collectionView.delegate = self;
self.collectionView.dataSource = self;
[self.collectionView reloadData];
}
}
- (void)setphotoAtIndexHandlerBlock:(SGPhotoBrowserDataSourcePhotoBlock)handler {
_photoAtIndexHandler = handler;
[self checkImplementation];
}
- (void)setNumberOfPhotosHandlerBlock:(SGPhotoBrowserDataSourceNumberBlock)handler {
_numberOfPhotosHandler = handler;
[self checkImplementation];
}
排布尺寸處理
為了保證照片以極小的間隔緊密排布,需要根據(jù)屏幕尺寸嚴格計算每個Cell的尺寸并通過collectionView的代理方法提供,尺寸的計算說明圖如下。

根據(jù)上圖,設屏幕寬度為width,每行的照片數(shù)量為n,則每個Cell的寬高=(width - (n - 1) * gutt - 2 * margin) / n。
具體的尺寸計算的實現(xiàn)如下。
// 初始化
- (void)initParams {
_margin = 0;
_gutter = 1;
self.numberOfPhotosPerRow = 3;
}
// 邊距
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
return UIEdgeInsetsMake(_margin, _margin, _margin, _margin);
}
// Cell尺寸
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
CGFloat value = (self.view.bounds.size.width - (self.numberOfPhotosPerRow - 1) * _gutter - 2 * _margin) / self.numberOfPhotosPerRow;
return CGSizeMake(value, value);
}
// Cell上下間距(行間距)
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
return _gutter;
}
// Cell左右間距(列間距)
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
return _gutter;
}
Cell設計
每個Cell都通過數(shù)據(jù)模型SGPhotoModel來顯示數(shù)據(jù),Cell上鋪滿了一個ImageView來顯示縮略圖,除此之外,為了處理選中,在ImageView上加一個遮蓋層,默認隱藏,通過設置sg_select(為了和系統(tǒng)的select區(qū)分)這一屬性來處理隱藏和現(xiàn)實。遮蓋層包含了一個半透明背景和一個選中的效果圖,它同樣被定義在Cell的類文件中,具體實現(xiàn)如下。
objective-c
@interface SGPhotoCell : UICollectionViewCell
@property (nonatomic, strong) SGPhotoModel *model;
// 處理選中
@property (nonatomic, assign) BOOL sg_select;
// 處理重用和快速創(chuàng)建Cell
- (instancetype)cellWithCollectionView:(UICollectionView *)collectionView forIndexPaht:(NSIndexPath *)indexPath;
@end
```objective-c
// 遮蓋層視圖的定義
@interface SGPhotoCellMaskView : UIView
// 遮蓋層圖片
@property (nonatomic, weak) UIImageView *selectImageView;
@end
@implementation SGPhotoCellMaskView
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [[UIColor grayColor] colorWithAlphaComponent:0.6f];
self.hidden = YES;
UIImage *selectImage = [UIImage imageNamed:@"SelectButton"];
UIImageView *selectImageView = [[UIImageView alloc] initWithImage:selectImage];
self.selectImageView = selectImageView;
[self addSubview:selectImageView];
}
return self;
}
// 遮蓋層圖片在右下角顯示
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat padding = 8;
CGFloat selectWH = 28;
CGFloat selectX = self.bounds.size.width - padding - selectWH;
CGFloat selectY = self.bounds.size.height - padding - selectWH;
self.selectImageView.frame = CGRectMake(selectX, selectY, selectWH, selectWH);
}
@end
// Cell的實現(xiàn)
@interface SGPhotoCell ()
// 包含縮略圖顯示的ImageView與選中的遮蓋層
@property (nonatomic, weak) UIImageView *imageView;
@property (nonatomic, weak) SGPhotoCellMaskView *selectMaskView;
@end
@implementation SGPhotoCell
+ (instancetype)cellWithCollectionView:(UICollectionView *)collectionView forIndexPaht:(NSIndexPath *)indexPath {
static NSString *ID = @"SGPhotoCell";
[collectionView registerClass:[SGPhotoCell class] forCellWithReuseIdentifier:ID];
SGPhotoCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:ID forIndexPath:indexPath];
return cell;
}
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
UIImageView *imageView = [UIImageView new];
// 使得縮略圖顯示適當?shù)牟糠? imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.clipsToBounds = YES;
self.imageView = imageView;
[self.contentView addSubview:imageView];
// 添加遮蓋層
SGPhotoCellMaskView *selectMaskView = [[SGPhotoCellMaskView alloc] initWithFrame:self.contentView.bounds];
[self.contentView addSubview:selectMaskView];
self.selectMaskView = selectMaskView;
}
return self;
}
- (void)setModel:(SGPhotoModel *)model {
_model = model;
NSURL *thumbURL = model.thumbURL;
if ([thumbURL isFileURL]) {
self.imageView.image = [UIImage imageWithContentsOfFile:thumbURL.path];
} else {
[self.imageView sd_setImageWithURL:thumbURL];
}
// 設置模型時根據(jù)模型設置選中狀態(tài)
self.sg_select = model.isSelected;
}
// 通過選中屬性的setter來處理遮蓋層的顯示與隱藏
- (void)setSg_select:(BOOL)sg_select {
_sg_select = sg_select;
self.selectMaskView.hidden = !_sg_select;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.imageView.frame = self.contentView.bounds;
}
@end
關于選中的具體邏輯將在下一篇文章介紹。
照片瀏覽器的使用
上文主要介紹了照片瀏覽器的縮略圖瀏覽界面的具體設計,這里將介紹如何使用該瀏覽器。
1.繼承照片瀏覽器類SGPhotoBrowser。
@interface SGPhotoBrowserViewController : SGPhotoBrowser
// 用于存儲當前用戶相冊文件系統(tǒng)的根目錄,在前面的文章中有介紹
@property (nonatomic, copy) NSString *rootPath;
@end
2.使用一個數(shù)組來保存所有的數(shù)據(jù)模型,數(shù)據(jù)模型通過沙盒中特定用戶的文件系統(tǒng)去加載。
@interface SGPhotoBrowserViewController ()
@property (nonatomic, strong) NSArray<SGPhotoModel *> *photoModels;
@end
3.實現(xiàn)數(shù)據(jù)源的三個block。
- (void)commonInit {
// 設置每行顯示的照片數(shù)
self.numberOfPhotosPerRow = 4;
// 根據(jù)根目錄去獲取文件夾名稱(用/分割路徑字符串,并取最后一個部分),作為控制器標題
self.title = [SGFileUtil getFileNameFromPath:self.rootPath];
// 用于添加圖片的按鈕,后續(xù)的文章會介紹
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addClick)];
WS(); // 創(chuàng)建weakSelf的宏,防止循環(huán)引用
// 實現(xiàn)圖片瀏覽器的數(shù)據(jù)源block
[self setNumberOfPhotosHandlerBlock:^NSInteger{
return weakSelf.photoModels.count;
}];
[self setphotoAtIndexHandlerBlock:^SGPhotoModel *(NSInteger index) {
return weakSelf.photoModels[index];
}];
[self setReloadHandlerBlock:^{
[weakSelf loadFiles];
}];
}
4.實現(xiàn)加載數(shù)據(jù)模型的方法
- (void)loadFiles {
NSFileManager *mgr = [NSFileManager defaultManager];
// 在每個相冊文件夾內(nèi),有原圖文件夾Photo和縮略圖文件夾Thumb,分別獲取路徑
NSString *photoPath = [SGFileUtil photoPathForRootPath:self.rootPath];
NSString *thumbPath = [SGFileUtil thumbPathForRootPath:self.rootPath];
NSMutableArray *photoModels = @[].mutableCopy;
// 掃描Photo文件夾,獲取所有原圖文件的名稱
NSArray *fileNames = [mgr contentsOfDirectoryAtPath:photoPath error:nil];
for (NSUInteger i = 0; i < fileNames.count; i++) {
NSString *fileName = fileNames[i];
// 原圖與縮略圖同名,因此可以同時拼接出原圖和縮略圖路徑
// 使用URL是為了框架后期能夠兼容網(wǎng)絡圖片
NSURL *photoURL = [NSURL fileURLWithPath:[photoPath stringByAppendingPathComponent:fileName]];
NSURL *thumbURL = [NSURL fileURLWithPath:[thumbPath stringByAppendingPathComponent:fileName]];
// 每個模型都包含了一張圖片的原圖與縮略圖路徑
SGPhotoModel *model = [SGPhotoModel new];
model.photoURL = photoURL;
model.thumbURL = thumbURL;
[photoModels addObject:model];
}
self.photoModels = photoModels;
// 調用父類的reloadData方法要求collectionView重新加載數(shù)據(jù)
[self reloadData];
}
總結
本文主要介紹了圖片瀏覽器的縮略圖展示部分的設計,項目的下載地址可以在文首找到。下一篇文章將會介紹圖片的選取批處理方法以及查看原圖的一些細節(jié),歡迎關注項目后續(xù)。