首先需要在Storyboard中创建好TableViewController,使用动态Cell,在Prototype Cells中设计好Cell界面。
接着,定义好Autolayout,注意Autolayout一定要在上下都绑定控件的位置,不要只从上到下定义,只有正确定义Autolayout,后面我们用到的systemLayoutSizeFittingSize方法才会返回正确的结果。
如下图:
Xcode会提示Autolayout的各种Ambiguity,提示修改控件的Compression resistance,如下图:
这里让Xcode智能修正就可以了,具体哪个控件的Compression resistance无所谓,因为我们最终的目的是让UITabelViewCell的高度去适合所有控件的大小。
然后,因为是在Xcode 5 iOS 7模式下设计的Storyboard,所以在iOS 7下运行肯定是没问题的:
接着在iOS 6上运行:
什么情况?
出现这个问题的原因是:iOS 7和iOS 6中的许多控件默认高度都是不一样的,在其他普通UIView下,有了Autolayout,控件当然会正确显示。但是UITableViewCell的高度是通过UITableView的heightForRowAtIndexPath方法来返回的。默认情况下,它是保持不变的。所以当Cell内控件的高度发生变化后,如果Cell高度没有因此而作出调整,肯定会出问题的。
来慢慢看问题,首先,如何测量使用Autolayout的UIView的尺寸?可以使用UIView的systemLayoutSizeFittingSize方法,对于UITableViewCell,那就是测量其contentView的大小。那么,本例中需要返回Autolayout的Cell的高度,则可以写一个辅助方法,这样:
- (CGFloat)getCellHeight:(UITableViewCell*)cell { [cell layoutIfNeeded]; [cell updateConstraintsIfNeeded]; CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; return height; }
接着,另一个问题来了,是关于UITableView的Cell重用机制。我们必须在UITableView的heightForRowAtIndexPath方法中返回根据Cell内容计算出来的动态高度,对于使用Autolayout的Cell,必须创建这个Cell才可以获取他的动态高度。而经测试发现,如果在heightForRowAtIndexPath调用Cell重用方法,也就是dequeueReusableCellWithIdentifier方法,Cell还是会被重复创建,也就是说Cell的重用机制在heightForRowAtIndexPath中是无效的。解决方案是,在heightForRowAtIndexPath只创建一个Cell,这个Cell专门用作测量所有Cell的高度,然后在cellForRowAtIndexPath继续使用Cell重用逻辑就可以。
我们来再看本例代码,首先在TableViewController中加入必要的字段,声明数据源,加入测试数据等,这些都是很简单的内容,不需要多讲:
//测试数据源 NSMutableArray *_dataSource;
在viewDidLoad中初始化相关数据:
//viewDidLoad 初始化 _dataSource = [NSMutableArray arrayWithArray:@[@"Mgen", @"Tony", @"Jerry", @"一二三"]];
然后把Cell加载数据的逻辑写在一个方法里,这个方法是被heightForRowAtIndexPath和cellForRowAtIndexPath方法所共用的,因为不管是测量Cell的高度还是展示Cell,我们都需要Cell加载相应的数据:
- (void)loadCellContent:(MyCell*)cell indexPath:(NSIndexPath*)indexPath { //这里把数据设置给Cell cell.titleLabel.text = [_dataSource objectAtIndex:indexPath.row]; }
接下来是关键的heightForRowAtIndexPath方法,这里的逻辑上面已经讲过,不需要用Cell重用机制,我们只创建一个Cell,利用这个Cell,不停地加载内容,然后返回高度就可以了,这两个步骤的辅助方法上面也都有,我们直接用,如下代码:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { //log [[GlobalCounter getInstance] add:@"get height"]; //只创建一个cell用作测量高度 static MyCell *cell = nil; if (!cell) cell = [self.tableView dequeueReusableCellWithIdentifier:@"MyCell"]; [self loadCellContent:cell indexPath:indexPath]; return [self getCellHeight:cell]; }
然后是cellForRowAtIndexPath方法,这里调用dequeueReusableCellWithIdentifier进行Cell重用就然后加载Cell内容就可以了:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { //log [[GlobalCounter getInstance] add:@"get cell"]; static NSString *CellIdentifier = @"MyCell"; //注意在heightForRowAtIndexPath:indexPath无法使用dequeueReusableCellWithIdentifier:forIndexPath: MyCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier]; //用dequeueReusableCellWithIdentifier:就得判断Cell为nil的情况 //如果在Storyboard中Prototype Cells中设置了具体Table View Cell的Identifier也是"MyCell"(也就是重用ID),那这里不会有返回nil的情况 if (!cell) { cell = [[MyCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } [self loadCellContent:cell indexPath:indexPath]; return cell; }
读者可以看到,上面代码里,我在heightForRowAtIndexPath和cellForRowAtIndexPath方法里都打了Log,还有一处需要打Log的地方,就是Cell本身的创建上,注意Storyboard中UITableViewCell的创建是在initWithCoder方法中的,而不是initWithStyle:reuseIdentifier方法里的。
- (instancetype)initWithCoder:(NSCoder *)coder { [[GlobalCounter getInstance] add:@"create cell"]; return [super initWithCoder:coder]; }
OK,现在再次运行程序,即便是你Storyboard中把Cell高度手动调整成这样:
在iOS 6下会显示出正确的结果:
iOS 7下也下一样:
最后Log的信息,在1000个数据源的情况下,运行程序后没有进行任何滚动操作:
create cell" = 7; //创建Cell 7次
"get cell" = 6; //调用cellForRowAtIndexPath 6次
"get height" = 2006; //调用heightForRowAtIndexPath 2006次