六天完成一个简单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阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容