關(guān)于TableViewCell的收縮展開與動(dòng)態(tài)高度的方法網(wǎng)上有很多,但本文主要運(yùn)用AutoLayout和更改Constraint priority的方式來(lái)實(shí)現(xiàn),另外包括了StackView動(dòng)態(tài)插入在TableViewCell里的運(yùn)用。
Cell收縮與展開
這是在實(shí)際項(xiàng)目中遇到一個(gè)需求。展示一個(gè)表格,每一行為一個(gè)項(xiàng)目的匯總行,點(diǎn)擊表格每一行時(shí)展開一系列細(xì)節(jié)小行,在此點(diǎn)擊收起該行,僅展示匯總行。如下圖所示。



我們用TableView去實(shí)現(xiàn)該需求。在點(diǎn)擊cell時(shí)展開該cell, 再次點(diǎn)擊時(shí)收起。不同的cell展開高度依賴于該cell所展示的內(nèi)容。每一個(gè)行用StackView去實(shí)現(xiàn)。這里將用到嵌套的StackView和StackView動(dòng)態(tài)插入。另外我用了設(shè)置約束優(yōu)先級(jí)去解決Cell內(nèi)容顯示錯(cuò)位的問(wèn)題。
我們先實(shí)現(xiàn)Cell的展開與收縮。
- 定義一個(gè)Dictionary來(lái)存儲(chǔ)每個(gè)Cell的展開或收縮狀態(tài)。key為IndexPath。
@property (nonatomic, copy) NSMutableDictionary *expandedFlagOfRowDict;
- 每次選擇cell后toggle展開狀態(tài)。實(shí)現(xiàn)TableView delegate方法。
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
BMMerchantCell *cell = [tableView cellForRowAtIndexPath:indexPath];
//toggle the expanded flag*
self.expandedFlagOfRowDict[indexPath] = [NSNumber numberWithBool:![self.expandedFlagOfRowDict[indexPath] boolValue]];;
[tableView beginUpdates];
[tableView endUpdates];
}
- 調(diào)用-beginUpdates 后 tableView delegate會(huì)回調(diào) heightForRowAtIndexPath方法。
-beginUpdates 和 -endUpdates 必須組合使用。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
*//cell expanded height*
if ([self.expandedFlagOfRowDict[indexPath] boolValue]) {
return [self.heightForRowDict[indexPath] floatValue];
}
*//cell collapsed height*
return 44;
}
- 由此實(shí)現(xiàn)了Cell的展開與收縮。
Cell動(dòng)態(tài)高度
上面代碼我們留意到有 self.heightForRowDict 這玩意兒,每個(gè)row展開的高度是不一樣的。它與self.expandedFlagOfRowDict 類似,我們定義一個(gè)字典來(lái)存儲(chǔ)每一個(gè)Cell的高度。該字典Key為IndexPath,value為高度。
@property (nonatomic, copy) NSMutableDictionary *heightForRowDict;
而每個(gè)Cell的高度由Cell對(duì)象本身確定。我們用IB去定義Cell原型。

Cell收縮狀態(tài)時(shí),僅展示匯總一行(即Sum Stack View)。點(diǎn)擊展開時(shí),顯示細(xì)節(jié)行,細(xì)節(jié)行數(shù)不定。細(xì)節(jié)行嵌套到Detail Stack View這個(gè)大的StackView中。
細(xì)節(jié)行:(稱之為DetailItemStackView)

可見(jiàn),在SumStackView高度已定,DetailStackView設(shè)定top與bottom約束,DetailItemStackView高度已定情況下,Cell的高度就由細(xì)節(jié)行的插入的多少?zèng)Q定了。
Note:與Label、Button等控件一樣,Stack View 也有自己的Intrinsic Size,在未設(shè)定約束時(shí)亦能確定自己的大小。其依賴于內(nèi)部subView的大小和space。在subView約束未定時(shí)其本身大小同依據(jù)于本身Intrinsic Size確定。這是一個(gè)嵌套遞歸的過(guò)程。
接下來(lái)就計(jì)算TableView Cell的高度。
- 在收縮狀態(tài)時(shí),其高度等于SumStackView的高度。
- 在展開狀態(tài)時(shí),其高度等于SumStackView和DetailStackView高度加兩者距離加DetailStackView與Cell.Bottom距離。
考慮到解耦與性能優(yōu)化問(wèn)題,Cell的高度與內(nèi)容將不在Controller層計(jì)算。-tableView:heightForRowAtIndexPath: 中僅負(fù)責(zé)讀取高度值。
自定義Cell對(duì)象:
//MyCell.h
@interface MyCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UIStackView *detailStackView;
@property (weak, nonatomic) IBOutlet UIStackView *sumStackView;
@property (weak, nonatomic) IBOutlet UILabel *cityLabel;
@property (weak, nonatomic) IBOutlet UILabel *sumLabel;
@property (weak, nonatomic) IBOutlet UILabel *vLabel;
@property (assign, nonatomic) CGFloat rowHeight;
@end
先把cell的高度都設(shè)為定值,運(yùn)行看看收縮與展開效果,假定rowHeight=100;

運(yùn)行時(shí)可見(jiàn)編譯器報(bào)了一大堆約束沖突警告,界面Cell內(nèi)容顯示位置偏離,而IB界面設(shè)計(jì)并無(wú)約束警告。經(jīng)過(guò)本人探究后發(fā)現(xiàn),實(shí)際上是與運(yùn)行時(shí)的 -tableView:heightForRowAtIndexPath: 代理方法返回的高度相沖突。該高度H !== sumStackView.height + space + detailStackView.height + space; 故而沖突。而將其中一個(gè)space的約束優(yōu)先級(jí)調(diào)低至750,沖突解決。界面顯示正常。

Note:由此得知,-tableView:heightForRowAtIndexPath: 返回高度值的約束優(yōu)先級(jí)實(shí)際上是介于750和1000之間的(750<p<1000)。
Cell與StackView
至此Cell展開后顯示的是一個(gè)空的StackView,現(xiàn)在我們往Cell類里添加兩個(gè)方法內(nèi)容。
//MyCell.m
//Cell collapsed content
- (void)loadSumLineByDict:(NSDictionary *)dataDict {
self.cityLabel.text = dataDict[@"cityDes"];
self.sumLabel.text = [(NSNumber *)dataDict[@"sum"] stringValue];
}
//Cell expanded content
- (void)loadDetailLinesByDict:(NSDictionary *)dataDict {
NSArray *detailList = (NSArray *)dataDict[@"detail"];
//Insert sub stackViews. Performance consuming process.
[detailList enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSString *mccTag = [(NSDictionary *)obj objectForKey:@"mccTag"];
NSNumber *num = [(NSDictionary *)obj objectForKey:@"num"];
DetailItemStackView *itemSV = [[[NSBundle mainBundle] loadNibNamed:@"DetailItemStackView" owner:nil options:nil] lastObject];
itemSV.mccTagLabel.text = mccTag;
itemSV.numLabel.text = [NSString stringWithFormat:@"%@", num];
[self.detailStackView addArrangedSubview:itemSV];
}];
[self.detailStackView layoutIfNeeded];
[self layoutIfNeeded];
//Calculate the cell's height based on AutoLayout
CGSize size = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
self.rowHeight = size.height;
}
- 參數(shù)
dataDict為數(shù)據(jù)模型; -
-loadSumLineByDict:加載匯總行,即Cell收縮狀態(tài)時(shí)的展示行; -
-loadDetailLinesByDict:加載細(xì)節(jié)行,即Cell展開時(shí)展示的內(nèi)容;該方法設(shè)計(jì)遍歷以及創(chuàng)建StackView對(duì)象,為耗性能的步驟。這個(gè)我們讓它有生之年只Run一次就夠了。 -
-layoutIfNeeded在stackView們都插入完畢后,更新布局。實(shí)現(xiàn)Runtime時(shí)的Autolayout; -
-systemLayoutSizeFittingSize:方法返回滿足約束的大小,用以獲取Cell增加內(nèi)容后的高度值。
由此獲取cell的datasource方法如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *cellID = @"MyCellID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
//load the collpased content
[cell loadSumLineByDict:self.dataList[indexPath.row]];
return cell;
}
-cellForRow:方法為高頻調(diào)用,因此此處盡量進(jìn)行輕量級(jí)的數(shù)據(jù)加載。此處僅加載Cell收縮狀態(tài)的內(nèi)容,而展開的內(nèi)容則在select時(shí)加載。didSelectRow方法就變成了這樣:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
MyCell *cell = [tableView cellForRowAtIndexPath:indexPath];
*//Check if the row had been loaded. If not,*
if (![self.loadedFlagOfRowDict[indexPath] boolValue]) {
*//Load detail lines and calculate the height for row*
[cell loadDetailLinesByDict:self.dataList[indexPath.row]];
*//store the height*
self.heightForRowDict[indexPath] = @(cell.rowHeight + 1);
*//set the loaded flag to true and it will end up true ever since*
self.loadedFlagOfRowDict[indexPath] = @YES;
}
*//toggle the expanded flag*
self.expandedFlagOfRowDict[indexPath] = [NSNumber numberWithBool:![self.expandedFlagOfRowDict[indexPath] boolValue]];;
[tableView beginUpdates];
[tableView endUpdates];
}
- 此處加入了一個(gè)字典屬性來(lái)存儲(chǔ)是否加載的標(biāo)志位
@property (nonatomic, copy) NSMutableDictionary *loadedFlagOfRowDict;
依此判斷使得該Cell展開內(nèi)容有生之年僅加載一次。
- 加載完內(nèi)容后計(jì)算高度,并存儲(chǔ)至
_heightForRowDict中,在-tableView:heightForRowAtIndexPath:中讀取并展示實(shí)際高度。由此實(shí)現(xiàn)了TableViewCell 的收縮展開與動(dòng)態(tài)高度。
小結(jié):
-
-beginUpdates&-endUpdates實(shí)現(xiàn)select Cell時(shí)收縮與展開。 -
-tableView:heightForRowAtIndexPath:的約束優(yōu)先級(jí)。 -
-tableView:cellForRowAtIndexPath:與-tableView:heightForRowAtIndexPath:為高頻調(diào)用方法,高耗能的操作盡量轉(zhuǎn)移到其它方法中,本例子中如Cell高度計(jì)算,內(nèi)容加載等。本例中cellForRow方法僅加載cell最基本的內(nèi)容。
下一篇講延續(xù)本篇內(nèi)容,探討TableView的性能優(yōu)化方面的問(wèn)題。