你真的知道如何更新cell上的进度条吗?
我们经常会遇到这样的场景: 在一个TableView上,每个cell都有一个进度条,可能是下载的进度或者音乐播放的进度,我们需要实时地更新这个进度条。是不是听起来很简单?当心,这里有坑!
大多数人首先想到block或者delegate的回调方式来更新进度。想法是对的,但是忽视了一个问题——“Cell是重用的”。当然,你可以说就不重用。不过大多数时候,为了节省内存空间,优化程序性能,还是建议重用cell的。既然cell被重用,那么用刚刚的方法就会遇到一个奇怪的现象:cell0开始更新自己的进度条,上下滚动TableView时发现进度条跑到cell3上更新了。
来看我的Demo:
/*SimulateDownloader.h*/ @protocol DownloadDelegate <NSObject> - (void)downloadProgress:(float)progress; - (void)downloadCompleted; @end /*SimulateDownloader*/ - (void)startDownload { self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(downLoadTimer) userInfo:nil repeats:YES]; [self.timer fire]; } - (void)downLoadTimer { static float progress = 0; progress += 0.05; if (progress > 1.01) { if (self.delegate && [self.delegate respondsToSelector:@selector(downloadCompleted)]) { [self.delegate downloadCompleted]; } } else { if (self.delegate && [self.delegate respondsToSelector:@selector(downloadProgress:)]) { [self.delegate downloadProgress:progress]; } } } /*ProcessCell.m*/ - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { ... _downloader = [[SimulateDownloader alloc] init]; _downloader.delegate = self; } return self; } #pragma mark - DownloadDelegate - (void)downloadProgress:(float)progress { static float oldValue = 0; [self setCircleProgressFrom:oldValue To:progress]; oldValue = progress; } - (void)downloadCompleted { self.circle.hidden = YES; [_btnPlay setImage:[UIImage imageNamed:@"ic_play_transfer"] forState:UIControlStateNormal]; }
运行结果截图如下:
开始下载第2行[图1,进度条在第2行]上下滑动TableView后进度条在第3行
[图2,进度条在第3行]
正如我们开始说的,最开始下载第2行,显示进度条,上下滑动TableView,进度条变到第3行了。
试想,假设最开始系统分配了10个cell并复用。当前cell2的地址是0x000222,它的downloader实例地址是0xfff222。此时,downloader的delegate是cell2,但实际上downloader的delegate绑定的是地址为0x000222的对象,并不是cell2本身。当我们滑动TableView时,cell都被重绘,这时候可能恰好cell3重用了0x000222的对象。那么可想而知,下次更新进度时,downloader的delegate指向的就是cell3,所以cell3会显示进度条变化。
为了解决上面的问题,一般主要有两种思路:
cell不重用
一般在cell数很少的时候可以使用这种方法。比如总共就5个cell,系统开始就分配了5个cell,那么就不会重用cell。也就不会有delegate指向错误cell的情况出现。
downloader与cell持有的Model绑定
假如每个cell都有一个对应的model数据结构:
@interface CellModel : NSObject @property (nonatomic, strong) NSNumber *modelId; @property (nonatomic, assign) float progress; @end
我们可以用KVO方式监听每个CellModel的进度,并且用modelId来判断当前的Cell是否在下载状态以及是否被更新。
稍作修改的代码:
/*ProgressCell.m*/ - (void)setLabelIndex:(NSUInteger)index model:(CellModel *)model { self.lbRow.text = [NSString stringWithFormat:@"%u",index]; self.model = model; //这里根据model值来绘制UI if (model.progress > 0) { [_btnPlay setImage:nil forState:UIControlStateNormal]; } else { [_btnPlay setImage:[UIImage imageNamed:@"ic_download_transfer"] forState:UIControlStateNormal]; } //监听progress [self.model addObserver:self forKeyPath:@"progress" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld context:nil]; } //下载器也与model绑定,这样可以通知到准确的model更新 - (void)simulateDownloadProgress { [_btnPlay setImage:nil forState:UIControlStateNormal]; [_downloader startDownload:self.model]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { CellModel *model = (CellModel *)object; //检查是否是自己的model更新,防止复用问题 if (model.modelId != self.model.modelId) { return; } float from = 0, to = 0; if ([keyPath isEqualToString:@"progress"]) { if (change[NSKeyValueChangeOldKey]) { from = [change[NSKeyValueChangeOldKey] floatValue]; } if (change[NSKeyValueChangeNewKey]) { to = [change[NSKeyValueChangeNewKey] floatValue]; } [self setCircleProgressFrom:from To:to]; } } /*SimulateDownloader.m*/ - (void)downLoadTimer { static float progress = 0; progress += 0.1; if (progress > 1.01) { // if (self.delegate && [self.delegate respondsToSelector:@selector(downloadCompleted)]) { // [self.delegate downloadCompleted]; // } } else { // if (self.delegate && [self.delegate respondsToSelector:@selector(downloadProgress:)]) { // [self.delegate downloadProgress:progress]; // } //更新Model,会被KVO的监听对象监听到。 self.model.progress = progress; } } }
当然如果这里是一个音乐播放进度条,我们可以使用一个单例的播放器并与model绑定。cell同样监听model的progress字段,或者在播放器进度更新时发出通知,所有收到通知的cell检测如果更新的model是自己的才更新UI。
总结:
不要对复用的cell直接使用delegate
或者block
回调来更新进度条,使用回调更新UI时一定记得与cell所持有的数据绑定,并在绘制cell时检测数据的相应字段