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

综述

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

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

遇到的问题

  • cellForRowAtIndexPath 代理中,对 cell(尤为是自定义cell) 的初始化异同
    • OC的区别 —— 不能使用OC的那种判空方式来初始化
    • 初始化不能使用自定义的方法 —— 经过dequeue方法获得的cell 永远都是非空的,换言之,即使你自定义了一个初始化方法,它也不会被执行到。
    • 经过渲染方式(render)来绘制图像,赋值
    • 理解cell的复用机制
  • 刷新的问题
    • 使用 reloadData时候,在iOS 11 上会产生抖动
    • insertRowdeleteRowreloadRows 同样都属于局部刷新的范畴,局部刷新时,系统会建立一个新的cell来,并和旧的cell在刷新时来回切换。

先明确几个概念

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

重申 Cell 的复用机制和使用

简单的来讲,tableview 的复用机制是咱们在 cellForRowAtIndexPath 的一系列操做。app

  • CellUI 一旦被建立,系统就会存放在复用池中等待复用。
  • Cell 的可变内容(一般是labeltextimage的内容,选中的背景色等),是不会记录的。
  • 删除某个 Cell 后再建立一个新的 Cell, 实际上你会发现新的 Cell 中有部分 UI 时旧 Cell中的
  • reloadRows 局部刷新时会建立新的 Cell,再刷新时会和旧的Cell来回切换

很简单的状况是,若是咱们不每次滚动的时候去dataSource数组中把对应index的数值取出来,只管的感觉就是UI虽然固定,可是数据和图片一直在乱跑布局

鉴于Swift 没法自定义cell的初始化,那么上下滚动时,怎么从新赋值而不重复绘制就显得格外重要。性能

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

咱们要讨论的是在Cell复用过程当中的赋值和 UI 重叠的问题。google

典型案例 —— Cell 的 UI 内容根据数据而定

描述

根据上面所说的,CellUI 在被建立后,就会被放进复用池中,等待被重用。可是若是像下面这种状况:spa

一个TableView 中每一个Cell 的内容是根据数据中数组的个数来渲染的,就会出问题: 3d

image.png
咱们这里的 Cell 分了不少层级,

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

这样会形成一个很大的问题:

  • 由于根据复用机制,数据是每次都有可能不一样的,而根据数据建立的 UI 一旦被建立,就会一直存在于复用池中。
  • 若是 Cell 发生了删除,再添加,就有可能将那些不用的Cell UI 复用进来。
  • 局部刷新时会建立新的 Cell,这时候叠加在旧的UI上切换时,就会形成视图的重叠

来看下错误的现象图

局部刷新的效果

局部刷新的效果.gif

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

image.png

正确的代码

为了不混淆,我这里就不贴原来错误的代码了。

来看下面正确的代码

// 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
}
复制代码

思路:

  • 上面的图中,Header的部分是固定的,也就是否是动态变化的 UI,所以每次render的时候只要从新赋值便可
  • 而下面的infoA, infoB, infoC...是根据数值来变化的。咱们如今能作的就是对于动态的 Cell UI,先把这几个 subViewremoveFromSuperView 避免干扰,而后setUp重绘一次,再render进赋值。

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

// 略去类的初始化,这里为了 render ,去持有静态的 UI
    private var headerBaseInfoView: BaseInfoView = BaseInfoView()

    public func renderCell(info: accountModel) {
    // 除了静态的 UI,剩下的都remove 掉,避免重用时的干扰
        for view in contentView.subviews {
            guard view != headerBaseInfoView else {
                continue
            }
            view.removeFromSuperview()
        }
        
        headerBaseInfoView.render(renderInfo: info.baseInfo!)
        setupAndRenderInfoViews(bindInfos)
    }
    
    private func setupAndRenderInfoViews(_ bindInfos: [infoModel]) {
        var infoViews: [infoView] = []
        for (index, bindInfo) in bindInfos.enumerated() {
            // 建立后渲染数据
            let bindInfoView = InfoView()
            bindInfoView.render(bindInfo: bindInfo)
            
            // 布局 (也能够先布局再渲染数据,这无所谓)
            contentView.addSubview(bindInfoView)
            bindInfoView.snp.makeConstraints { (make) in
                //这里略去约束的部分
            }
            infoViews.append(bindInfoView)
        }
    }
复制代码

下面是讲解:

  • 类中要去持有静态的视图,做为属性内容。
  • headerBaseInfoView 是固定的内容,因此实际上咱们在重写他的初始化方法的时候,直接就把 setupUI()(只会执行一次)这个绘图的工做作掉了
  • infoViews 属于我一开始没办法知道你有几个,因此我没法初始化。只在每次渲染数据的时候:
    • 先将全部动态视图remove
    • 根据数据内容从新渲染视图并赋值(也能够先赋值再渲染数据,不影响)

刷新的问题

先来讲说 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()
复制代码

实际上和 reload 没有太多的差别,只是注意局部刷新,会建立新的Cell

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


由于以前对重用机制的理解存在误区,因此文章内容更新了。

相关文章
相关标签/搜索