美文网首页iOS高级开发我喜欢的技术篇iOS开发
你真的知道如何更新cell上的进度条吗?

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

作者: foolishBoy | 来源:发表于2017-11-07 10:25 被阅读155次

    我们经常会遇到这样的场景: 在一个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会显示进度条变化。

    为了解决上面的问题,一般主要有两种思路:

    1. cell不重用

      一般在cell数很少的时候可以使用这种方法。比如总共就5个cell,系统开始就分配了5个cell,那么就不会重用cell。也就不会有delegate指向错误cell的情况出现。

    2. 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时检测数据的相应字段

    相关文章

      网友评论

      • 愤怒的小懒懒:每一个cell 下载都有一个ID 标记是哪一个任务,由ID 来判断那一条cell 的进度条到多少,这样也可以啊。
        foolishBoy:这还是会出现cell重用的问题啊。比如第一行是cell1 ,它的任务是id1, 上下滑动后,可能第二行变成cell1了,那它的任务还是id1,就出现第二行进度在更新了

      本文标题:你真的知道如何更新cell上的进度条吗?

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