使用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/

相關文章
相關標籤/搜索