如何优雅地动态插入数据到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复制代码
相关文章
相关标签/搜索