UITableView 全解析

1.两个获得重用cell方法对比

1.1 iOS 6出的新方法。open func dequeueReusableCell(withIdentifier identifier: String, for indexPath: IndexPath) -> UITableViewCell

该方法需要提前registerClass,一定会返回cell(内部判断从缓存池或者通过注册的标识符创建新的cell)。

1.2 iOS 旧方法。dequeueReusableCell(withIdentifier identifier: String) -> UITableViewCell?

该方法一般从缓存池中获取可复用的cell,可能为nil。但从官方文档 可以看到,这个方法也会通过注册的标识符创建cell,只有缓存池没有取到,且没有注册标识符时才会返回nil。
https://developer.apple.com/documentation/uikit/uitableview/1614891-dequeuereusablecellwithidentifie?language=objc

A table view maintains a queue or list of UITableViewCell objects that the data source has marked for reuse. Call this method from your data source object when asked to provide a new cell for the table view. This method dequeues an existing cell if one is available or creates a new one using the class or nib file you previously registered. If no cell is available for reuse and you did not register a class or nib file, this method returns nil. ``

1.3 两个方法的差异。

除了使用方式的差异以外,测试发现iOS 6新方法会在cell创建的同时调用heightForRowAt indexPath来获取高度,而旧方法不会。新旧方法都会在cell创建之后获取一次高度。

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        print("cellForRowAtIndexPath CellWillCreate:" + String(indexPath.row))
        let cell = table.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as? TestCell
        print("cellForRowAtIndexPath CellDidCreated:" + String(indexPath.row))
        if let cell = cell {
            cell.setupSubviews(content: dataSource[indexPath.row])
        }
        return cell ?? UITableViewCell()
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        print("heightForRowAt:" + String(indexPath.row))
        if let height = heightCache.readHeight(indexPath: indexPath) {
            return height
        } else {
            tempCell.setupSubviews(content: dataSource[indexPath.row])
            let height = tempCell.getCellHeight()
            heightCache.setHeight(height, for: indexPath)
            return height
        }
    }

输出结果:

cellForRowAtIndexPath CellWillCreate:0
heightForRowAt:0
heightForRowAt:0
cellForRowAtIndexPath CellDidCreated:0
heightForRowAt:0
cellForRowAtIndexPath CellWillCreate:1
heightForRowAt:1
heightForRowAt:1
cellForRowAtIndexPath CellDidCreated:1
heightForRowAt:1
cellForRowAtIndexPath CellWillCreate:2
heightForRowAt:2
heightForRowAt:2
cellForRowAtIndexPath CellDidCreated:2
heightForRowAt:2

而使用旧方法输出结果:

cellForRowAtIndexPath CellWillCreate:0
cellForRowAtIndexPath CellDidCreated:0
heightForRowAt:0
cellForRowAtIndexPath CellWillCreate:1
cellForRowAtIndexPath CellDidCreated:1
heightForRowAt:1
cellForRowAtIndexPath CellWillCreate:2
cellForRowAtIndexPath CellDidCreated:2
heightForRowAt:2

2. cell获取高度方式汇总:

2.1 对于table中所有cell都是固定高度的情况,推荐直接设置table的rowHeight属性,避免 heightForRowAt 代理的频繁回调。
        table.rowHeight = 40
        // 关闭预估高度,默认开启AutomaticDimension
        table.estimatedRowHeight = 0
2.2 如果table中存在高度不同的cell,可以在 heightForRowAt 回调返回每个cell的高度。
        // 关闭预估高度,默认开启AutomaticDimension
        table.estimatedRowHeight = 0

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
          return 1
      }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 10
    }
      
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        print("heightForRowAt:", String(indexPath.section))
        return 100
    }
    
      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        print("cellForRowAt:", String(indexPath.section))

        let cell = table.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as? TestCell

        return cell ?? UITableViewCell()
      }

输出结果:

heightForRowAt: 9
heightForRowAt: 0
heightForRowAt: 1
heightForRowAt: 2
heightForRowAt: 3
heightForRowAt: 4
heightForRowAt: 5
heightForRowAt: 6
heightForRowAt: 7
heightForRowAt: 8
heightForRowAt: 9
heightForRowAt: 0
heightForRowAt: 1
heightForRowAt: 2
heightForRowAt: 3
heightForRowAt: 4
heightForRowAt: 5
heightForRowAt: 6
heightForRowAt: 7
heightForRowAt: 8
heightForRowAt: 9
heightForRowAt: 0
heightForRowAt: 1
heightForRowAt: 2
heightForRowAt: 3
heightForRowAt: 4
heightForRowAt: 5
heightForRowAt: 6
heightForRowAt: 7
heightForRowAt: 8
cellForRowAt: 0
heightForRowAt: 0
heightForRowAt: 0
cellForRowAt: 1
heightForRowAt: 1
heightForRowAt: 1
cellForRowAt: 2
heightForRowAt: 2
heightForRowAt: 2
cellForRowAt: 3
heightForRowAt: 3
heightForRowAt: 3
cellForRowAt: 4
heightForRowAt: 4
heightForRowAt: 4
cellForRowAt: 5
heightForRowAt: 5
heightForRowAt: 5
cellForRowAt: 6
heightForRowAt: 6
heightForRowAt: 6

通过测试可以发现,在没有开启estimatedRowHeight的情况下,table只会加载当前页面显示所包括的cell(cellForRowAt回调),但table会加载所有cell展示所需要的高度(heightForRowAt)。table继承自UIScrollView,可以理解成通过读取所有cell的高度来更新scrollView的contentSize。

2.3 通过estimateRowHeight设置预估高度。

heightForRowAtIndexPath的方式会在table初次加载数据时加载所有数据的高度,会增加初次加载的性能开销。iOS 7便推出了 estimateRowHeight来优化该问题,将高度计算延迟到滑动到cell将要加载时,而在cell未加载时,只需告诉table一个预估的高度即可(table在cell未加载时,通过预估高度来计算contentSize,当cell加载进入heightForRowAtIndexPath回调后,用回调返回的高度代替预估高度)。

可以通过设置table的estimatedRowHeight属性设置所有cell采用相同预估高度,也可以通过estimatedHeightForRowAt回调指定每个cell的预估高度。

    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        print("estimateheight" + String(indexPath.section))
        return 100 或 20 或 150
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        print(scrollView.contentSize.height)
    }

输出结果总结:
任何情况下estimatedHeightForRowAt都会加载所有的section。
当 estimatedHeightForRowAt 返回 100 或 150时,table初次加载时cellForRowAt 和 heightForRowAt 都只会加载 section 为0-6。
当 estimatedHeightForRowAt 返回 20时,cellForRowAt 加载 0-6 的cell,heightForRowAt 加载所有( 0-9)。
当estimatedHeightForRowAt 为 150时,随着table初次向下滑动,右侧滚动条长度和位置出现跳动,contentSize.height也随着发生变化,但当所有cell都加载一遍以后,滑动不会造成contentSize.height的变化。

分析:在设置预估高度以后,table在初次加载时,会对所有的cell使用预估高度来初始化table的contentSize,然后通过预估高度和实际展示的cells决定哪些cell需要调用heightForRowAt来获取高度。比如当预估高度设置为20,table认为所有的cell都需要在初次加载时加载,所以会对所有的cell调用heightForRowAt。当滑动时候,随着新的cell加载,heightForRowAt将会代替estimatedHeightForRowAt来计算contentSize,当整个table所有cell都进入了heightForRowAt,用于计算contentSize.height预估高度都已经把真实cell高度代替。

2.4 对于动态高度的cell,使用frame布局,在cell中专门创建一个临时cell用于计算高度。
// TestCell
    func setupUI() {
        contentView.addSubview(contentLabel)
    }
    
    func setupSubviews(content: String?) {
        contentLabel.width = kScreenW
        contentLabel.text = content
        contentLabel.sizeToFit()
    }
    
    func getCellHeight() -> CGFloat {
        return contentLabel.bottom
    }

// TestTableViewController
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataSource.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = table.dequeueReusableCell(withIdentifier: "TestCell", for: indexPath) as? TestCell
        if let cell = cell {
            cell.setupSubviews(content: dataSource[indexPath.row])
        }
        return cell ?? UITableViewCell()
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        // 从缓存中读取高度,若未取到,使用临时cell初始化数据并获取高度
        if let height = heightCache.readHeight(indexPath: indexPath) {
            return height
        } else {
            tempCell.setupSubviews(content: dataSource[indexPath.row])
            let height = tempCell.getCellHeight()
            heightCache.setHeight(height, for: indexPath)
            return height
        }
    }

缺点:每个cell的首次初始化都需要执行两次赋值数据(第二次用于计算高度),对性能有少量影响。

2.5 对于动态高度的cell,使用frame布局,在cell中实现一个获取高度的类方法。
// Test2Cell
    class func getCellHeight(content: String?) -> CGFloat {
        let tempLabel = UILabel()
        tempLabel.numberOfLines = 0
        tempLabel.text = content
        // 使用boundingRectWithSize获取高度,若遇到\n不准
        let height = tempLabel.sizeThatFits(.init(width: kScreenW, height: 0)).height
        return tempLabel.top + height
    }

// TestViewController
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        if let height = heightCache.readHeight(indexPath: indexPath) {
            return height
        } else {
            let height = Test2Cell.getCellHeight(content: dataSource[indexPath.row])
            heightCache.setHeight(height, for: indexPath)
            return height
        }
    }

缺点:在每个cell中实现类方法获取高度工作量巨大,且性能低下。

2.6 对于动态高度的cell,使用frame布局,在cell创建的同时获取实例的高度并放入缓存,heightForRowAtIndexPath取缓存中的高度。

分析:若能在heightForRowAtIndexPath前获取到cell实例,则可解决前两种方案的不足。进行如下代码测试:

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        print("cellForRowAtIndexPath CellWillCreate:" + String(indexPath.row))
        let cell = table.dequeueReusableCell(withIdentifier: "Test2Cell", for: indexPath) as? Test2Cell
        print("cellForRowAtIndexPath CellDidCreated:" + String(indexPath.row))
        if let cell = cell {
            cell.setupSubviews(content: dataSource[indexPath.row])
        }
        return cell ?? UITableViewCell()
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        print("heightForRowAt:" + String(indexPath.row))
//        let cell = table.cellForRow(at: indexPath)
        if let height = heightCache.readHeight(indexPath: indexPath) {
            return height
        } else {
            let height = Test2Cell.getCellHeight(content: dataSource[indexPath.row])
            heightCache.setHeight(height, for: indexPath)
            return height
        }
    }

输出结果:

cellForRowAtIndexPath CellWillCreate:0
heightForRowAt:0
heightForRowAt:0
cellForRowAtIndexPath CellDidCreated:0
heightForRowAt:0
cellForRowAtIndexPath CellWillCreate:1
heightForRowAt:1
heightForRowAt:1
cellForRowAtIndexPath CellDidCreated:1
heightForRowAt:1
cellForRowAtIndexPath CellWillCreate:2
heightForRowAt:2
heightForRowAt:2
cellForRowAtIndexPath CellDidCreated:2
heightForRowAt:2

发现heightForRowAt的调用与cell的创建同时触发,而在heightForRowAt内使用table.cellForRow(at: indexPath)将会返回nil(因为cellForRowAt indexPath: 还未执行完)。
解决思路:
在cell的创建同时,获取到实例的高度并放入高度缓存中,在heightForRowAt触发时,从缓存中获取高度。

class TestTableView: UITableView {

    var heightCache = HeightCache()

    func testDequeueReusableCell<T: UITableViewCell>(withIdentifier identifier: String? = nil, for indexPath: IndexPath, configureCallback: ((T) -> Void)?) -> T {
        
        let cellIdentifier: String
        if let identifier = identifier {
            cellIdentifier = identifier
        } else {
            let className = NSStringFromClass(T.self)
            cellIdentifier = className
        }
        // 从缓存池或注册标识符中获取cell,若未注册过标识符,可能为nil
        var cell = self.dequeueReusableCell(withIdentifier: cellIdentifier) as? T
        if cell == nil {
            // 注册标识符后再获取cell,必定不为nil
            register(T.self, forCellReuseIdentifier: cellIdentifier)
            cell = dequeueReusableCell(withIdentifier: cellIdentifier) as? T
        }
        
        // 虽然cell必定不为nil,但是仍然为可选类型,转成非可选类型
        let reuseCell = cell ?? T.init(style: .default, reuseIdentifier: cellIdentifier)

        configureCallback?(reuseCell)
        
        if let reuseCell = reuseCell as? ViewHeightCacheProtocol {
            // 判断是否有高度缓存,没有则将高度存入缓存
            let height = self.heightCache.readHeight(indexPath: indexPath)
            if height == nil {
                self.heightCache.setHeight(reuseCell.viewCacheHeight(), for: indexPath)
            }
        }
        return reuseCell
    }

    func getCellHeightFromCache(for indexPath: IndexPath) -> CGFloat? {
        let height = self.heightCache.readHeight(indexPath: indexPath)
        return height
    }
}

/// 高度缓存协议
protocol ViewHeightCacheProtocol {
    func viewCacheHeight() -> CGFloat
}

extension Test3Cell: ViewHeightCacheProtocol {
    func viewCacheHeight() -> CGFloat {
        return contentLabel.bottom
    }
}
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
          return dataSource.count
      }
      
      func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
          let cell: Test3Cell = self.table.testDequeueReusableCell(for: indexPath) { cell in
            cell.setupSubviews(content: self.dataSource[indexPath.row])
          }
        return cell
      }
      
      func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
          
          let height = self.table.getCellHeightFromCache(for: indexPath) ?? 0
          print("heightForRowAt:" + String(indexPath.row) + "/" + "\(height)")
          return height
      }

输出结果:

heightForRowAt:0/20.5
heightForRowAt:1/41.0
heightForRowAt:2/61.0
2.6.1 但对headerFooterView的获取高度时机进行同样测试,发现和cell不同。

tableView为grouped风格时,输出如下:

heightForHeaderInSection:0
viewForHeaderInSection viewWillCreate:0
viewForHeaderInSection viewDidCreated:0
heightForHeaderInSection:1
viewForHeaderInSection viewWillCreate:1
viewForHeaderInSection viewDidCreated:1
heightForHeaderInSection:2
viewForHeaderInSection viewWillCreate:2
viewForHeaderInSection viewDidCreated:2

tableView为plain风格时,输出如下:

heightForHeaderInSection:1
heightForHeaderInSection:2
heightForHeaderInSection:0
viewForHeaderInSection viewWillCreate:0
viewForHeaderInSection viewDidCreated:0
viewForHeaderInSection viewWillCreate:1
viewForHeaderInSection viewDidCreated:1
viewForHeaderInSection viewWillCreate:2
viewForHeaderInSection viewDidCreated:2

分析:两种输出结果表明对高度的获取都在获取view(viewForHeaderInSection)以前,表现出和cell不同的触发时机。
可以在获取高度的时候提前创建headerFooterView,在viewForHeaderInSection复用刚才创建的view。虽然获取高度时会对cell多执行一次viewForHeaderInSection回调,但因为view是可复用,并没有创建多余的view,且因为对高度进行了缓存,下一次获取高度时直接从缓存获取高度,并不会多执行一次回调。

enum TableViewSectionPostion {
    case header
    case footer
}

class TestTableView: UITableView {

    var heightCache = HeightCache()
    var headerViewHeightCache = HeightCache()
    var footerViewHeightCache = HeightCache()

    func testDequeueReusableCell<T: UITableViewCell>(withIdentifier identifier: String? = nil, for indexPath: IndexPath, configureCallback: ((T) -> Void)?) -> T {
        
        let cellIdentifier: String
        if let identifier = identifier {
            cellIdentifier = identifier
        } else {
            let className = NSStringFromClass(T.self)
            cellIdentifier = className
        }
        // 从缓存池或注册标识符中获取cell,若未注册过标识符,可能为nil
        var cell = self.dequeueReusableCell(withIdentifier: cellIdentifier) as? T
        if cell == nil {
            // 注册标识符后再获取cell,必定不为nil
            register(T.self, forCellReuseIdentifier: cellIdentifier)
            cell = dequeueReusableCell(withIdentifier: cellIdentifier) as? T
        }
        
        // 虽然cell必定不为nil,但是仍然为可选类型,转成非可选类型
        let reuseCell = cell ?? T.init(style: .default, reuseIdentifier: cellIdentifier)

        configureCallback?(reuseCell)
        
        if let reuseCell = reuseCell as? ViewHeightCacheProtocol {
            // 判断是否有高度缓存,没有则将高度存入缓存
            let height = self.heightCache.readHeight(indexPath: indexPath)
            if height == nil {
                self.heightCache.setHeight(reuseCell.viewCacheHeight(), for: indexPath)
            }
        }
        return reuseCell
    }

    func getCellHeightFromCache(for indexPath: IndexPath) -> CGFloat? {
        let height = self.heightCache.readHeight(indexPath: indexPath)
        return height
    }
    
    func testDequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(withIdentifier identifier: String? = nil, for section: Int, position: TableViewSectionPostion, configureCallback: ((T) -> Void)?) -> T {
        
        let viewIdentifier: String
        if let identifier = identifier {
            viewIdentifier = identifier
        } else {
            let className = NSStringFromClass(T.self)
            viewIdentifier = className
        }
        // 从缓存池或注册标识符中获取view,若未注册过标识符,可能为nil
        var headerFooterView = self.dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier) as? T
        if headerFooterView == nil {
            register(T.self, forHeaderFooterViewReuseIdentifier: viewIdentifier)
            headerFooterView = dequeueReusableHeaderFooterView(withIdentifier: viewIdentifier) as? T
        }
        
        // 虽然headerFooterView必定不为nil,但是仍然为可选类型,转成非可选类型
        let reuseHeaderFooterView = headerFooterView ?? T.init(reuseIdentifier: viewIdentifier)

        configureCallback?(reuseHeaderFooterView)
        
        if let reuseHeaderFooterView = reuseHeaderFooterView as? ViewHeightCacheProtocol {
            // 判断是否有高度缓存,没有则将高度存入缓存
            if position == .header {
                let height = self.headerViewHeightCache.readHeight(section: section)
                if height == nil {
                    self.headerViewHeightCache.setHeight(reuseHeaderFooterView.viewCacheHeight(), forSection: section)
                }
            } else {
                let height = self.footerViewHeightCache.readHeight(section: section)
                if height == nil {
                    self.footerViewHeightCache.setHeight(reuseHeaderFooterView.viewCacheHeight(), forSection: section)
                }
            }
        }
        return reuseHeaderFooterView
    }
    
    func getViewHeightFromCache(for section: Int, position: TableViewSectionPostion) -> CGFloat? {
        let height: CGFloat?
        if position == .header {
            height = self.headerViewHeightCache.readHeight(section: section)
        } else {
            height = self.footerViewHeightCache.readHeight(section: section)
        }
        
        if let height = height {
            // 返回缓存中获取高度
            return height
        } else {
            // 将cell复用提前到获取高度时
            if position == .header {
                _ = self.delegate?.tableView?(self, viewForHeaderInSection: section)
                return self.headerViewHeightCache.readHeight(section: section)
            } else {
                _ = self.delegate?.tableView?(self, viewForFooterInSection: section)
                return self.footerViewHeightCache.readHeight(section: section)
            }
        }
    }
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        print("viewForHeaderInSection viewWillCreate:" + String(section))
        let headerView: TestHeaderFooterView = self.table.testDequeueReusableHeaderFooterView(for: section, position: .header) { headerFooterView in
            headerFooterView.setupSubviews(content: self.dataSource[section])
        }
        print("viewForHeaderInSection viewDidCreated:" + String(section))
        return headerView
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        
        let height = table.getViewHeightFromCache(for: section, position: .header)
        print("heightForHeaderInSection:" + String(section) + "/" + "\(height)")
        return height ?? 0
    }

输出结果:

viewForHeaderInSection viewWillCreate:1
viewForHeaderInSection viewDidCreated:1
heightForHeaderInSection:1/Optional(41.0)
viewForHeaderInSection viewWillCreate:2
viewForHeaderInSection viewDidCreated:2
heightForHeaderInSection:2/Optional(61.0)
viewForHeaderInSection viewWillCreate:0
viewForHeaderInSection viewDidCreated:0
heightForHeaderInSection:0/Optional(20.5)
viewForHeaderInSection viewWillCreate:0
viewForHeaderInSection viewDidCreated:0
viewForHeaderInSection viewWillCreate:1
viewForHeaderInSection viewDidCreated:1
viewForHeaderInSection viewWillCreate:2
viewForHeaderInSection viewDidCreated:2
效果图.png
2.7 使用autolayout结合 systemLayoutSizeFitting 方法。

在2.6的基础上将cell改造成自动布局,通过self.contentView.systemLayoutSizeFitting方法获取contentView的约束后高度。
systemLayoutSizeFitting 方法的计算的时间开销非常大。这个方法调用,其目的是从计算引擎中重新获得调用方法对应视图的 Size。所以在大多数情况下,减少 systemLayoutSizeFitting()的调用可以削减时间开销。
在理解systemLayoutSizeFitting前需要先理解intrinsicContentSize。
intrinsicContentSize可以理解成view一个默认的size,可以通过重写intrinsicContentSize方法来改变view的默认size。在autolayout中,如果没有为view指定size,view将会使用intrinsicContentSize作为默认size。像UILabel,UIImageView,UIButton都可以通过填充内容来决定自身大小,可以理解成系统为它们重写了intrinsicContentSize方法,并返回内容填充后的最适合大小。autolayout在设置约束后并不会立即生效,可以调用layoutIfNeeded让约束立即生效。
UILabel有个preferredMaxLayoutWidth属性,用于为多行UILabel设定单行最大宽度。官方的说明如下:
// Support for constraint-based layout (auto layout). If nonzero, this is used when determining -intrinsicContentSize for multiline labels
intrinsicContentSize会使用preferredMaxLayoutWidth来计算默认size。由于文字排列的不同,多行UILabel 的preferredMaxLayoutWidth>= intrinsicContentSize.width。

let label = UILabel()
        label.font = UIFont.preferredFont(forTextStyle: .body)
        label.text = "test label text"
        label.numberOfLines = 0
        view.addSubview(label)
        label.snp.makeConstraints { (make) in
            make.top.equalTo(50)
            make.width.lessThanOrEqualTo(50)
            make.left.equalToSuperview()
        }
        
        func printLabelInfo() {
            print(label.frame, label.intrinsicContentSize, label.preferredMaxLayoutWidth)
        }
        
        // 设置约束后,约束并没有立即生效,intrinsicContentSize按单行显示计算
        printLabelInfo()
        label.layoutIfNeeded()
        // 让label自身的约束立即生效
        // 因为frame.origin是相对父坐标系,自身layout只会更新frame.size,layer.position保持(0, 0)
        // 默认 anchorPoint为(0.5, 0.5),label的中心点相对父视图在position(0, 0)位置
        // 约束生效后,系统根据约束自动设置preferredMaxLayoutWidth,并使用preferredMaxLayoutWidth更新多行下的intrinsicContentSize
        printLabelInfo()
        // 将锚定点改成左上角,label左上角相对父视图position为 (0, 0),frame.orgin变成(0, 0)
        label.layer.anchorPoint = .init(x: 0, y: 0)
        printLabelInfo()
        // 让父视图的约束立即生效,更新子视图的frame.origin
        label.superview?.layoutIfNeeded()
        printLabelInfo()

输出结果:

(0.0, 0.0, 0.0, 0.0) (102.5, 20.5) 0.0
(-20.25, -32.25, 40.5, 64.5) (40.5, 64.5) 50.0
(0.0, 0.0, 40.5, 64.5) (40.5, 64.5) 50.0
(20.25, 82.25, 40.5, 64.5) (40.5, 64.5) 50.0

在2.6的基础上,将cell内部代码改成auto layout

// TestCell4
    func setupUI() {
        contentView.addSubview(contentLabel)
        contentLabel.snp.makeConstraints { (make) in
            make.top.left.equalToSuperview()
            make.width.lessThanOrEqualToSuperview()
            // 通过约束与contentView的距离,约束contentView的高度
            make.bottom.equalToSuperview()
        }
    }
    
    func setupSubviews(content: String?) {
        contentLabel.text = content
    }


extension Test4Cell: ViewHeightCacheProtocol {
    
    func viewCacheHeight() -> CGFloat {
        contentView.layoutIfNeeded()
        print("contentLabel.intrinsicContentSize:", terminator: "")
        print(contentLabel.intrinsicContentSize)
        print("contentLabel.preferredMaxLayoutWidth:", terminator: "")
        print(contentLabel.preferredMaxLayoutWidth)
        let size =  self.contentLabel.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        print("contentLabel.systemLayoutSizeFitting:", terminator: "")
        print(size)
        let size2 =  self.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
        print("contentView.systemLayoutSizeFitting:", terminator: "")
        print(size2)

        return size2.height + 1
    }
}

输出结果:

contentLabel.intrinsicContentSize:(312.0, 61.0)
contentLabel.preferredMaxLayoutWidth:320.0
contentLabel.systemLayoutSizeFitting:(312.0, 61.0)
contentView.systemLayoutSizeFitting:(728.0, 20.5)

通过测试发现两个问题:

  1. 虽然通过layoutIfNeeded使contentView的约束立即生效,进而系统算出preferredMaxLayoutWidth,但preferredMaxLayoutWidth的值却是错的。因为cell在创建的时候并不知道table的宽度,所以使用了一个默认宽度320。只有在cellForRowAt indexPath返回cell以后,cell才会将宽度保持和table一致。
  2. contentView.systemLayoutSizeFitting 并没有考虑contentLabel的多行情况。若主动设置contentLabel.preferredMaxLayoutWidth = kScreenW,测试后输出正确:
contentLabel.systemLayoutSizeFitting:(364.0, 41.0)
contentView.systemLayoutSizeFitting:(364.0, 41.0)

systemLayoutSizeFitting遇到多行label的情况,需要手动设置label的preferredMaxLayoutWidth属性。

2.8 第三方框架UITableView-FDTemplateLayoutCell
2.9 使用 self-sizing cell 。

3 高度缓存

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容