UITableView 滚动时跳动和闪烁

UITableView jumps and flickers when scrolling

我有一个 UITableView 可以显示带有图像和一些文本的单元格。数据是按需请求的——我首先请求 10 行的数据,然后是接下来的 10 行,依此类推。我在 tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) 中执行此操作。问题是,当我收到数据并需要更新 tableview 时,它有时会跳 and/or 闪烁。我打电话给 reloadData。这是部分代码:

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {

    DispatchQueue.global(qos: .background).async {
        if indexPath.row + 5 >= self.brands.count && !BrandsManager.pendingBrandsRequest {
            BrandsManager.getBrands() { (error, brands) in

                self.brands.append(contentsOf: brands as! [Brand])

                DispatchQueue.main.async { 

                    UIView.performWithoutAnimation {
                        self.brandsTableView.reloadData()
                    }

                }

            }
        }
    }

}

单元格的高度像这样返回常量:

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 70
}

我正在使用 Kingfisher 下载和缓存图像。这是来自数据源的更多代码:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return brands.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CellIdentifiers.ImageTableCell, for: indexPath) as! ImageTableViewCell
    let brand = brands[indexPath.row]
    cell.centerLabel.text = brand.brand
    cell.leftImageView.image = nil

    if let url = BrandsManager.brandLogoURL(forLogoName: brand.logo!) {
        let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
        cell.leftImageView.kf.setImage(with: resource)
    } else {
        print("Cannot form url for brand logo")
    }

    return cell
}

如何避免滚动时 table 视图的闪烁和跳跃?我查看了一些类似的问题,但找不到适合我的案例的有效解决方案。

如果您要将数据追加到 tableView 的末尾,请不要调用 reloadData,这会强制重新计算和重绘所有单元格。而是使用 UITableView.insertRows(at:with:),如果您使用 .automatic 并保留现有单元格,它将执行适当的插入动画。

要消除跳跃问题,您需要将 estimatedHeightForRowAt 设置为与行高相同。假设您没有性能问题,您可以简单地执行以下操作:

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return self.tableView(tableView, heightForRowAt: indexPath)
}

或者如果单元格高度不变,您可以 tableView.estimatedRowHeight = 70.0.

为什么会发生这种情况是因为table重新加载时的视图将使用estimatedRowHeight用于不可见的单元格,当估计高度与实际高度不同时会导致跳跃。给你一个想法:

假设估计身高为 50,而实际身高为 75。现在您已经向下滚动,屏幕上有 10 个单元格,您有 10*75 = 750 像素的内容偏移量。不,当重新加载发生时 table 视图将忽略隐藏的单元格数量并尝试重新计算。它将继续重复使用估计的行高,直到找到应该可见的索引路径。在此示例中,它开始使用索引 [0, 1, 2... 调用您的 estimatedHeightForRow 并将 offset 增加 50 直到它到达您的内容偏移量仍然是 750。所以这意味着它到达索引 750/50 = 15。这会在重新加载时产生从单元格 10 到单元格 15 的跳转。

至于闪烁有很多种可能。您可以通过仅重新加载已更改的数据源部分来避免重新加载不需要重新加载的单元格。在您的情况下,这意味着插入新行,例如:

tableView.beginUpdates()
tableView.insertRows(at: myPaths, with: .none)
tableView.endUpdates()

你甚至看到闪烁看起来还是很奇怪。如果只有图像闪烁,则问题可能出在其他地方。像这样获取图像通常是一个异步操作,即使图像已经被缓存。您可以通过检查是否真的需要更新资源来避免它。如果您的单元格已经在显示您要显示的图像,则没有理由应用新资源:

if let url = BrandsManager.brandLogoURL(forLogoName: brand.logo!) {
    if url != cell.currentLeftImageURL { // Check if new image needs to be applied
        let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
        cell.currentLeftImageURL = url // Save the new URL
        cell.leftImageView.kf.setImage(with: resource)
    }
} else {
    print("Cannot form url for brand logo")
}

不过我宁愿将此代码放入单元格本身

var leftImageURL: URL {
    didSet {
        if(oldValue != leftImageURL) {
            let resource = ImageResource(downloadURL: url, cacheKey: url.absoluteString)
            leftImageView.kf.setImage(with: resource)
        }
    }
}

但这完全取决于您。