你真的知道如何更新cell上的进度条吗?

咱们常常会遇到这样的场景: 在一个TableView上,每一个cell都有一个进度条,多是下载的进度或者音乐播放的进度,咱们须要实时地更新这个进度条。是否是听起来很简单?小心,这里有坑!bash

大多数人首先想到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行
开始下载第2行

[图1,进度条在第2行]

上下滑动TableView后进度条在第3行
上下滑动TableView后进度条在第3行

[图2,进度条在第3行]

正如咱们开始说的,最开始下载第2行,显示进度条,上下滑动TableView,进度条变到第3行了。ui

试想,假设最开始系统分配了10个cell并复用。当前cell2的地址是0x000222,它的downloader实例地址是0xfff222。此时,downloader的delegate是cell2,但实际上downloader的delegate绑定的是地址为0x000222的对象,并非cell2自己。当咱们滑动TableView时,cell都被重绘,这时候可能刚好cell3重用了0x000222的对象。那么可想而知,下次更新进度时,downloader的delegate指向的就是cell3,因此cell3会显示进度条变化。atom

为了解决上面的问题,通常主要有两种思路:spa

  1. cell不重用3d

    通常在cell数不多的时候可使用这种方法。好比总共就5个cell,系统开始就分配了5个cell,那么就不会重用cell。也就不会有delegate指向错误cell的状况出现。code

  2. downloader与cell持有的Model绑定orm

    假如每一个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时检测数据的相应字段

相关文章
相关标签/搜索