雖然目前市面上有一些不錯的加密相冊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
每個模型被傳遞到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以自身作為數據源,同時負責處理Cell長按事件,刪除相冊並將事件繼續傳遞到控制器來重新加載文件,數據源需要的數據由控制器加載沙盒文件來提供,綜上所述,collectionView需要的屬性如下。
@interface SGHomeView : UICollectionView
// 數據源需要的數據,由控制器負責加載
@property (nonatomic, strong) NSArray *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目錄來存儲原圖和縮略圖,一個文件結構的例子如下圖所示。
被高亮的行是某個賬戶的根目錄
vc+24LXEzsS8/sK3vrajrNLytMvKudPD0ru49rmkvt/A4MC0udzA7dXi0KnCt762o6yzxs6qPGNvZGU+U0dGaWxlVXRpbDwvY29kZT6jrLjDwOC4+b7d1cu7p7bUz/PAtLP1yry7r9K7uPbVy7unttTTprXEuPnEv8K8o6yxvr3a1tDTw7W9tcTK9NDUus23vbeoyOfPwqGjPC9wPg0KPHByZSBjbGFzcz0="brush:java;">
@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 *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];
}
}
本文主要介紹了相冊頁面的設計與實現,目前項目已經完成了圖片浏覽器,歡迎關注項目後續,項目的下載地址可以在文章開頭找到。