美文网首页ios漫漫学习路
Swift 踩坑笔记(五)—— UITableView Cell

Swift 踩坑笔记(五)—— UITableView Cell

作者: 黑羽肃霜 | 来源:发表于2018-09-13 23:15 被阅读553次

    综述

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

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

    遇到的问题

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

    先明确几个概念

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

    重申 Cell 的复用机制和使用

    简单的来说,tableview 的复用机制是我们在 cellForRowAtIndexPath 的一系列操作。

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

    很简单的情况是,如果我们不每次滚动的时候去dataSource数组中把对应index的数值取出来,只管的感受就是UI虽然固定,但是数据和图片一直在乱跑

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

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

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

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

    描述

    根据上面所说的,CellUI 在被创建后,就会被放进复用池中,等待被重用。但是如果像下面这种情况:

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

    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

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


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

    相关文章

      网友评论

        本文标题:Swift 踩坑笔记(五)—— UITableView Cell

        本文链接:https://www.haomeiwen.com/subject/ywfngftx.html