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