Swift 踩坑笔记 —— UITableView Cell初始化和刷新的问题探讨

综述

讲到 UITableView,大家一定都不陌生。有一个相对夸张的说法,叫做学好 UITableView,你就是一名合格的iOS 工程师

闲话少说,最近在写 Swift 的过程中碰到了以下几个问题,特别在此记录。

遇到的问题

  • cellForRowAtIndexPath 代理中,对 cell(尤其是自定义cell) 的初始化异同
    • OC的区别 —— 不能使用OC的那种判空方式来初始化
    • 初始化不能使用自定义的方法 —— 通过dequeue方法得到的cell 永远都是非空的,换言之,即便你自定义了一个初始化方法,它也不会被执行到。
    • 通过渲染方式(render)来绘制图像,赋值
  • 刷新的问题
    • 使用 reloadData时候,在iOS 11 上会产生抖动
    • 慎用局部刷新 reloadRows的相关方法,会造成cell复用混乱
    • insertRowdeleteRowreloadRows 一样都属于局部刷新的范畴,需要用 beginUpdateendUpdate包起来。这上上一点类似

先明确两个概念

  • 代码中的 setup 表示只会执行一次,而且在 cell 的初始化中表示他的绘图(不带数据)也只会执行一次
  • 代码中的render 表示渲染,实际上是意味着setup已经完成了绘图,我要在每次重用时把数据传进去渲染

初始化问题 cellForRowAtIndexPath

关于 cellForRowAtIndexPath 的初始化问题其实在这篇文章中已经讨论过,这里不作赘述 Swift 踩坑笔记(二)—— 初始化Tableview 及自定义 TableviewCell

我们要讨论的是在cell复用过程中的赋值问题。

简单的来说,tableview 的复用机制是我们在 cellForRowAtIndexPath 初始化时绘制好必要的控件及相关约束,但是并不会去赋值。因为每次上下滚动都会重新从复用池中取出 cell,将 DataSource 对应的数据赋值一次. 鉴于Swift 无法自定义cell的初始化,那么上下滚动时,怎么重新赋值而不重复绘制就显得格外重要。

来看下面的代码

// tableview 代理
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: someCellID, for: indexPath) as! MyCell
    cell.renderCell(info: dataSource[indexPath.row])
    return cell
}
复制代码

再来看下面的Cell示意图

image.png

我们这里的 Cell 分了很多层级,

除了顶部的 Header区域是固定知道的高度外,下面的 区域 InfoA, InfoB, InfoC ...等等,都是根据具体的信息去绘制的。 换言之,我不知道每个 Cell 具体要画几个 InfoX

普通的cell初始化,只能满足固定的视图,这种动态的,只能用cell自定义一个render方法来做了。

扫描二维码关注公众号,回复: 3183076 查看本文章

再来看下面的这段 自定义 Cell 的代码

  // 略去类的初始化,这里为了  render 的目的,必须去持有这两块视图
    private var headerBaseInfoView: IGGIDBaseInfoView
    private var infoViews: [infoView] = []

    public func renderCell(info: IGGIDAccountModel) {
        headerBaseInfoView.render(renderInfo: info.baseInfo)
        renderInfoViews(info.infos)
    }
    
    private func renderInfoViews(_ infos: [someInfoModel]?) {
        guard let infos = infos, infos.count > 0 else { return }
        
        // 如果已经创建过了,那么只要赋值就可以了
        if (infoViews.count > 0) {
            for (index, bindInfo) in bindInfos.enumerated() {
                let infoView = infoViews[index]
                infoView.render(info)
            }
        } else {
            setupInfoViews(infos) // 如果没有持有,表示初次渲染,这时候要赋值
        }
    }
    
    private func setupPlatformBindInfoViews(_ infos: Array<Info>) {
        for (index, bindInfo) in infos.enumerated() {
            let infoView = InfoView()
            containerView.addSubview(infoView)
            infoView.snp.makeConstraints { (make) in
                //... 写约束
            }
            infoViews.append(infoView)
            infoView.render(info) // 记得再去调用一次
        }
    }
复制代码

下面是讲解:

  • 类中要去持有那些会被渲染的视图,作为属性内容。
  • headerBaseInfoView 是固定的内容,所以实际上我们在重写他的初始化方法的时候,直接就把 setupUI()(只会执行一次)这个绘图的工作做掉了
  • infoViews 属于我一开始没办法知道你有几个,所以我无法初始化。因此我只能在render的时候做下面两个操作:
    • 先对infoViews判空,不存在,那么就做第一次的setupUI操作,这时候根据model也已经知道数量了,先把绘图工作做了。
      • 绘图完毕之后,也要做一次正规的 render操作,不然会没数据,变成空白
    • 完成了setupUI之后,刷新数据

综上所述,就是和概念中说的一样:

cell的操作都是先通过setupUI绘制视图,然后去render把数值赋过去。只是一些条件下,我们不知道要画几个,那么只能在render的时候根据当前数据,补上setupUI之后,再去做真正的render


刷新的问题

先来说说 reloadData的缺点

  • 性能问题 我们都知道,UITableviewreloadData 是需要慎用的。因为他会将整个tableview 都刷新一遍。这意味着也许我只需要刷新2个cell,你却让所有的cell都重渲染了一遍。从性能而言这显然是不可取的。 所以我们才会想到去用局部刷新。

  • reloadData 无法像系统提供的其他刷新方法一样,带有animate参数,这让刷新时,整个页面看起来非常突兀。如果你不自己加动画,那么体验真的不太好

  • iOS 11 上会有一个问题,就是重载之后页面会乱跑:

    页面乱跑.gif

    • 解决办法: google后,得到的内容是说 Self-Sizing在iOS11下是默认开启的,Headers, footers, and cells都默认开启Self-Sizing,所有estimated 高度默认值从iOS11之前的 0 改变为UITableViewAutomaticDimension

      if #available(iOS 11.0, *) {
        taleview.estimatedRowHeight = 0
        taleview.estimatedSectionHeaderHeight = 0
        taleview.estimatedSectionFooterHeight = 0
      }
      复制代码

局部刷新的问题

鉴于上面讲的reloadData,我们很自然的就会想到使用局部刷新来做。

tableview.beginUpdates()
tableview.reloadRows(at: tableview.indexPathsForVisibleRows!, with: .none)
tableview.endUpdates()
复制代码

然而,事实比我们想象的要残酷:

局部刷新的效果

局部刷新的效果.gif

使用 reveal 查看,发现多了一个层级,盖在应该有的位置

image.png

我查看了官方的文档,也自己做了断点调试,发现:

  • reloadData 的重载,会把复用池中的 cell 按需要重新拿出来用一次。不会创建新的cell
  • reloadRows 的重载,会重新创建两个新的cell. 创建就创建了吧,可是你为啥在复用的时候,视图的渲染出了问题?

下面两篇文章也提到了类似的问题。 参考文章一 慎用局部刷新

目前来看,似乎是OC下不会出问题,swift 3(我用的是4.0的版本)下会有这个 bug。 目前还是先使用reloadData的全局刷新替换局部刷新,希望后续会有更好的办法

如果有更好的办法解决,欢迎告知。

猜你喜欢

转载自juejin.im/post/5b9a80276fb9a05d035bc640