UITableView 执行deleteRowsAtIndexPaths:withRowAnimation: 后偏移错误

bug 出现现象

首先描述下场景。UITableView 列表使用场景,UITableViewCell 上有按钮响应事件,当触发按钮点击事件时,删除对应被选择的 UITableViewCell。
使用的是 UITableView 的 deleteRowsAtIndexPaths:withRowAnimation: 方法来实现较好的体验。

UITableView 的一些设置:定高、没有设置estimatedRowHeight,使用 [UIView new] 作为区头和区尾,没有设置estimatedSectionHeaderHeight和estimatedSectionHeaderHeight。

UITableView 和 UITableViewCell 基本设置,代码如下:

- (UITableView *)tableView {
    if (!_tableView) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, STATUS_BAR_HEIGHT + NAVIGATION_BAR_HEIGHT, SCREEN_WIDTH, SCREEN_HEIGHT - STATUS_BAR_HEIGHT - NAVIGATION_BAR_HEIGHT - HOME_INDICATOR_HEIGHT) style:UITableViewStyleGrouped];
        _tableView.delegate = self;
        _tableView.dataSource = self;
        
        _tableView.rowHeight = AutoSize(120);;

        [_tableView registerClass:[KKECGoodsListCell class] forCellReuseIdentifier:NSStringFromClass([KKECGoodsListCell class])];
    }
    return _tableView;
}


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataSource.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    KKECGoodsListCell *goodListCell = (KKECGoodsListCell *)[tableView dequeueReusableCellWithIdentifier:NSStringFromClass([KKECGoodsListCell class]) forIndexPath:indexPath];
    // 其他处理

    @weakify(self)
    [goodListCell setOffOnShelfBlock:^(KKECGoodsListCell * _Nonnull cell) {
       @strongify(self)

       // 其他处理

       [self handleDeleteCellWithTableView:tableView indexPath:indexPath];
    }];
    return goodListCell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
     return AutoSize(12);
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
     return AutoSize(14);
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    return [UIView new];
}

- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
    return [UIView new];
}

具体的事件响应,代码如下:

- (void)handleDeleteCellWithTableView:(UITableView *)tableView indexPath:(NSIndexPath *)indexPath {
    [tableView.dataArray removeObjectAtIndex:indexPath.row];

    if (@available(iOS 11, *)) {
        [tableView performBatchUpdates:^{
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
        } completion:^(BOOL finished) {
            // 更新 indexPath
            [tableView reloadData];
        }];
    } else {
        [tableView beginUpdates];
        [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:(UITableViewRowAnimationNone)];
        [tableView endUpdates];
        // 更新 indexPath
        [tableView reloadData];
    }
}

测试良好。当提交提测后,反馈在 iPhone 6s iOS12 上有异常。

具体的异常是:当删除第一页数据的第四条数据(所处位置是屏幕底部边缘)后,UITableView 会额外的向下滚动一定的距离。正常来说,UITableView 能够完全展示剩下的三个 Cell,contentOffset.y 应该为 0。

除了这个操作,从第一个正序删除均正常,倒叙除第四个删除均正常。

解决办法

查看了网上的一些其他资料,基本是偏移 64pt 的居多,设置 edgesForExtendedLayout或 automaticallyAdjustsScrollViewInsets等属性均无效。

后来偶然间看到了可能与 estimatedRowHeight 有关,于是尝试设置了 estimatedRowHeight与 rowHeight值相同。问题得以解决。

思考

因为 UITableView 是定高,所以没有设置estimatedRowHeight,在测试过程中其他设备都正常。之前实现的代码也没有遇到这样的问题,所以在遇到异常情况后,也没有往 estimatedRowHeight 考虑。

如果说是 reloadData时高度计算有误,那已经设置的rowHeight应该可以提供有效数据,为什么还需要设置estimatedRowHeight?并且其他设备不设置也是正常的。

造成 bug 的深层原因还不清楚,也可能是 UITableView 的问题,也或许是有哪些情况被自己疏忽了。

如果有更好的解释和想法,可以评论交流。


update
有个比较合理的解释:

Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
The default value is UITableViewAutomaticDimension, which means that the table view selects an estimated height to use on your behalf. Setting the value to 0 disables estimated heights, which causes the table view to request the actual height for each cell. If your table uses self-sizing cells, the value of this property must not be 0.
When using height estimates, the table view actively manages the contentOffset and contentSize properties inherited from its scroll view. Do not attempt to read or modify those properties directly.

你可能感兴趣的:(UITableView 执行deleteRowsAtIndexPaths:withRowAnimation: 后偏移错误)