TL;DR:(Too long,Don’t read,太長不看)如果你不想閱讀內容,你可以直接跳轉到這個github上簡單的工程:
iOS 8工程 - 需要iOS8以上 iOS 7工程 - 需要iOS7+以上最開始的兩步與你所開發的iOS的版本號無關
在你的UITableViewCell
的子類中,通過約束你的cell的subView從而使得他們有自己的邊框,從而撐起這個cell的contentView(最主要的是上下邊的約束)。
注意:不要設置cell的subView與cell本身的約束,只能和cell的contentView
約束。
讓我們使用內容真正的大小來設置tableViewCell的內容,並通過設置內容在垂直方向上的壓縮阻力(content compression resistance)和內容收縮約束(content hugging constraints)來重新加載他的尺寸。
記住,這個方法(指設置壓縮阻力和內容收縮約束)是為了能夠cell的subView能夠在垂直方向上能夠與cell相連接,從而當內容“產生壓力”的時候他們能夠伸展內容的View從而適應他們。就如下面的這個例子中展出的部分約束(不是全部!):
你能夠想象,就如例子中一樣,當內容在多行的label中不斷增加的時候,label將會更好的變寬從而使得使得內容變得更加合適。(當然,為了能夠使Cell更好的顯示出來,你需要設置正確的約束)
設置正確的約束是使用AutoLayout動態獲得cell高度最困難同時也是最重要的一步。沒有這一步,其余的都只是無用功。所以在這一步上面請花費一定的時間。我建議你通過代碼來實現約束,這樣可以更好的明白你在哪裡添加了這些約束,同時對於調試來說,這可以更好的找到錯誤。通過代碼添加約束比通過interface Builder更加方便、有效,特別是通過某些別人已經設置好的API。這裡是我所設計、維護、使用的一個第三方庫(譯者表示:我更喜歡用Masonry和SnapKit)
如果你在代碼中添加了約束,你將至少使用一次updateConstraints
方法來設置你的UITableViewCell的subClass。注意:因為updateConstraints
在你的代碼中將不止一次的被調用,所以不要反復添加一樣的約束,通過檢查didSetupConstraints
函數返回的布爾值(boolean)來確定updateConstraints
中包含的內容是否只包含了一次。(你可以在你運行了一次你的約束後,將他設置為YES)。另一方面,如果你有代碼來更新已經存在的約束(比如說調整某些約束的常量屬性),請將這些更新的代碼放在updateConstraints
中,但是在檢查didSetupConstraints
以外。這樣你可以在每次運行的時候都調用它。
對於每一個唯一的cell中的約束,使用一個唯一的標示符來設置有這些約束的Cell。另一方面來說,如果你的cell有不止一個的獨一無二的layout,每一個唯一的layout都應該有一個他自己的標示符。(這裡有一個建議,一般你只有當你的Cell的subViews發生改變或者他們按照不同位置發生排列的時候,才會用新的標示符。)
比如說,如果你在每個cell中展示每一條email的內容的時候,你可能需要4種展示方式:有主題的郵件,有主題和文章主體的郵件,有主題和照片附件的郵件,有主題、文章主體還有照片附件的郵件。每種展示方式都有自己的不同約束來實現它,所以當你的cell創建並且添加約束的時候,每種不同的cell需要給予特定的標示符。這意味著當你將cell放入緩存池的時候,他們當中已經加入了各自的約束,並且已經設定好了他們的類型。
注意:由於內容的大小不同,所以cell有著一樣的約束可能仍舊有不同的高度。不要因為內容有著不同的大小,卻需要設置不同的layout(不同的約束)和計算框架方式不同(通過相同的約束進行解決)而感到迷惑。
不要將有著完全不同約束的cell添加到同一個緩存池(reuse pool)(比如說使用相同的標示符),同時企圖刪掉老的約束來重新開始來設置每一個緩寸池中的cell。因為Autolayout的內部不是用來進行大規模的約束變化,如果你這麼做的話,你將會看到明顯的性能問題。對於iOS 8 ,蘋果公司已經在你使用iOS 8之前內化了大量的工作,為了能夠使用這種行高的自動估計,你必須首先設置他的rowHeight
這一屬性,一般我們把它設置為UITableViewAutomaticDimension
。然後你需要設置tableView的estimatedRowHeight
來開啟行高估計,同時這個屬性不能為0,下面是一個例子:
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0; // 設置你cell的均值
這是為了在你的cell還沒有顯示到屏幕上之前,先給予你的tableView中的每一個cell一個臨時的估計高度(占位符)。為了得到每一行的准確高度,tableView自動詢問每一個cell,他們自己內部基於cell自身固定寬度的內容視圖,從而確定自身的contentView
的高度(這將基於tableView自己的寬度減去外部的索引或者他的附屬視圖)和你添加到cell的View和SubView的autolayout的約束。當cell的實際高度被計算出來之後,老的估算高度將會被更新為新的高度,同時它將調整為你所需要的tableView的contentSize
和contentOffset
總的來說,你所設置的估算高度不會被真的調用,這只是用來設置tableView中正確大小的滾動指示符,同時在你的cell被顯示到屏幕上的時候,這種方法將很好的調整那些不正確的估算值。你應該設置estimatedRowHeight
這個tableView中的屬性(一般在viewDidLoad
或者和他類似的方法中)為行的均高。只有當你的行高產生極端變化(例如相差了一個數量級),你就會發現你的滾動指示符發生“跳動”的時候,你才需要費心去實現tableView:estimatedHeightForRowAtIndexPath
方法,從而去做最小的計算,從而返回更加精確的cell的高度值。
首先實例化一個不會出現在屏幕上的tableViewCell的例子,這個cell將會被設置一個所有cell都會使用的重用表示符,這個cell將被進行嚴格的高度計算(不會出現在屏幕上意味著他只是一個屬性,同時將不會在tableView:cellForRowAtIndexPath:
調用,這意味他不會被渲染到屏幕上)然後,這個cell將會被添加將來顯示在tableView中的具體的內容,(比如說文本,圖片等等)。
然後迫使這個cell立即調整他的subView的布局,同時使用UItableViewCell的content的systemLayoutSizeFittingSize:
方法來獲得這個cell所需要的高度。使用UILayoutFittingCompressedSize
來獲得用來撐開cell的內容的最小的大小。高度必須靠代理中的tableView:heightForRowAtIndexPath:
方法來返回。
如果你的tableView有多行的內容,你將會autolayout的約束將會使得主線程在第一次加載tableView的時候產生問題,那是因為tableView:heightForRowAtIndexPath:
方法將會在每一行第一次被調用的時候。(為了計算滾動指示符正確的大小)
對於iOS 7,你能夠(或者說你絕對應該)在tableView中使用estimatedRowHeight
屬性,在沒有被顯示到屏幕上前,這將會給予tableView一個預估的行高(占位符)。然後這個cell在放到屏幕上的時候,每一行的高度將會被計算(通過調用tableView:heightForRowAtIndexPath:
方法),同時這個真實高度將替代原有的預估的行高(占位符)。
//這一段和iOS 8的第三段一模一樣
總的來說,你所設置的估算高度不會被真的調用,這只是用來設置tableView中正確大小的滾動指示符,同時在你的cell被顯示到屏幕上的時候,這種方法將很好的調整那些不正確的估算值。你應該設置estimatedRowHeight
這個tableView中的屬性(一般在viewDidLoad
或者和他類似的方法中)為行的均高。只有當你的行高產生極端變化(例如相差了一個數量級),你就會發現你的滾動指示符發生“跳動”的時候,你才需要費心去實現tableView:estimatedHeightForRowAtIndexPath
方法,從而去做最小的計算,從而返回更加精確的cell的高度值。
如果你已經做了以上所有步驟,但是仍舊不能忍受設備因為調用tableView:heightForRowAtIndexPath:
方法去計算約束時候顯示tableView的緩慢,你可能很不幸的需要緩存cell的高度(這個建議來自於蘋果的工程師)。總的想法是通過autolayout來計算第一次約束,然後通過緩存來計算將來需要使用的所有cell的高度。在使用這個方法的時候,你需要清楚的知道緩存中的行高和什麼時候緩存中的行高將會發生改變。這將用於當cell的內容發生改變或者一些其他重要的事情發生的時候(比如說用戶通過滑塊來動態的調整文本的大小時候)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//設置標示符
static NSString *reuseIdentifier = ...;
//獲取緩存池中的cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
// 設置cell的內容
// cell.textLabel.text = someTextForThisCell;
// ...
// 確定約束已經被添加
// 或者對約束進行調整
// 如果你確定約束已經被設定好,記得更新約束
// 更新的方法: [cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
// 如果你使用多行的Label,不要忘記正確的使用preferredMaxLayoutWidth
// 記得在子類中也進行調用
// -[layoutSubviews] 方法. For example:
// cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Determine which reuse identifier should be used for the cell at this
// index path.
NSString *reuseIdentifier = ...;
// Use a dictionary of offscreen cells to get a cell for the reuse
// identifier, creating a cell and storing it in the dictionary if one
// hasn't already been added for the reuse identifier. WARNING: Don't
// call the table view's dequeueReusableCellWithIdentifier: method here
// because this will result in a memory leak as the cell is created but
// never returned from the tableView:cellForRowAtIndexPath: method!
UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
if (!cell) {
cell = [[YourTableViewCellClass alloc] init];
[self.offscreenCells setObject:cell forKey:reuseIdentifier];
}
// Configure the cell with content for the given indexPath, for example:
// cell.textLabel.text = someTextForThisCell;
// ...
// Make sure the constraints have been set up for this cell, since it
// may have just been created from scratch. Use the following lines,
// assuming you are setting up constraints from within the cell's
// updateConstraints method:
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
// Set the width of the cell to match the width of the table view. This
// is important so that we'll get the correct cell height for different
// table view widths if the cell's height depends on its width (due to
// multi-line UILabels word wrapping, etc). We don't need to do this
// above in -[tableView:cellForRowAtIndexPath] because it happens
// automatically when the cell is used in the table view. Also note,
// the final width of the cell may not be the width of the table view in
// some cases, for example when a section index is displayed along
// the right side of the table view. You must account for the reduced
// cell width.
cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));
// Do the layout pass on the cell, which will calculate the frames for
// all the views based on the constraints. (Note that you must set the
// preferredMaxLayoutWidth on multi-line UILabels inside the
// -[layoutSubviews] method of the UITableViewCell subclass, or do it
// manually at this point before the below 2 lines!)
[cell setNeedsLayout];
[cell layoutIfNeeded];
// Get the actual height required for the cell's contentView
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
// Add an extra point to the height to account for the cell separator,
// which is added between the bottom of the cell's contentView and the
// bottom of the table view cell.
height += 1.0f;
return height;
}
// NOTE: Set the table view's estimatedRowHeight property instead of
// implementing the below method, UNLESS you have extreme variability in
// your row heights and you notice the scroll indicator "jumping"
// as you scroll.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Do the minimal calculations required to be able to return an
// estimated row height that's within an order of magnitude of the
// actual height. For example:
if ([self isTallCellAtIndexPath:indexPath]) {
return 350.0f;
} else {
return 40.0f;
}
}
這幾個工程完全解決了因為UILabel的情況下tableView需要中cell的高度需要動態調整的問題。
你可以隨時提出任何問題或者你所遇到的問題(你可以在Github上提交你的評論),我將盡力幫助你!
如果你使用Xamarin,可以檢出KentBoogaart的一個簡單的工程