六天完成一個簡單iOS App - 第五天

第五天任務

今天主要完成精華頁面中cell內內容的處理。

  1. cell高度的計算
  2. cell中間內容的顯示
  3. 精華模塊的重構
  4. 查看圖片
  5. 保存圖片到相冊

cell高度的計算

cell間距的設置,每個cell之間有10的間距,因為cell的重用機制,我們發現即使在tableView :didDeselectRowAtIndexPath方法中通過點擊cell,減少cell的高度,當cell重新顯示的時候還是會變回原來的高度,并且系統內部對cell進行了一些處理,已經在內部設置好cell的frame,所以我們通過重寫cell的setFrame方法對系統設置cell的frame進行攔截,我們先做一些處理,然后在讓系統設置。

//重寫這個方法的目的: 能夠攔截所有設置cell frame的操作
- (void)setFrame:(CGRect)frame
{
    // 先設置cell的高度減10,然后在讓系統內部設置。
    frame.size.height -= XMGMargin;
    [super setFrame:frame];
}

cell高度的計算
cell的高度計算需要根據每一個cell的不同內容進行計算,模型中添加type屬性,用來區別cell中間內容。這里使用枚舉。

typedef NS_ENUM(NSInteger , CLTopicType) {
    /** 全部 */
    CLTopicTypeAll = 1,
    /** 圖片 */
    CLTopicTypePicture = 10,
    /** 段子 */
    CLTopicTypeWord = 29,
    /** 音頻 */
    CLTopicTypeVoice = 31,
    /** 視頻 */
    CLTopicTypeVideo = 41,
};
/** 中間內容類型 */
@property(nonatomic,assign)CLTopicType type;

另外我們可以將cell高度計算分為五部分,看下圖


cell高度計算分析

而cell的內容,文字,圖片高度等只能在模型中拿到,所以在模型中添加cellHeight屬性和contentF屬性,重寫cellHeight的get方法計算cell的高度。并且將計算好中間內容的fram用contentF存儲起來,用來之后在cell中設置中間內容的frame。

計算高度的代碼,其中需要注意的地方都已經寫了注釋。

// cell高度的計算
-(CGFloat)cellHeight
{
    // iOS8 開始cell不會緩存 cell的高度,每次顯示cell都會來到這里計算一下,造成不必要計算
    // 如果計算過一次高度就不要在計算了,直接返回模型的高度即可。
   // if (_cellHeight)return _cellHeight;

    if (_cellHeight == 0) {
        // 1.頭像高度
        _cellHeight = 56;
        // 2.文字高度 需要提供文字的大小和顯示的寬度
        CGFloat textMaxW = [UIScreen mainScreen].bounds.size.width - 2 * CLMargin;
        // 需要給一個較大值的高度,如果計算出來的高度小于這個高度就會使用計算出來的高度
        CGSize textMaxSize = CGSizeMake(textMaxW, MAXFLOAT);
    //    CGSize textSize = [self.text sizeWithFont:[UIFont systemFontOfSize:15] constrainedToSize:textMaxSize];
        CGSize textSize = [self.text boundingRectWithSize:textMaxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15]} context:nil].size;
        _cellHeight += textSize.height + CLMargin;
        // 3. 圖片的高度,需要判斷有沒有圖片顯示
        if (self.type != CLTopicTypeWord) {
            // 圖片高度需要根據能顯示的最大寬度等比進行計算 中間內容高度 = 中間內容寬度 * 圖片實際高度 / 圖片實際寬度
            CGFloat Height = textMaxW * self.height / self.width;
            // 判斷是否是大圖,如果是大圖則高度設置為250
            if (Height >= [UIScreen mainScreen].bounds.size.height) {
                Height = 250;
                self.isBigPicture = YES;
            }
            self.contentF = CGRectMake(CLMargin, _cellHeight, textMaxW, Height);
            _cellHeight += Height + CLMargin;
        }
        // 4. 最熱評論高度計算
        if (self.top_cmt) {
            // 4.1 最熱評論標題高度 18
            _cellHeight += 18;
            // 如果最熱評論是音頻
            NSString *contentText = topic.top_cmt.content;
            // 如果音頻url有長度,說明是語音評論,需要將高度計算其中,防止用戶名過長,熱門評論高度計算不對
             if (topic.top_cmt.voiceuri.length) {
                   contentText = @"[語音消息]";
              }
            // 4.2 最熱評論內容高度
            NSString *topCmtContent = [NSString stringWithFormat:@"%@ : %@",self.top_cmt.user.username,contentText];  
    //        CGSize topCmtContentSize = [topCmtContent sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:textMaxSize];  
            CGSize topCmtContentSize = [topCmtContent boundingRectWithSize:textMaxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:14]} context:nil].size;
            _cellHeight += topCmtContentSize.height + CLMargin;
        }
        // 5. 底部工具條 + cell之間的間距10
        _cellHeight += 35 + CLMargin;
    }
    return _cellHeight;
}

最后在tableView: heightForRowAtIndexPath:中拿到模型直接返回cellHeight即可

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 直接返回cell的高度即可
    return self.topicArr[indexPath.row].cellHeight;
}

至此cell的高度已經根據每個cell顯示內容不同而決定,在這里需要強調一個問題:cell的高度沒有必要再每次顯示的時候都重新計算一遍,所以先對cellHeight進行判斷,如果有值則直接返回即可,沒有值在進行計算,避免不必要且耗時的計算。

cell中間內容的顯示

cell中間內容分為四大模塊,視頻、音頻、圖片、段子。段子沒有圖片顯示,我們使用xib來分別描述視頻,音頻,和圖片的顯示。如圖

視頻xib
音頻xib
圖片xib

這里圖片里面又分為普通圖片、gif圖片、長圖。需要根據圖片的不同判斷gif標識ImageView和點擊查看大圖Button是否隱藏。

因為之前在計算cell高度的時候使用模型中屬性contentF存儲了中間內容的frame,在CLTopicCell中的setTopic:方法中通過判斷中間內容的類型,決定顯示的內容

#pragma mark - 中間數據類型
    if (topic.type == CLTopicTypeVideo) {
        self.videoView.hidden = NO;
        self.videoView.frame = topic.contentF;
        self.videoView.topic = topic;
        self.voiceView.hidden = YES;
        self.pictureView.hidden = YES;
    }else if (topic.type == CLTopicTypeVoice){
        self.videoView.hidden = YES;
        self.voiceView.hidden = NO;
        self.voiceView.frame = topic.contentF;
        self.voiceView.topic = topic;
        self.pictureView.hidden = YES;
    }else if (topic.type == CLTopicTypeWord){
        self.videoView.hidden = YES;
        self.voiceView.hidden = YES;
        self.pictureView.hidden = YES;
    }else if (topic.type == CLTopicTypePicture){
        self.videoView.hidden = YES;
        self.voiceView.hidden = YES;
        self.pictureView.hidden = NO;
        self.pictureView.frame = topic.contentF;
        self.pictureView.topic = topic;
    }

注意:因為cell的重用機制,所以中間內容為video的cell很有可能重用到其他內容的cell中,所以需要顯示自己內容的同時,隱藏其他兩種內容的view,防止發生錯亂,其中段子cell中沒有圖片顯示,需要將其他三種cell的view全部隱藏。

另外:在這里根據模型中存儲的中間內容的frame設置中間內容view的frame,此時發現,雖然我們計算好的中間內容的frame是正確的,但是顯示在cell中的frame,只有x,y值正確,width和height不正確。
這是因為在xib中使用了自動布局,從xib中加載進來的控件的autoresizingMask默認是UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight ,會自動將控件根據父控件進行伸縮,所以造成了width和height不正確。只需要在-(void)awakeFromNib方法中取消其伸縮效果即可self.autoresizingMask = UIViewAutoresizingNone;

中間內容圖片的顯示

中間內容的圖片url可以通過模型拿到,所以給三種類型的View添加模型屬性,并在cell中根據類型設置view顯示的時候,將模型賦值給view的模型屬性,拿到模型屬性即可拿到中間圖片的url。視頻和音頻服務器也提供一張圖片供顯示,根據服務器返回得圖片url賦值給iamgeView即可。

圖片的設置稍有些復雜,數據庫返回給我們三種圖片,小圖,中圖和原圖,我們這里先使用原圖。在View的setTopic方法中設置imageView的圖片即可。

其中圖片需要添加判斷是否為gif圖片和是否為長圖。

// 判斷是否為gif
if (topic.is_gif) {
    self.gifImageView.hidden = NO;
}else{
    self.gifImageView.hidden = YES;
}
// 判斷是否為大圖
if(topic.isBigPicture){
    self.seeBigButton.hidden = NO;
    // 設置imageView的裁剪,以顯示頂部為準
    self.imageView.contentMode = UIViewContentModeTop;
    self.imageView.clipsToBounds = YES;
}else{
    self.seeBigButton.hidden = YES;
    // 如果不是需要將大圖的設置還原,防止cell重用設置
    self.imageView.contentMode = UIViewContentModeScaleToFill;
    self.imageView.clipsToBounds = NO;
}

其中判斷gif服務器提供了是否為gif的屬性,直接判斷即可,判斷是否為大圖,需要我們自己添加isBigPicture屬性,并且回到設置cell高度的方法,如果中間內容的高度超過一個屏幕高度,則表示是長圖,設置isBigPicture為YES。并且同樣需要注意cell重用的問題,設置顯示gif標識和查看大圖button顯示就需要在相對的方法中設置隱藏,防止cell重用時發生錯亂。

中間內容顯示的一些細節處理
此時中間內容已經可以顯示,但是還是需要做一些細節處理。

  1. 長圖顯示的處理,此時我們看到的長圖的顯示是這樣的


    未處理長圖顯示

    圖片被壓縮填充在ImageView中,此時在判斷如果是長圖的方法中修改imageView的contentMode即可

// 設置imageView的內容以頂端對齊顯示,多余的會被裁剪掉
self.imageView.contentMode = UIViewContentModeTop;

同樣,防止cell重用發生錯亂如果不是長圖需要設置

self.imageView.contentMode = UIViewContentModeScaleToFill;
  1. 圖片顯示進度條,進度條使用的DACircularProgress第三方。方法非常簡單,這里不在贅述。可以使用sd的方法監聽下載進度。
// 設置圖片并顯示進度
[self.imageView sd_setImageWithURL:[NSURL URLWithString:topic.large_image]placeholderImage:nil options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize) {
         // receivedSize :已經下載的進度
         // expectedSize :完整大小
         CGFloat progress =1.0 * receivedSize / expectedSize;
         self.progressView.progress = progress;
         self.progressView.progressLabel.text = [NSString stringWithFormat:@"%.0f%%",progress * 100];
         self.progressView.hidden = NO;
} completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
         self.progressView.hidden = YES;
}];
  1. 視頻和音頻view播放次數,播放次數的顯示非常簡單,并且視頻和音頻一樣,只不過修改一下控件即可
-(void)setTopic:(CLTopic *)topic
{
          _topic = topic;
          [self.imageView sd_setImageWithURL:[NSURL URLWithString:topic.large_image]];
          NSInteger minute = topic.videotime / 60;
          NSInteger second = topic.videotime % 60;
          self.videoTimeLabel.text = [NSString stringWithFormat:@"%02zd:%02zd",minute , second];
          self.playCountLabel.text = [NSString stringWithFormat:@"%zd次播放",topic.playcount];
}
  1. 監控網絡狀態對顯示圖片進行簡單優化
    前面提到過服務器返回給我們的圖片數據有三種小圖,中圖,大圖,我們可以使用AFN對用戶當前網絡進行判斷,如果當前用戶使用的是蜂窩網絡,則加載小圖,為用戶節省流量,同時也加快cell中圖片顯示的速度。如果用戶在wifi環境下,則加載原圖,提高圖片清晰度。如果用戶當前沒有網絡,則提醒用戶沒有網絡。
//使用AFN進行網絡判斷
AFNetworkReachabilityStatus status = [AFNetworkReachabilityManager sharedManager].networkReachabilityStatus;
if (status == AFNetworkReachabilityStatusReachableViaWWAN) { // 手機自帶網絡
    [self.imageView sd_setImageWithURL:[NSURL URLWithString:topic.small_image]];
} else if (status == AFNetworkReachabilityStatusReachableViaWiFi) { // WIFI
    [self.imageView sd_setImageWithURL:[NSURL URLWithString:topic.large_image]];
} else { // 網絡有問題, 清空當前顯示的圖片
    self.imageView.image = nil;
}

注意:模擬器無法區分網絡狀態,需要真機測試。

精華模塊的重構

全部界面完成之后,我們發現之后的視頻,音頻,圖片,段子的頁面顯示非常簡單,直接將全部界面的代碼復制過去,修改數據請求的參數即可,1為全部,41為視頻,31為音頻,10為圖片,29為段子。但是這樣一來,造成了大量的重復代碼,精華控制器的5個子控制器內代碼基本相同,此時可以使用繼承來重構代碼。
創建基類CLTopicViewController繼承自UITableViewController,其他五個子類繼承CLTopicViewController,同樣將代碼復制過來。
重構的方法很多種,我們通過比較選擇最好的一種。

  1. 通過判斷控制器的類型,根據不同控制器類型不同,確定不同的請求參數
if ([NSStringFromClass(self.class) isEqualToString:@"CLAllViewController"]) {
    params[@"type"] = @"1";
} else if ([NSStringFromClass(self.class) isEqualToString:@"CLVideoViewController"]) {
    params[@"type"] = @"41";
} else if ([NSStringFromClass(self.class) isEqualToString:@"CLVoiceViewController"]) {
    params[@"type"] = @"31";
} else if ([NSStringFromClass(self.class) isEqualToString:@"CLPictureViewController"]) {
    params[@"type"] = @"10";
} else if ([NSStringFromClass(self.class) isEqualToString:@"CLWordViewController"]) {
    params[@"type"] = @"29";
}

缺點:需要些大量繁瑣代碼,下拉刷新和上拉加載都需要重新判斷一遍,并且這里由父控制器來設置子控制器的type,違背了誰的內容由其自己管理的代碼原則。

  1. 給基類添加一個type屬性
/** 帖子的類型 */
// @property (nonatomic, assign) CLTopicType type;

然后我們在給主控制器添加子控制器的時候就可以設置子控制器的 type屬性,舉一個例子,其他子控制器相同。

CLAllViewController *all = [[CLAllViewController alloc]init];
all.type = CLTopicTypeAll;
[self addChildViewController:all];   

其實此時我們直接使用基類即可,主控制器中添加5個基類控制器,每個基類控制器的type屬性不同,但是這樣做很有局限性,如果之后有需求需要往子控制器中添加單獨的控件,或者個性化設置,還是需要在基類中進行判斷,延展性非常不好。

  1. 通過重寫基類type屬性的get方法
    基類中提供type的get方法,我們可以在子類中重寫基類的get方法,返回type,get方法只能子類可以重寫,其他類也沒有辦法改變子類的type。保證了父類中的某個內容, 只允許由子類來修改或提供, 不能由外界來修改或提供,并且我們可以在子類中對子類單獨的界面做一些個性化的設置,延展性非常好。
 - (CLTopicType)type
{
    return CLTopicTypeAll;
}

其實也可以為屬性添加readonly,這個屬性會生成一個type的get方法和 _type成員變量,相較于上面的方法,多創建了沒有必要的成員變量。并且需要考慮代碼順序問題,如果在父類中對type屬性有一些調用,則會出現問題,因為type在super方法之后設置。

至此我們通過繼承并重寫type的get方法對精華模塊進行了重構。子控制器內的代碼變得非常簡單,只需要重寫覆蓋父類的get方法即可,并且可以在子類中對子類進行一些個性化的設置。

查看圖片

對于圖片cell,點擊圖片會Mode出一個控制器來顯示圖片,同樣使用xib來描述圖片顯示控制器,創建CLSeeBigViewController控制器,通過xib描述控制器view


CLSeeBigViewController的view

在圖片的view,CLTopicPictureView中為中間顯示圖片的iamgeView添加點擊事件,imageView默認不支持交互,需要開啟交互。

self.imageView.userInteractionEnabled = YES;
[self.imageView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(seeBig)]];
- (void)seeBig
{
    CLSeeBigViewController *seeBig = [[CLSeeBigViewController alloc] init];
    seeBig.topic = self.topic;
    [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:seeBig animated:YES completion:nil];
}

接下來需要在CLSeeBigViewController中進行一些判斷,首先有可能是長圖,長圖的長度肯定超過一個屏幕大小,所以CLSeeBigViewController中需要使用scrollView來顯示長圖,因為xib中已經在CLSeeBigViewController的view上添加了返回和保存按鈕,所以scrollView需要使用insertSubview:atIndex添加在最底層,防止后加入的scrollView覆蓋擋住返回和保存按鈕。并在scrollView中添加imageView。
對圖片的長度進行計算,如果長度沒有超過一個屏幕大小,則根據屏幕的寬高比計算出圖片的高度,居中顯示在屏幕中,保證imageView占據整個屏幕的寬度。如果長度超過一個屏幕大小,則設置imageView的y值為0,scrollView的contentSize橫向為0,縱向為圖片的高度。
最后通過scrollView的代理方法對imageView的縮放比例進行設置。

#import "CLSeeBigViewController.h"
#import "CLTopic.h"
#import <UIImageView+WebCache.h>
@interface CLSeeBigViewController ()<UIScrollViewDelegate>
/** 圖片控件 */
@property (nonatomic, weak) UIImageView *imageView;
@end

@implementation CLSeeBigViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // scrollView
    UIScrollView *scrollView = [[UIScrollView alloc] init];
    scrollView.delegate = self;
    scrollView.frame = [UIScreen mainScreen].bounds;
    [self.view insertSubview:scrollView atIndex:0];
    // imageView
    UIImageView *imageView = [[UIImageView alloc] init];
    [imageView sd_setImageWithURL:[NSURL URLWithString:self.topic.large_image]];
    [scrollView addSubview:imageView];
    imageView.cl_width = scrollView.cl_width;
    imageView.cl_height = self.topic.height * imageView.cl_width / self.topic.width;
    imageView.cl_x = 0;
    if (imageView.cl_height >= scrollView.cl_height) { // 圖片高度超過整個屏幕
        imageView.cl_y = 0;
        // 滾動范圍
        scrollView.contentSize = CGSizeMake(0, imageView.cl_height);
    } else { // 居中顯示
        imageView.cl_centerY = scrollView.cl_height * 0.5;
    }
    self.imageView = imageView;
    // 縮放比例
    CGFloat scale =  self.topic.width / imageView.cl_width;
    if (scale > 1.0) {
        scrollView.maximumZoomScale = scale;
    }
}
- (IBAction)back {
    [self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)save {
}
#pragma mark - <UIScrollViewDelegate>
//返回一個scrollView的子控件進行縮放
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
    return self.imageView;
}
@end

保存圖片到相冊

保存圖片到相冊需要用到的框架

#import <AssetsLibrary/AssetsLibrary.h> // iOS9開始廢棄
#import <Photos/Photos.h> // iOS9開始推薦

首先來看一下系統相簿的內容

系統相簿

如果僅僅是將圖片保存到系統中相機膠卷相簿中,<AssetsLibrary/AssetsLibrary.h>提供了非常簡單的函數。

UIImageWriteToSavedPhotosAlbum(self.imageView.image, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);

這個函數中的SEL方法必須按照一定格式傳三個參數才可以,方法內部已經給出說明


UIImageWriteToSavedPhotosAlbum - API

訪問系統相冊需要獲得用戶授權,且只會請求一次,如果用戶點擊了不允許,則永遠不允許訪問相冊,此時需要提醒用戶去[設置]-[隱私]-[照片]中開啟。


取得用戶授權

我們這里想要實現將圖片保存到項目自己創建的相簿中,其實將圖片保存到項目自己創建的相簿中,也需要先將圖片保存到相機膠卷相簿中,然后在轉移到自己創建的相簿中。

將圖片保存到自己創建相簿的步驟

1.判斷用戶授權情況

// 獲取用戶授權狀態
PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];

// 授權狀態
PHAuthorizationStatusRestricted, 因為家長控制, 導致應用無法方法相冊(跟用戶的選擇沒有關系)很少出現。
如果這種狀態提醒用戶 系統原因無法訪問相冊 
PHAuthorizationStatusDenied, 用戶拒絕當前應用訪問相冊(用戶當初點擊了"不允許") 
如果用戶拒絕,提醒用戶去[設置]-[隱私]-[照片]中開啟。
PHAuthorizationStatusAuthorized,用戶允許當前應用訪問相冊(用戶當初點擊了"好")  
如果獲得用戶授權,則開始保存圖片
PHAuthorizationStatusNotDetermined, 用戶還沒有做出選擇
如果用戶還沒有做出選擇,則對用戶授權信息進行請求,如果用戶點擊了不允許則什么都不做,點擊了好則開始保存圖片

2.將圖片存儲在交卷相冊中
3.判斷是否已經創建自己相簿
4.如果已經創建了則獲得曾經創建過的相簿,獲得圖片,獲取添加圖片到相簿中的請求,將圖片添加到相簿
5.如果沒有創建相簿,創建相簿的請求,獲得創建相簿,獲得圖片,獲取圖片添加到相簿的請求,將圖片添加到相簿中

直接來看保存圖片到相冊的save按鈕點擊事件吧,<Photos/Photos.h>框架的設計雖然使用起來繁瑣,但是非常巧妙,如果想對"相冊"進行修改(增刪改), 那么修改代碼必須放在[PHPhotoLibrary sharedPhotoLibrary]performChanges方法的block中,并且將圖片添加到相簿中、創建相簿都是耗時操作,他們都在子線程中執行。所以如果做添加過程中想要修改UI,例如提醒用戶保存成功或失敗等,需要會到主線程中執行。

- (IBAction)save {
    /*
    PHAuthorizationStatusNotDetermined,     用戶還沒有做出選擇
    PHAuthorizationStatusDenied,            用戶拒絕當前應用訪問相冊(用戶當初點擊了"不允許")
    PHAuthorizationStatusAuthorized         用戶允許當前應用訪問相冊(用戶當初點擊了"好")
    PHAuthorizationStatusRestricted,        因為家長控制, 導致應用無法方法相冊(跟用戶的選擇沒有關系)
    */
    PHAuthorizationStatus status = [PHPhotoLibrary authorizationStatus];
    if (status == PHAuthorizationStatusRestricted) {
        // 因為家長控制,導致應用無法訪問相冊(與用戶沒有關系)
        [SVProgressHUD showErrorWithStatus:@"因為系統原因,無法訪問系統相冊"];
    }else if (status == PHAuthorizationStatusDenied){
        // 用戶點擊了不允許
        CLLog(@"設置-隱私-照片-百思不得姐xx_cc-允許");
    }else if (status == PHAuthorizationStatusAuthorized){
        // 獲得用戶授權,在這里保存圖片
        [self saveImage];
    }else if (status == PHAuthorizationStatusNotDetermined){
        // 用戶還沒有選擇進行授權
        [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
           // 用戶點擊好或者不允許 都會到這里,如果不允許則什么都不做,如果好,則保存圖片
            if (status == PHAuthorizationStatusAuthorized) {
                // 保存圖片
                [self saveImage];
            }
        }];
    }    
}
/** 保存圖片到相冊 */
- (void)saveImage
{
    // PHAsset : 一個資源, 比如一張圖片\一段視頻
    // PHAssetCollection : 一個相簿

    // PHAsset的標識, 利用這個標識可以找到對應的PHAsset對象(圖片對象)
    __block NSString *assetLocalIdentifier = nil;
    
    // 如果想對"相冊"進行修改(增刪改), 那么修改代碼必須放在[PHPhotoLibrary sharedPhotoLibrary]的performChanges方法的block中
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        // 1.保存圖片A到"相機膠卷"中
        // 創建圖片的請求
        assetLocalIdentifier = [PHAssetCreationRequest creationRequestForAssetFromImage:self.imageView.image].placeholderForCreatedAsset.localIdentifier;
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        // 這個方法在子線程中執行,所以需要返回到主線程中去修改UI
        if (success == NO) {
            [self showError:@"保存圖片失敗!"];
            return;
        }
        // 2.獲得相簿
        PHAssetCollection *createdAssetCollection = [self createdAssetCollection];
        if (createdAssetCollection == nil) {
            // 這個方法在子線程中執行,所以需要返回到主線程中去修改UI
            [self showError:@"創建相簿失敗!"];
            return;
        }
        [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
            // 3.添加"相機膠卷"中的圖片A到"相簿"D中
            
            // 獲得圖片
            PHAsset *asset = [PHAsset fetchAssetsWithLocalIdentifiers:@[assetLocalIdentifier] options:nil].lastObject;            
            // 添加圖片到相簿中的請求
            PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:createdAssetCollection];
            // 添加圖片到相簿
            [request addAssets:@[asset]];
        } completionHandler:^(BOOL success, NSError * _Nullable error) {
            // 這個方法在子線程中執行,所以需要返回到主線程中去修改UI
            if (success == NO) {
                [self showError:@"保存圖片失敗!"];;
            } else {
                [self showSuccess:@"保存圖片成功!"];;
            }
        }];
    }];
}

/**
 *  獲得相簿
 *  如果已經找到應用對應的相簿則直接添加到相簿,如果沒有找到則創建新的相簿 
 */
- (PHAssetCollection *)createdAssetCollection
{
    // 相簿名字 CLAssetCollectionTitle
    static NSString * CLAssetCollectionTitle = @"百思不得姐xx_cc";

    // 從已存在相簿中查找這個應用對應的相簿
    PHFetchResult<PHAssetCollection *> *assetCollections = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];
    for (PHAssetCollection *assetCollection in assetCollections) {
        if ([assetCollection.localizedTitle isEqualToString:CLAssetCollectionTitle]) {
            return assetCollection;
        }
    }
    // 沒有找到對應的相簿, 得創建新的相簿
    // 錯誤信息
    NSError *error = nil;
    
    // PHAssetCollection的標識, 利用這個標識可以找到對應的PHAssetCollection對象(相簿對象)
    __block NSString *assetCollectionLocalIdentifier = nil;
    
    // 這個方法在主線程張中執行,等相簿創建完畢之后才會返回
    [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{
        // 創建相簿的請求  CLAssetCollectionTitle 表示相簿名字
        assetCollectionLocalIdentifier = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:CLAssetCollectionTitle].placeholderForCreatedAssetCollection.localIdentifier;
    } error:&error];
    
    // 如果有錯誤信息
    if (error) return nil;
    
    // 獲得剛才創建的相簿
    return [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[assetCollectionLocalIdentifier] options:nil].lastObject;
}

// 注意:因為添加圖片到相簿,和創建相簿都在子線程中執行,所以修改UI需要回到主線程,我們這里進行了封裝。
- (void)showSuccess:(NSString *)text
{
    dispatch_async(dispatch_get_main_queue(), ^{
        [SVProgressHUD showSuccessWithStatus:text];
    });
}
- (void)showError:(NSString *)text
{
    dispatch_async(dispatch_get_main_queue(), ^{
        [SVProgressHUD showErrorWithStatus:text];
    });
}

雖然將圖片保存到自己創建的項目相簿中比較繁瑣,但是寫一次基本上就可以循環利用了,上面的代碼稍作修改完全可以使用到別的項目中。

總結

今天主要完成了cell內部的一些細節操作,計算cell的高度,顯示cell內容,查看圖片等等,同時對精華模塊進行重構,使得精華模塊的結構更加清晰。
看一下第五天成果

第五天成果

文中如果有不對的地方歡迎指出。我是xx_cc,一只長大很久但還沒有二夠的家伙。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容