最近在工作中遇到了一个问题,就是在AutoLayout
中如何使UITableViewCell
的行高根据内容(具体就是UILabel
的多行显示)达到自适应高度。google so一通找到了一些资料,但都不是特别齐全,特别是针对iOS7的兼容上,因此也踩了不少坑,在这里做一个总结。
首先看一下效果
其实在iOS8以上,cell的动态高度的实现已经变的很方便了,只需要你在cell里面正确设置约束,再设置tableview
的几个属性,就大功告成了!
首先正确设置约束
这里要注意的就是,一定要确保在设置约束之前,你
UITableViewCell
的size inspector里面 Row Height 是Default而不是custom的数值,否则之后不管你如何操作,UITableViewCell
优先使用的都是custom的数值
还有一点要注意的是,如果你和我一样,是
UILabel
需要多行显示造成的行高不固定,那么你的UILabel
的行数要设置为0,表示UILabel
显示的是多行。
最后在viewDidLoad中加上
self.tableView.estimatedRowHeight = 56
self.tableView.rowHeight = UITableViewAutomaticDimension
estimatedRowHeight
是假定的高度,因为需要预估UITableView
的UIScrollView
的contentSize
。因此这种方法可能潜在的问题就是数据量大的时候滚动条可能会闪动。具体的解决方法还没有研究,应该可以通过UIScrollView
的代理方法解决。UITableViewAutomaticDimension
这一句在iOS8+是作为rowHeight
的默认值的,这句话也可以不写。
至此iOS8+ AutoLayout的动态行高就大功告成了,你甚至可以不用去实现heightForRowAtIndexPath
。
但是iOS7的兼容就显得蛋疼许多了,主要是由于,iOS7使用UITableView一定要实现heightForRowAtIndexPath
代理方法,这里你可能会觉得,那我们实现这个代理方法,返回UITableViewAutomaticDimension
就好了啊,遗憾的是UITableViewAutomaticDimension
在iOS7里面也是不可用的,不信你可以试一下,直接crash。
所以我们的思路得这样走,实现heightForRowAtIndexPath
, 创建一个临时的cell,设置cell里面各个view的属性,主动触发layout,通过UIView的方法systemLayoutSizeFittingSize(UILayoutFittingCompressedSize)
获取cell的实际尺寸,作为返回的高度。代码看起来像下面这样.
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
//兼容ios7
if NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 {
let mockcell = tableView.dequeueReusableCellWithIdentifier(youtidentifier)
//
//设置你的cell的子view,比如UILabel的title
//
let height = mockcell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + 1
return height
}
}
else {
return UITableViewAutomaticDimension
}
}
上面的代码需要注意的一点就是,tableView.dequeueReusableCellWithIdentifier
这个方法千万不能传入indexPath
,否则会死循环,一直调用该方法。
上面的代码其实还有一个很明显的问题,就是每一次计算高度都需要创建一次UITableViewCell,我们可以做一个简单的改进,用一个Dictionary<NSIndexPath,CGFloat>
存储计算过的indexPath对应的height,代码如下。
private var heightOfIndex = [NSIndexPath:CGFloat]()
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
//兼容ios7
if NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 {
if let height = self.heightOfIndex[indexPath] {
return height
} else {
let mockcell = tableView.dequeueReusableCellWithIdentifier(youtidentifier)
//
//设置你的cell的子view,比如UILabel的title
//
let height = mockcell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + 1
self.heightOfIndex[indexPath] = height
return height
}
}
else {
return UITableViewAutomaticDimension
}
}
为了能够代码重用,我们可以把他封装成一个类,设置cell的过程作为一个block传入。
class ALTableViewCellHeight {
private var heightOfIndex = [NSIndexPath:CGFloat]()
func heightForRowAtIndexPath(indexPath:NSIndexPath,initCell:()->UITableViewCell) -> CGFloat {
if NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_8_0 {
if let height = self.heightOfIndex[indexPath] {
return height
} else {
let cell = initCell()
cell.layoutIfNeeded()
let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height + 1
self.heightOfIndex[indexPath] = height
return height
}
}
else {
return UITableViewAutomaticDimension
}
}
}
最后再说说iOS7下UILabel的一个坑,如果要使你的UILabel能够正常的多行显示,除了一开始说到的设置numberOfLines
为0,还需要设置preferredMaxLayoutWidth
,这个属性指的当换行的最大宽度。iOS8+能够通过AutoLayout计算出UILabel
的宽度,把这个宽度作为preferredMaxLayoutWidth
,但是iOS7下面不行,应该是一个bug。解决的方案是继承UILabel
,重写layoutSubviews
。
class LabelDynamicHeight:UILabel {
override func layoutSubviews() {
super.layoutSubviews()
self.preferredMaxLayoutWidth = self.frame.size.width
super.layoutSubviews()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.numberOfLines = 0
}
}