美文网首页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