实际开发中,遇到 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 方法时可能造成页面跳转的现象,暂时还没想出好的解决方法,希望有完善的小伙伴积极留言交流。
网友评论