使用Autolayout实现UITableView的Cell动态布局和高度动态改变

本文翻译自:stackoverflow

 

有人在stackoverflow上问了一个问题:git

1
如何在UITableViewCell中使用Autolayout来实现Cell的内容和子视图自动计算行高,而且可以保持平滑滚动的?

 

这个问题获得了300+的支持和450+的收藏,答案获得了730+的支持,很详细的说明了如何在iOS7和iOS8上实现UITableView的动态行高功能,而且这个答案对实现UICollectionView的动态行高也具备参考意义。因此在这里将这个答案翻译了一下,但愿对你们有所帮助。如下是答案的全文翻译:github

 

答案略长,若是你不喜欢细读,能够直接看这两个示例的代码:缓存

 

    ● iOS8的示例代码 - iOS8以上才支持iview

    ● iOS7的示例代码 - iOS7+布局

 

核心概念性能

 

无论你是在哪一个iOS版本上作开发,如下步骤中的前两个步骤都是必须的:ui

 

一、设置好布局约束条件spa

 

在UITableViewCell子类中,添加布局约束,使得cell子视图的边缘固定(pin)到cell的contentView的边缘(最重要的是要有顶部和底部的边距约束条件)。注意:不要将子视图的边距约束固定到cell自己上了,只能固定到cell的contentView上! 确保每一个子视图垂直方向上的内容压缩阻力(compression resistance)和吸附性约束(hugging constraints)没有被你添加的更高优先级的约束条件覆盖,让这些子视图的固有内容尺寸(intrinsic content size)来驱动contentView的高度。(没看懂?点这里。线程

 

记住,要点是让cell的子视图与contentView之间产生垂直的连结,让它们可以对contentView“施加压力”,使contentView扩展以适合它们的尺寸。下面用一个cell和一些子视图做为示例,展现了你的一些(不是所有!)布局约束应该看起来是什么样的:翻译

 

54c8ac4b0001ae9504630221.jpg

 

能够设想,随着更多的文本被添加到上例中“Multi-line body”那个label上,它须要垂直地增高以适合文本,这将有效地迫使cell的高度增长。(固然,你须要正确地设置约束条件,以使其正常的工做!)

 

如何设置正确的约束条件,绝对是使用Autolayout实现动态行高时最难最重要的部分。若是弄错了,它就可能没法正常工做——因此,不要着急,慢慢来!我建议你用代码来设置布局约束,这样你就彻底知道每一个布局约束被加到了什么地方,出问题时也更容易调试。特别若是使用一些优秀开源库,可让用代码设置约束和用Interface Builder设置约束同样简单直观,而且功能还更强大。这里也有一个由我设计和维护的专用库:https://github.com/smileyborg/PureLayout

 

    ● 若是你用代码来设置布局约束,你应该在UITableViewCell子类的updateConstraints方法里面一次性完成。注意,updateConstraints可能不止被调用一次,所以要避免重复添加相同的布局约束。在updateConstraints中,能够将添加布局约束的代码包在一个if条件语句中(好比用一个叫didSetupConstraints的布尔属性,运行一次添加布局约束的代码后就将其设置为YES),以确保不重复添加相同的布局约束。另外,更新已有布局约束的代码(好比调整布局约束的constant属性),也应该将它们放置在updateConstraints 中,可是要在didSetupConstraints条件语句的外面,这样才能够确保每次调用的时候都会被执行。

 

2. Cell使用具备惟一性的重用标示符

 

在cell里面,为每一组特定的约束条件,使用一个特定的cell重用标示符。换句话说,若是cell有多种不一样的布局,每一种布局应当有其对应的重用标示符。(当cell有多种不一样数量的子视图的时候,或者子视图以一种独特的方式布局的时候,这些状况下你就须要使用一个新的重用标示符。)

 

例如,要在一个cell中显示一条email消息,可能会有4种独特的布局:第一种,只有主题的消息;第二种,带主题和正文的消息;第三种,带主题和图片附件的消息;第四种,带主题、正文和图片附件的消息。每一种布局都须要彻底不一样的布局约束才能实现。所以,一旦cell被初始化而且布局约束被加到其中任意一种类型的cell上后,cell应当获得一个惟一的重用标示符来指定该cell类型。这就意味着,当你dequeue重用一个cell的时候,该类型cell的布局约束已经添加好了,能够直接使用。

 

注意,因为固有内容尺寸的不一样,具备相同布局约束的cell仍然可能具备不一样的高度!不要混淆了布局(不一样的约束)和由不一样内容尺寸而计算出(经过相同的布局约束来计算)的不一样视图frame这两个概念,它们根本是彻底不一样的两个东西。

 

    ● 不要将拥有不一样布局约束条件的cell丢到同一个重用池当中(也就是使用相同的重用标示符),而后又在每次dequeue事后将旧的约束移除,又从头开始从新添加约束。自动布局引擎内部并无被设计来能够处理大规模的约束更改,你会看到大量的性能问题。

 

iOS8 - Self-Sizing Cells

 

3. 启用估算行高

 

在iOS8上,苹果将许多在iOS8以前比较难实现的东西都内置实现了。为了让cell实现self-sizing的机制,必须先将tableView的rowHeight属性设置为常量UITableViewAutomaticDimension。而后,只需将tableView的estimatedRowHeight属性设置为非零值便可开启行高估算功能,例如:

 

1
2
  self.tableView.rowHeight = UITableViewAutomaticDimension;
  self.tableView.estimatedRowHeight = 44.0;  // 设置为一个接近“平均”行高的值

   

 

这样作就为tableView上尚未显示在屏幕上的cell提供了一个临时的估算的行高。而后,当cell即将滚入屏幕范围内的时候,会计算出实际的高度。为了肯定每一行的实际高度,tableView会自动让每一个cell基于其contentView的已知固定宽度(tableView的宽度,减去其余额外的,像section index或accessoryView这些宽度)和被加到contentView及其子视图上的自动布局约束规则来计算contentView的高度。一旦真正的行高被计算出来后,旧的估算的行高会被更新为这个真实的行高(而且其余任何须要对tableView的contentSize或contentOffset的更改都自动替你完成了)。

 

通常来讲,行高估算值不须要太精确——它只是被用来修正tableView中滚动条的大小的,当你在屏幕上滑动cell的时候,即使估算值不许确,tableView仍是能很好地调节滚动条。将tableView的estimatedRowHeight属性设置成(在viewDidLoad或相似的方法中)一个接近于“平均”行高的常量值便可。只有行高变化很极端的时候(好比相差一个数量级),才会在滚动时产生滚动条“跳跃”的现象。这个时候,你才应当实现tableView:estimatedHeightForRowAtIndexPath:方法,为每一行返回一个更精确的估算值。

 

iOS7支持(须要本身实现cell尺寸自适应功能)

 

3. 完成一个完整的布局过程 & 计算cell的高度

 

首先,为每个cell都初始化一个离屏(offscreen)实例,为每一个重用标示符实例化一个与之对应的cell实例,这些cell彻底用于高度计算。(离屏表示cell的引用被存储在view controller的一个属性或实例变量之中,而且这个cell绝对不会被用做tableView:cellForRowAtIndexPath:方法的返回值以实际呈如今屏幕上。)接着,这个cell的内容(例如,文本、图片等等)还必须和会被显示在table view中的内容彻底一致。

 

而后,强制cell当即更新子视图的布局,再用cell的contentView调用systemLayoutSizeFittingSize:方法计算出cell所需的高度是多少。使用UILayoutFittingCompressedSize参数能够获得适合cell中全部内容所需的最小尺寸。而后其高度就能够做为tableView:heightForRowAtIndexPath:方法的返回值。

 

4. 使用估算的行高

 

若是你的table view超过了几十行,你会发现自动布局约束的解决方式在第一次加载table view的时候会迅速地卡住主线程。由于,在第一次加载过程当中,每一行都会调用tableView:heightForRowAtIndexPath:方法(为了计算滚动条的尺寸)。

 

iOS7中,你能够(也绝对应该)使用table view的estimatedRowHeight属性。这样会为还不在屏幕范围内的cell提供一个临时估算的行高值。而后,当这些cell即将要滚入屏幕范围内的时候,真实的行高值会被计算出来(经过tableView:heightForRowAtIndexPath:方法),估算的行高就会被替换掉。

 

通常来讲,行高估算值不须要太精确——它只是被用来修正tableView中滚动条的大小的,当你在屏幕上滑动cell的时候,即使估算值不许确,tableView仍是能很好地调节滚动条。将tableView的estimatedRowHeight属性设置成(在viewDidLoad或相似的方法中)一个接近于“平均”行高的常量值便可。只有行高变化很极端的时候(好比相差一个数量级),才会在滚动时产生滚动条“跳跃”的现象。这个时候,你才应当实现tableView:estimatedHeightForRowAtIndexPath:方法,为每一行返回一个更精确的估算值。

 

5. 缓存行高(若是须要)

 

若是上面提到的你都作了,可是tableView:heightForRowAtIndexPath:的性能仍然慢的不可接受。很是不幸,你须要给行高作一些缓存(这是苹果的工程师们给出的改进建议)。大致的思路是,第一次计算时让自动布局引擎解析约束条件,而后将计算出的行高缓存起来,之后全部对该cell的高度的请求都返回缓存值。固然,关键还要确保任何会致使cell高度变化的状况发生时你都清除了缓存的行高——这一般发生在cell的内容变化时或其余重大事件发生时(好比用户调节了动态类型文本大小(Dynamic Type text size)的滑动条)。

 

iOS7示例代码(包含详细的注释)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{    
     // 判断indexPath对应cell的重用标示符,    
     // 取决于特定的布局需求(可能只有一个,也或者有多个)    
     NSString *reuseIdentifier = ...;    
     
     // 取出重用标示符对应的cell。    
     // 注意,若是重用池(reuse pool)里面没有可用的cell,这个方法会初始化并返回一个全新的cell,    
     // 所以无论怎样,此行代码事后,你会能够获得一个布局约束已经彻底准备好,能够直接使用的cell。    
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];    
     
     // 用indexPath对应的数据内容来配置cell,例如:    
     // cell.textLabel.text = someTextForThisCell;    
     // ...    
     
     // 确保cell的布局约束被设置好了,由于它可能刚刚才被建立好。    
     // 使用下面两行代码,前提是假设你已经在cell的updateConstraints方法中设置好了约束:
     [cell setNeedsUpdateConstraints];    
     [cell updateConstraintsIfNeeded];    
     
     // 若是你使用的是多行的UILabel,不要忘了,preferredMaxLayoutWidth须要设置正确。 
     // 若是你没有在cell的-[layoutSubviews]方法中设置,就在这里设置。    
     // 例如:    
     // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);    
     return  cell;
}
 
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{    
     // 判断indexPath对应cell的重用标示符,    
     NSString *reuseIdentifier = ...;    
     
     // 从cell字典中取出重用标示符对应的cell。若是没有,就建立一个新的而后存储在字典里面。
     // 警告:不要调用table view的dequeueReusableCellWithIdentifier:方法,由于这会致使cell被建立了可是又不曾被tableView:cellForRowAtIndexPath:方法返回,会形成内存泄露!    
     UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];    
     if  (!cell) {        
         cell = [[YourTableViewCellClass alloc] init];        
         [self.offscreenCells setObject:cell forKey:reuseIdentifier];    
     }    
     
     // 用indexPath对应的数据内容来配置cell,例如:    
     // cell.textLabel.text = someTextForThisCell;    
     // ...    
     
     // 确保cell的布局约束被设置好了,由于它可能刚刚才被建立好。    
     // 使用下面两行代码,前提是假设你已经在cell的updateConstraints方法中设置好了约束:
     [cell setNeedsUpdateConstraints];    
     [cell updateConstraintsIfNeeded];    
     
     // 将cell的宽度设置为和tableView的宽度同样宽。     
     // 这点很重要。    
     // 若是cell的高度取决于table view的宽度(例如,多行的UILabel经过单词换行等方式),
     // 那么这使得对于不一样宽度的table view,咱们均可以基于其宽度而获得cell的正确高度。  
     // 可是,咱们不须要在-[tableView:cellForRowAtIndexPath]方法中作相同的处理,    
     // 由于,cell被用到table view中时,这是自动完成的。    
     // 也要注意,一些状况下,cell的最终宽度可能不等于table view的宽度。    
     // 例如当table view的右边显示了section index的时候,必需要减去这个宽度。    
     cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));    
     
     // 触发cell的布局过程,会基于布局约束计算全部视图的frame。    
     // (注意,你必需要在cell的-[layoutSubviews]方法中给多行的UILabel设置好preferredMaxLayoutWidth值;    
     // 或者在下面2行代码前手动设置!)    
     [cell setNeedsLayout];    
     [cell layoutIfNeeded];    
     
     // 获得cell的contentView须要的真实高度    
     CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;    
     
     // 要为cell的分割线加上额外的1pt高度。由于分隔线是被加在cell底边和contentView底边之间的。    
     height += 1.0f;    
     
     return  height;
}
 
// 注意:除非行高极端变化而且你已经明显的觉察到了滚动时滚动条的“跳跃”现象,你才须要实现此方法;不然,直接用tableView的estimatedRowHeight属性便可。
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath{    
     // 以必需的最小计算量,返回一个实际高度数量级以内的估算行高。    
     // 例如:    
     //     
     if  ([self isTallCellAtIndexPath:indexPath]) {        
         return  350.0f;    
     else  {        
         return  40.0f;    
     }
}

 

示例项目

 

    ● iOS8的示例代码 - iOS8以上才支持

    ● iOS7的示例代码 - iOS7+

 

来源:Coding With Objective-C

原文地址:http://codingobjc.com/blog/2014/10/15/shi-yong-autolayoutshi-xian-uitableviewde-celldong-tai-bu-ju-he-ke-bian-xing-gao/

相关文章
相关标签/搜索