美文网首页iOS 的那些事儿
一个解决多个 UITableViewCell 网络请求的方案

一个解决多个 UITableViewCell 网络请求的方案

作者: uniapp | 来源:发表于2018-04-21 14:00 被阅读8次

实际开发中,遇到 UITableViewCell 中显示图片的问题,首先想到的就是 SDWebImage 框架。该框架中已经将 Cell 图片请求的缓存、更新、取消等封装完毕,使用起来十分方便。但是如果 Cell 中的图片不是单纯的展示,而是要求对请求的各种状态(下载、失败、成功)都要展示,而且还能在请求失败的情况下,再次发送请求(类似微信聊天过程发送图片消息时的状态)。在这样的场景下,该如何处理呢?

下面就以上应用场景,介绍一下自己在项目中的解决方案。首先看一下项目最终的展示结果:


multi-cell.gif

按照常用 MVC 的框架创建项目:

1 Model。

postion 属性用来记录图片哪一张,imageUrl 用来表示图片地址,image 属性缓存下载完成后的图片。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface ZDCellModel : NSObject
@property (nonatomic, assign) NSInteger position;
@property (nonatomic, strong) NSString *imageUrl;
@property (nonatomic, strong) UIImage *image;
@end


#import "ZDCellModel.h"

@implementation ZDCellModel
@end
2 xib 创建 Cell 。

添加 cellModel 属性,及在赋值属性时在 Cell 中展示。


Cell
#import <UIKit/UIKit.h>
#import "ZDCellModel.h"

@interface ZDTableViewCell : UITableViewCell
@property (nonatomic, strong) ZDCellModel *cellModel;
@end


@implementation ZDTableViewCell

- (void)awakeFromNib {
    [super awakeFromNib];
    _iconImage.userInteractionEnabled = true;
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];

    // Configure the view for the selected state
}
- (void)setCellModel:(ZDCellModel *)cellModel{
    _cellModel = cellModel;
    self.titleL.text = [NSString stringWithFormat:@"这是第%zd副图", cellModel.position];
}
@end
3 Controller。

通过 MJRefresh 设置上/下拉刷新,用 dispatch_after 方法模拟网络请求延时。在 modelArray 中存储 model 模型。

#import "ZDTableViewController.h"
#import "ZDTableViewCell.h"
#import <MJRefresh/MJRefresh.h>

@interface ZDTableViewController ()
@property (nonatomic, strong) NSArray *urlArray;
@property (nonatomic, strong) NSMutableArray *modelArray;
@end

@implementation ZDTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.tableView registerNib:[UINib nibWithNibName:@"ZDTableViewCell" bundle:nil] forCellReuseIdentifier:@"ZDTableViewCell"];
    self.tableView.rowHeight = 120;
    self.tableView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(loadHeader)];
    self.tableView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingTarget:self refreshingAction:@selector(loadFooter)];
}

- (void)loadHeader{
    //模拟网络请求
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.tableView.mj_header endRefreshing];
        [self.tableView.mj_footer endRefreshing];
        [self.modelArray removeAllObjects];
        [self addModel:30];
        [self.tableView reloadData];
    });
}

- (void)loadFooter{
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self.tableView.mj_header endRefreshing];
        [self.tableView.mj_footer endRefreshing];
        [self addModel:20];
        [self.tableView reloadData];
    });
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.modelArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    ZDTableViewCell *zdCell = [tableView dequeueReusableCellWithIdentifier:@"ZDTableViewCell" forIndexPath:indexPath];
    ZDCellModel *cellModel = self.modelArray[indexPath.row];
    zdCell.cellModel = cellModel;
    zdCell.selectionStyle = UITableViewCellSelectionStyleNone;
    return zdCell;
}

- (NSMutableArray *)modelArray{
    if (!_modelArray) {
        _modelArray = [NSMutableArray array];
        [self addModel:30];
    }
    return _modelArray;
}

- (void)addModel:(NSInteger)count{
    for (int i = 0; i<count; i++) {
        ZDCellModel *cellM = [[ZDCellModel alloc] init];
        cellM.position = i%2;
        NSLog(@"%zd\n", cellM.position);
        cellM.imageUrl = self.urlArray[i%2];
        [_modelArray addObject:cellM];
    }
}

- (NSArray *)urlArray{
    if (!_urlArray) {
        _urlArray = @[@"https://gss2.bdstatic.com/9fo3dSag_xI4khGkpoWK1HF6hhy/baike/c0%3Dbaike80%2C5%2C5%2C80%2C26/sign=27e41b0d80cb39dbd5cd6f04b17f6241/7acb0a46f21fbe099143bcf36a600c338744ad3c.jpg",                       @"https://gss2.bdstatic.com/9fo3dSag_xI4khGkpoWK1HF6hhy/baike/c0%3Dbaike272%2C5%2C5%2C272%2C90/sign=e31d7a55dba20cf4529df68d17602053/91ef76c6a7efce1b27893518a451f3deb58f6546.jpg",];
    }
    return _urlArray;
}
@end

在 Cell 中使用 SDWebImage 框架中 UIImageView 的分类方式进行图片赋值时,无法监听下载进度。所以要使用 SDWebImage 中 SDWebImageManager 来下载。在 Cell 中添加下载方法:

- (void)loadImage{
    NSURL *url = [NSURL URLWithString:_cellModel.imageUrl];
    [self.iconImage bringSubviewToFront:self.activityView];
    [self.activityView startAnimating];
    __weak typeof(self) weakSelf = self;
    [[SDWebImageManager sharedManager] loadImageWithURL:url options:(SDWebImageRefreshCached) progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
        //
    } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
        [weakSelf.activityView stopAnimating];
        if (error) {
            weakSelf.errorBtn.hidden = false;
            [weakSelf.iconImage bringSubviewToFront:_errorBtn];
        }else{
            _errorBtn.hidden = true;
            __strong typeof(self) strongSelf = weakSelf;
            _cellModel.image = image;
            if(strongSelf.loadDelegate){
                strongSelf.loadDelegate(strongSelf);
            }
        }
    }];
}

- (UIButton *)errorBtn{
    if (!_errorBtn) {
        _errorBtn = [[UIButton alloc] init];
        [_errorBtn setTitle:@"重新下载" forState:(UIControlStateNormal)];
        [_iconImage addSubview:_errorBtn];
        [_errorBtn mas_makeConstraints:^(MASConstraintMaker *make) {
            make.edges.equalTo(@0);
        }];
        [_errorBtn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];
        _errorBtn.titleLabel.textColor = [UIColor redColor];
        [_errorBtn addTarget:self action:@selector(loadImage) forControlEvents:(UIControlEventTouchUpInside)];
        
    }
    return _errorBtn;
}

- (UIActivityIndicatorView *)activityView{
    if (!_activityView) {
        _activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:(UIActivityIndicatorViewStyleGray)];
        [_iconImage addSubview:_activityView];
        [_activityView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.center.equalTo(@0);
        }];
    }
    return _activityView;
}

其中 errorBtn 和 activityView 默认为隐藏状态,只在网络请求过程中显示。

由于 TabelViewCell 中重用机制下,里面的 iconImage 会带着上一次请求成功的图片,需要在 setCellModel 中对 iconImage 进行提前清除。完善后的 Cell 赋值方法为:

- (void)setCellModel:(ZDCellModel *)cellModel{
    self.errorBtn.hidden = true;
    _cellModel = cellModel;
    self.titleL.text = [NSString stringWithFormat:@"这是第%zd副图", cellModel.position];
    //清除重用时的缓存
    self.iconImage.image = nil;
    
    if (cellModel.image != nil) {
        self.iconImage.image = cellModel.image;
        return;
    }
    [self loadImage];
}

为了能在图片下载成功后更新改图片所在的 Cell, 我在 Cell 中设置了 Block 的回调 loadDelegate,并且为 Cell 增添 indexPath 属性记录位置。在 Controller 中设置 Cell 的回调处理方式:

    zdCell.indexPath = indexPath;
    __weak typeof(self) weakSelf = self;
    
    zdCell.loadDelegate = ^(ZDTableViewCell *cell) {
        //回到主线程更新,不然有可能会崩溃
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf.tableView reloadRowsAtIndexPaths:@[cell.indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
        });
    };

回调更新需要切换到主线程,这样能保证 TableView 更新完 Cell 时,将正确的 indexPath 赋值给 Cell 完成后,执行回调方法,防止出现重用机制下 indexPath 的越界问题。

所以最终 Controller 中 Cell 的设置方式为:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    ZDTableViewCell *zdCell = [tableView dequeueReusableCellWithIdentifier:@"ZDTableViewCell" forIndexPath:indexPath];
    NSLog(@"aaaaaa%zd", indexPath.row);
    ZDCellModel *cellModel = self.modelArray[indexPath.row];
    zdCell.cellModel = cellModel;
    NSLog(@"********%zd",cellModel.position);
    zdCell.indexPath = indexPath;
    __weak typeof(self) weakSelf = self;
    
    zdCell.loadDelegate = ^(ZDTableViewCell *cell) {
        //回到主线程更新,不然有可能会崩溃
        dispatch_async(dispatch_get_main_queue(), ^{
            [weakSelf.tableView reloadRowsAtIndexPaths:@[cell.indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
        });
    };
    zdCell.selectionStyle = UITableViewCellSelectionStyleNone;
    return zdCell;
}

到此,经过以上对 UITableViewCell 创建及赋值时的完善后,整个逻辑已经顺利完成,能够保证 Cell 中的图片下载成功后更新到对应的 Cell 上。而且在下载失败后,能够在次点击重新下载按钮进行二次加载。

最后附上项目git: MultiCellDownload

总结:

本工程需要对 UITabelView 中 Cell 的重用机制、reloadData 方法、reloadRowsAtIndexPaths 方法的执行过程需要有深刻的认识。在单个主线程中,所有的方法都会顺序执行。尤其是 reloadRowsAtIndexPaths 方法可以在子线程执行的特点,可能造成 UITabelView 刷新奔溃的奇怪现象比较难以识别。

本工程的一个小瑕疵是: 使用 reloadRowsAtIndexPaths 方法时可能造成页面跳转的现象,暂时还没想出好的解决方法,希望有完善的小伙伴积极留言交流。

喜欢和关注都是对我的鼓励和支持~

相关文章

网友评论

    本文标题:一个解决多个 UITableViewCell 网络请求的方案

    本文链接:https://www.haomeiwen.com/subject/nuqxlftx.html