Fork me on GitHub

UITableViewCell 高度自适应牵扯出的若干问题

写在前面

阔别了一年写代码的感觉,工作以来一直从事产品经理的相关工作,最近开始寻找当年写代码的感觉。碰巧上来被一个原以为很简单的问题困扰了很久,下面给大家详细讲讲使用 Self-Sizing Cell 做高度自适应遇到的若干坑。

Self-Sizing Cell

Self-Sizing 是 Apple 在 iOS 8 之后推出的新技术,是用于在调整系统字体大小后,控件元素中的文字能跟自动使用布局,下图所示的就是配合 Self-Sizing 推出的系统修改字体。

文字大小

在 iOS8 以前,决定 tableviewcell 的高度的都是 tableView:heightForRowAt:,在该方法中手动计算 cell 的高度。iOS 8 之后,Apple 推出了新技术 Self-Sizing,通过 Working with Self-Sizing Tableview cells,将 cell 和 Self-Sizing 进行配合使用后这个协议函数就不用管了,高度系统会自动进行计算。具体的实现方式如下:

Step One

通过 StoryBoard 新建 Custom TableView Cell,并且把要自适应文本高度的 UILabel 添加上约束。约束的原则如果上下左右没有其他元素,直接和 SuperView 做相对约束,如果有其他元素,则其他元素需要布局确定,UILabel 做相对约束。注意很重要的一点,最后 UILabel 的约束最后对宽和高都没有做约束,只有上下左右四边的约束看,这样的 Cell 才可以进行高度自适应。

设置 StoryBoard

Step Two

在代码中添加两个方法,接下来加载出数据的时候就可以自动计算高度。

1
2
3
4
5
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 100//估算高度尽可能的接近 cell 的高度
}

由此引发的问题

1.willDisplayCell:forRowAtIndexPath: and cellForRowAtIndexPath:

在关于优化 UITableView 的文章中,提到了一个优化方案。

我们经常在注意cellForRowAtIndexPath:中为每一个cell绑定数据,实际上在调用cellForRowAtIndexPath:的时候cell还没有被显示出来,为了提高效率我们应该把数据绑定的操作放在cell显示出来后再执行,可以在tableView:willDisplayCell:forRowAtIndexPath:方法中绑定数据。

我刚刚开始对这个观点是认可,并且也确实这么做了,于是代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: CustomCell = tableView.dequeueReusableCell()
return cell
}
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
guard let cell = cell as? CustomCell else {
return
}
cell.titleLabel.text = Json.desc
return
}

在执行上述代码之后,我发现了一个问题,UITableViewCell 的高度加载并不正确,是估算的高度,并不是计算的高度,如下图。

未计算出正确高度

滑动之后新加载的 TableViewCell 高度为计算出来且正确。(继承 UITableViewViewController 即 Delegate and DataSource 方法为 override 可以滑动加载正确高度;而继承自 UIViewController 即 Delegate and DataSource 方法是 extension 滑动也不可以加载正确高度。注意,后面的继承未写 Demo 论证,若有不对请指出

从上面的问题里面,我开始寻找两个方法的作用和遇到类似问题的解答方式,我在 Stack Overflow 上面找到了类似的问题 UITableView cellForRowAt vs willDisplay。上面阐述的解释是高度的计算是在 cellForRowAtIndexPath: 之后,在 willDisplayCell:forRowAtIndexPath: 之前,所以需要再次调用 reloadData 方法。

My understanding that at height is calculating after cellForRowAt but before willDisplay, so when I’m mapping in willDisplay height is already set for my cell and I need to reload it again (which is not good).

这个结论我是认同,所以解决这个问题的办法就有两个方案。

  • 将数据绑定从 willDisplayCell:forRowAtIndexPath: 中移到 cellForRowAtIndexPath: 中;
  • 再次调用 reloadData 放大,代码如下:
1
2
3
4
5
self.tableView.reloadData()
// 3 new lines of codes to force size adjustment
self.tableView.setNeedsLayout()
self.tableView.layoutIfNeeded()
self.tableView.reloadData()

上述两个方案,第二个方案是强制高度自适应,具体可以参考这边文章 Swift: How to Resize UITextView + TableViewCell Correctly After JSON Fetch,但是这个方法显得很臃肿,也并不能算是优雅的解决方案。于是我看到第一个方案时产生了疑问:

【为了提高效率我们应该把数据绑定的操作放在 cell 显示出来后再执行】 这个观点是正确的吗?

数据绑定应该放在哪儿?

我们首先看下苹果官方文档关于 willDisplayCell:forRowAtIndexPath: 的解释,该方法应该是修改状态类型基本属性,比如选择状态、背景颜色等等:

This method gives the delegate a chance to override state-based properties set earlier by the table view, such as selection and background color.

官方文档关于 cellForRowAtIndexPath: 的解释,该方法应该是讲数据源插入到 UITableView 正确的位置:

Asks the data source for a cell to insert in a particular location of the table view.

在此同时,我看到了一篇详细的文章 Proper Use of CellForRowAtIndexPath and WillDisplayCell,文章中指出:

Orlov’s article is an important guide for advanced programming. However, he is lacking proof for the tableView delegate method willDisplayCell:forRowAtIndexPath:. This has been something that has bothered me for a while, as so many people quoted the paragraph about willDisplayCell from the article.

很重要的一句话,并没有任何理由能够支持他说的绑定数据在 willDisplayCell 能够提高 UITableView 的性能。并且他做了实验,发现 layoutSubviews 是在 willDisplayCell:forRowAtIndexPath:cellForRowAtIndexPath: 两个方法之后,也就是说不管你在哪个方法中去绑定数据,cell 被 layouted 总在他们之后,那么在这之前就不会 Rendering,也就不存在绑定数据在 cellForRowAtIndexPath: 方法中会影响性能。

那么,这个优化方案并没有任何理论根据,于是忘了吧,我们经常在注意cellForRowAtIndexPath:中为每一个cell绑定数据,实际上在调用cellForRowAtIndexPath:的时候cell还没有被显示出来,为了提高效率我们应该把数据绑定的操作放在cell显示出来后再执行,可以在tableView:willDisplayCell:forRowAtIndexPath:方法中绑定数据。

注意,关于 Orlov’s article 虽然提升这一点没有被佐证,但是其他的优化方案非常值得学习, 我这边附上链接,有兴趣可以学习!注意使用梯子~~ 文章:perfect smooth scrolling in uitableviews

自动计算加载了几次?

本来到这边应该结束文章,但是在探讨上述问题的时候,发现了很奇怪的一件事情。如果你在 UITableView Delegate 或者 DataSource 方法中打印一下,会发现每个方法中的打印出现了2遍。

打印两边

于是,我参考了两篇文章:scrollViewDidScroll is called “twice” after set table offsetheightForRowAtIndexPath called twice in iOS 8 but one time in iOS 7

tableView.estimatedRowHeight = 100 这句话是引起 UITableView 加载两次的原因,注释掉这句话后,发现打印一次。我并没有找到合理的理由去解释这个原因,网上部分猜测是 Apple 官方的 Bug,如果大家对这个问题有更好的解释,不妨联系我,我会第一时间更新。


版权声明



Ivan’s Blog by Ivan Ye is licensed under a Creative Commons BY-NC-ND 4.0 International License.
叶帆创作并维护的叶帆的博客博客采用创作共用保留署名-非商业-禁止演绎4.0国际许可证

本文首发于Ivan’s Blog | 叶帆的博客博客( http://yeziahehe.com ),版权所有,侵权必究。

本文链接:http://yeziahehe.com/2017/06/30/UITableViewCell_Self_Sizing/