我們經(jīng)常會遇到這樣的場景: 在一個(gè)TableView上,每個(gè)cell都有一個(gè)進(jìn)度條,可能是下載的進(jìn)度或者音樂播放的進(jìn)度,我們需要實(shí)時(shí)地更新這個(gè)進(jìn)度條。是不是聽起來很簡單?當(dāng)心,這里有坑!
大多數(shù)人首先想到block或者delegate的回調(diào)方式來更新進(jìn)度。想法是對的,但是忽視了一個(gè)問題——“Cell是重用的”。當(dāng)然,你可以說就不重用。不過大多數(shù)時(shí)候,為了節(jié)省內(nèi)存空間,優(yōu)化程序性能,還是建議重用cell的。既然cell被重用,那么用剛剛的方法就會遇到一個(gè)奇怪的現(xiàn)象:cell0開始更新自己的進(jìn)度條,上下滾動TableView時(shí)發(fā)現(xiàn)進(jìn)度條跑到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];
}
運(yùn)行結(jié)果截圖如下:
[圖1,進(jìn)度條在第2行]
[圖2,進(jìn)度條在第3行]
正如我們開始說的,最開始下載第2行,顯示進(jìn)度條,上下滑動TableView,進(jìn)度條變到第3行了。
試想,假設(shè)最開始系統(tǒng)分配了10個(gè)cell并復(fù)用。當(dāng)前cell2的地址是0x000222,它的downloader實(shí)例地址是0xfff222。此時(shí),downloader的delegate是cell2,但實(shí)際上downloader的delegate綁定的是地址為0x000222的對象,并不是cell2本身。當(dāng)我們滑動TableView時(shí),cell都被重繪,這時(shí)候可能恰好cell3重用了0x000222的對象。那么可想而知,下次更新進(jìn)度時(shí),downloader的delegate指向的就是cell3,所以cell3會顯示進(jìn)度條變化。
為了解決上面的問題,一般主要有兩種思路:
-
cell不重用
一般在cell數(shù)很少的時(shí)候可以使用這種方法。比如總共就5個(gè)cell,系統(tǒng)開始就分配了5個(gè)cell,那么就不會重用cell。也就不會有delegate指向錯(cuò)誤cell的情況出現(xiàn)。
-
downloader與cell持有的Model綁定
假如每個(gè)cell都有一個(gè)對應(yīng)的model數(shù)據(jù)結(jié)構(gòu):
@interface CellModel : NSObject @property (nonatomic, strong) NSNumber *modelId; @property (nonatomic, assign) float progress; @end
我們可以用KVO方式監(jiān)聽每個(gè)CellModel的進(jìn)度,并且用modelId來判斷當(dāng)前的Cell是否在下載狀態(tài)以及是否被更新。
稍作修改的代碼:
/*ProgressCell.m*/ - (void)setLabelIndex:(NSUInteger)index model:(CellModel *)model { self.lbRow.text = [NSString stringWithFormat:@"%u",index]; self.model = model; //這里根據(jù)model值來繪制UI if (model.progress > 0) { [_btnPlay setImage:nil forState:UIControlStateNormal]; } else { [_btnPlay setImage:[UIImage imageNamed:@"ic_download_transfer"] forState:UIControlStateNormal]; } //監(jiān)聽progress [self.model addObserver:self forKeyPath:@"progress" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionOld context:nil]; } //下載器也與model綁定,這樣可以通知到準(zhǔn)確的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更新,防止復(fù)用問題 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的監(jiān)聽對象監(jiān)聽到。 self.model.progress = progress; } } }
當(dāng)然如果這里是一個(gè)音樂播放進(jìn)度條,我們可以使用一個(gè)單例的播放器并與model綁定。cell同樣監(jiān)聽model的progress字段,或者在播放器進(jìn)度更新時(shí)發(fā)出通知,所有收到通知的cell檢測如果更新的model是自己的才更新UI。
總結(jié):
不要對復(fù)用的cell直接使用delegate
或者block
回調(diào)來更新進(jìn)度條,使用回調(diào)更新UI時(shí)一定記得與cell所持有的數(shù)據(jù)綁定,并在繪制cell時(shí)檢測數(shù)據(jù)的相應(yīng)字段