Tencent / QMUI_iOS

QMUI iOS——致力于提高项目 UI 开发效率的解决方案
http://qmuiteam.com/ios
Other
7.1k stars 1.39k forks source link

[UIKit Bug] 将 UISearchBar 作为 tableHeaderView 使用的 UITableView,如果同时设置了 estimatedRowHeight,则 contentSize 会错乱,导致滚动异常 #1161

Closed YearRen closed 3 years ago

YearRen commented 3 years ago

Bug 表现 列表往上滚动后,尾部出现大量空白,如下方录屏所示:

如何重现

  1. tableView.tableHeaderView = UISearchBar.new;
  2. tableView.estimatedRowHeight = AAA;// iOS 11 及以后系统默认会使用 estimatedRowHeight,iOS 10 及以前要手动设置。主要该 bug 仅与 estimatedRowHeight 有关,与 estimatedSectionHeaderHeight/footerHeight 无关
  3. tableView.rowHeight = BBB;// 设置一个与 estimatedRowHeight 不一样的值,差别越大 bug 表现越明显

以下是测试 Demo: TestTableViewContentSize.zip

其他信息

Dzerzhynski commented 3 years ago

遇到了相同的问题 请问老哥解决了吗

YearRen commented 3 years ago

没有:(

MoLice commented 3 years ago

Bug 解析:

先明确 UITableView 的2个系统默认特性:

  1. 将 UISearchBar 作为 tableHeaderView 使用时,UITableView 对 searchBar 有一个“吸附”的效果,也即如果你把 searchBar 滚动到被 navigationBar 挡住一半的时候,松手,此时要么 searchBar 自动吸附上去,隐藏到 navigationBar 背后,要么完整显示出来,正常情况下不会出现 searchBar 被挡住一半、露出一半的情况。为了实现这种效果,此时的 contentSize 会有另外的计算方式,以保证有足够高的 contentSize.height 来支持这种滚动。
  2. 当 UITableView 使用 estimatedRowHeight 时,contentSize 会按照 estimatedRowHeight 去估算,并且只有可视的 cell 才会将该 cell 的高度准确纳入 contentSize.height 中,换句话说,如果一开始展示列表时只能看到一部分 cell,那么此时的 contentSize.height 是估算的、不准确的,但当你从头到尾滚动一遍列表后,此时的 contentSize.height 就是准确的,最终的。

本 issue 描述的 bug 可以理解为上述两种情况叠加后,系统对 contentSize 的计算产生了偏差,导致出现多余的 contentSize.height(例如 issue 内的 Demo,即便只有一行 cell,理论上 contentSize 是准确的,因为所有 cell 都可视,但此时系统就会算出来一个错误的 contentSize.height),当 estimatedRowHeight 与 rowHeight 相差越大时,这个多余的 contentSize.height 就越大,bug 就越明显。

系统出现这个错误的具体原因未明,QMUI 仅针对现象给出修补的方案:

  1. 如果你的列表不需要使用 estimatedRowHeight,你可以简单地用 tableView.estimatedRowHeight = 0 关闭它,从而规避这个 bug。
  2. 如果你确实需要用 estimatedRowHeight + searchBar,可以临时将以下代码添加到你的项目里,QMUI 下个版本也会带上这段代码:
    
    // [UIKit Bug] 将 UISearchBar 作为 tableHeaderView 使用的 UITableView,如果同时设置了 estimatedRowHeight,则 contentSize 会错乱,导致滚动异常
    // https://github.com/Tencent/QMUI_iOS/issues/1161
    void (^fixBugOfTableViewContentSize)(UITableView *) = ^void(UITableView *tableView) {
    BOOL estimatesRowHeight = NO;
    [tableView qmui_performSelector:NSSelectorFromString(@"_estimatesRowHeights") withPrimitiveReturnValue:&estimatesRowHeight];
    if (estimatesRowHeight && [tableView.tableHeaderView isKindOfClass:UISearchBar.class]) {
        [tableView performSelector:NSSelectorFromString(@"_updateContentSize")];
    }
    };

if (@available(iOS 11.0, )) { / - (void) _coalesceContentSizeUpdateWithDelta:(double)arg1; (0x7fff248dbcaf) / OverrideImplementation([UITableView class], NSSelectorFromString(@"_coalesceContentSizeUpdateWithDelta:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableView selfObject, CGFloat firstArgv) {

        // call super
        void (*originSelectorIMP)(id, SEL, CGFloat);
        originSelectorIMP = (void (*)(id, SEL, CGFloat))originalIMPProvider();
        originSelectorIMP(selfObject, originCMD, firstArgv);

        if (fixBugOfTableViewContentSize) {
            fixBugOfTableViewContentSize(selfObject);
        }
    };
});

} else { / - (void)_applyContentSizeDeltaForEstimatedHeightAdjustments:(double)arg1; (0x106efad91) / OverrideImplementation([UITableView class], NSSelectorFromString(@"_applyContentSizeDeltaForEstimatedHeightAdjustments:"), ^id(__unsafe_unretained Class originClass, SEL originCMD, IMP (^originalIMPProvider)(void)) { return ^(UITableView *selfObject, CGFloat firstArgv) {

        // call super
        void (*originSelectorIMP)(id, SEL, CGFloat);
        originSelectorIMP = (void (*)(id, SEL, CGFloat))originalIMPProvider();
        originSelectorIMP(selfObject, originCMD, firstArgv);

        if (fixBugOfTableViewContentSize) {
            fixBugOfTableViewContentSize(selfObject);
        }
    };
});

}

MoLice commented 3 years ago

待版本发布后再 close

MoLice commented 3 years ago

已发布 4.2.3 修复该问题。