如何優雅地動態插入數據到UITableView

任他風吹雨打,我自巋然不動!redis

當咱們實時往UITableView中插入數據並刷新列表的時候,會發現列表是有抖動的。好比在微信聊天頁面,你滑動到某一個位置保持住,而後收到一個或者若干人的微信(這幾我的不在當前聊天列表中)。你會發現每收到一我的的信息,列表向下沉,就是有一個「抖動」的過程。固然,並非說微信體驗很差,只是拋磚引玉。數組

言歸正傳,我要討論的場景以下:微信

當前列表展現了不少新聞,同時後臺在加載第三方廣告。廣告加載完成後須要按照規定的位置順序循環地插入到列表中,好比第5,12,19,26...,要求插入廣告後當前展現的頁面沒有下沉抖動現象,避免剛剛看的新聞跳到不可知的位置去了。佈局

因爲這裏廣告不是直接附加在列表末尾,也不是一次性插入到相鄰的位置,而是離散地分佈在整個列表中,因此很差用
insertRowsAtIndexPaths:withRowAnimation:或者
reloadRowsAtIndexPaths:withRowAnimation:局部刷新,必須對整個列表ReloadData。顯然這會致使列表下沉抖動,最壞的狀況是當前展現的整個頁面下沉,這對於新聞客戶端來講體驗很很差。this

首先,我會想到scrollToRowAtIndexPath:atScrollPosition:animated:這個方法。在我刷新完整個列表以後,再將UITableView滾動到以前記錄的位置。大體思路看代碼:atom

//刷新列表以前找到當前屏最頂部的新聞Id
- (NSString *)topNewsId {
    NSArray *visibleCells = [self.tableView visibleCells];

    UITableViewCell *cell = [visibleCells firstObject];
    NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
    NewsModel *topNews = [self.dataArr objectAtIndex:indexPath.row];

    NSString *newsId = = topNews.newsId;
    return newsId;
}
//刷新以後再將以前頂部的新聞滾動到頂部 避免頁面抖動
- (void)keepTopNews:(NSString *)topNewsId {
    int topNewsRow = 0;
    for (int i = 0; i <[self.dataArr count] ; i ++) {
        id data = [self.dataArr objectAtIndex:i];
        if ([data isKindOfClass:[NewsModel class]]) {
            NewsModel *model = data;
            if ([model.newsId isEqualToString:topNewsId]) {
                topNewsRow = i;
                break;
            }
        }
    }
    if (topNewsRow) {
        NSIndexPath *toIndex = [NSIndexPath indexPathForRow:topNewsRow inSection:0];
        [self.tableView scrollToRowAtIndexPath:toIndex atScrollPosition:UITableViewScrollPositionTop animated:NO];
    }

}複製代碼

乍一看,這種方法挺優美的,也好像能達到咱們的目的。但實際上仍是有問題的,問題出在visibleCells這個方法。先來看看這個方法的定義:spa

Returns an array of visible cells currently displayed by the collection view.code

即返回當前展現的可見cell數組。
不過,這個方法並非"眼見爲實的",有時候咱們肉眼看不到的cell它卻認爲是可見的,或者只部分可見的它也會返回給咱們的。好比圖中網易新聞最上面的新聞 「...夫人鏡頭裏的民國世相」就只見到一部分,若是用它來置頂也是會有下沉抖動問題的。cdn

網易新聞截圖
網易新聞截圖

那麼還有沒有更優雅的方式呢?Absolutely!!!blog

既然用cell作單位來滾動太粗糙,咱們能夠用像素級別滾動來優雅地保持置頂新聞巋然不動。

首先咱們要知道ReloadData的一個特性:

When you call this method, the collection view discards any currently visible items and views and redisplays them. For efficiency, the collection view displays only the items and supplementary views that are visible after reloading the data. If the collection view’s size changes as a result of reloading the data, the collection view adjusts its scrolling offsets accordingly.

關於ContentOffset、ContentSize、ContentInset的區別這裏就不贅述了,能夠參考這裏

就是說ReloadData只刷新當前屏幕可見的哪些cell,只會對visibleCells調用
tableView:cellForRowAtIndexPath:contentOffset是保持不變的,因此咱們纔看到了「抖動現象」,就像新聞被擠下去了。

contentOffset模擬圖
contentOffset模擬圖

圖中灰色部分表示iPhone的屏幕,粉紅色表示全部數據的佈局大小,白色單元是隱藏在屏幕上方的數據,綠色表示目標廣告單于格。

左圖的當前屏幕最上面的新聞是news 11,UITableview的contentOffset是200,咱們能夠計算出news 11以前全部新聞單元格的高度總和得出如今news 11的偏移量preOffset。

右圖是在第三個位置插入一個廣告後的佈局。UITableview的contentOffset仍是200,可是news 11被「擠下去」了。咱們一樣能夠計算news 11以前全部新聞單元格和廣告單元格的高度總和得出如今news 11的偏移量afterOffset。

有了preOffset和afterOffset以後就能夠知道news 11被「擠下去」多少距離

deltaOffset = afterOffset - preOffset;

那麼,爲了保證news 11仍是展現在當初的位置,咱們只要手動更新ContentOffset的值就能夠了,至關於將粉紅色部分上移deltaOffset的距離。

看代碼:

- (void)insertAds:(NSArray *)ads {
    NSString *topNewsId = [self topNewsId];

    CGFloat preOffset = [self offSetOfTopNews:topNewsId];

    /*
    插入廣告...
    */

    [self.tableView reloadData];

    CGFloat afterOffset = [self offSetOfTopNews:topNewsId];

    CGFloat deltaOffset = afterOffset - preOffset;

    CGPoint contentOffet = [self.tableView contentOffset];
    contentOffet.y += deltaOffset;
    self.tableView .contentOffset = contentOffet;
}

//計算newsId對應新聞的偏移量
- (CGFloat)offSetOfTopNews:(NSString *)newsId {
    CGFloat offset = 0;
    for (int i = 0; i < [self.dataArr count]; i ++) {
        id data = [self.dataArr objectAtIndex:i];
        if ([data isKindOfClass:[NewsModel class]]) {
            NewsModel *model = data;
            if ([model.newsId isEqualToString:newsId]) {
                break;
            }
        }
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        CGFloat height = [self heightForRowAtIndexPath:indexPath];
        offset += height;
    }
    return offset;
}複製代碼

如此,就能夠真正作到當前屏幕一點都不下沉了。若是廣告插在當前屏幕以外,用戶是感受不到的,等滑動列表才能在相應位置看到廣告;若是插入到當前屏幕中,用戶在課間區域看到插入一個新聞,可是置頂的新聞位置是保持不動的。

盡享絲滑~

最後稍微提一下計算偏移量中用到的一個小技巧。

若是全部的新聞和廣告單元的高度是固定的,那麼heightForRowAtIndexPath:是很方便計算的。若是是動態的,就須要用到一點技巧了。

好比廣告的數據用AdModel表示。爲了讓廣告單元的高度隨廣告內容動態調整,咱們通常習慣在AdModel裏用一個cellHeight字段。

@interface AdModel:NSObject

@property (nonatomic, assign) NSInteger adId;
...
@property (nonatomic, assign) CGFloat   cellHeight;

@end複製代碼

在咱們填充內容渲染廣告位的時候算出高度再賦值給cellHeight

在上面的場景下,前面雖然插入了廣告,可是ReloadData的時候,UITableView並不會刷新不可見的廣告位,所以cellHeight始終爲0,這就致使heightForRowAtIndexPath:不能計算出正確的結果。

巧妙地,咱們在廣告插入self.dataArr的時候定義一個臨時的廣告單元變量AdCell,並主動調用渲染的接口來給cellHeight賦值。

AdCell *tmpCell = [AdCell new];
[tmpCell setAdsContent:model];//這裏會渲染廣告位並計算出cellHeight複製代碼
相關文章
相關標籤/搜索