本文翻譯自:stackoverflow
有人在stackoverflow上問了一個問題:
如何在UITableViewCell中使用Autolayout來實現(xiàn)Cell的內(nèi)容和子視圖自動計算行高型奥,并且保持平滑的滾動?
這個問題獲得了接近1000的支持和1100+的收藏碉京,答案更是超過了1800+的支持厢汹,很詳細的說明了如何在iOS7和iOS8上實現(xiàn)UITableView的動態(tài)行高計算。答案對實現(xiàn)UICollectionView的動態(tài)行高也具有參考意義谐宙,所以在這里將這個答案翻譯了一下烫葬,希望對大家有所幫助。以下是答案的全文翻譯:
全文略長凡蜻,不喜歡閱讀可以直接看示例代碼:
iOS8的示例代碼- iOS8以上才支持
iOS7的示例代碼- iOS7+
概念描述
不管在哪個iOS版本上進行開發(fā)搭综,前兩步是必須的:
1、設(shè)置好布局約束
在UITableViewCell子類中划栓,添加約束兑巾,使子視圖的邊緣與contentView的邊緣固定(pin)(最重要的是要有頂部和底部的邊距約束)。注意:不能將子視圖的邊緣設(shè)置成與cell的邊緣固定忠荞,只能設(shè)置為與contentView的邊緣固定蒋歌!確保每個子視圖在垂直方向上的內(nèi)容壓縮阻力(compression resistance)和內(nèi)容吸附性約束(content hugging constraints)沒有被你添加的更高優(yōu)先級的約束覆蓋,以使得這些子視圖的固有內(nèi)容尺寸(intrinsic content size)來推動contentView的高度委煤。(嗯堂油?點擊英文或中文。)
記住素标,重點是cell的子視圖與contentView要有垂直方向上的連結(jié)称诗,讓它們能夠?qū)ontentView“施加壓力”,使contentView擴張以適合它們的尺寸头遭。
下面用一個帶有一些子視圖的cell作為示例寓免,展示了一些必要的約束(沒有展示全部的約束):
可以想象癣诱,當更多的文本被添加到“Multi-line body”那個label上面后,它就需要垂直地增高以適應(yīng)文本袜香,這實際上將強迫cell增加高度撕予。(當然,前提是你需要把約束設(shè)置正確r谑住)
設(shè)置正確的約束是使用Autolayout實現(xiàn)動態(tài)行高時最難也最重要的部分实抡。如果你犯了一個錯誤,它可能使后面一切都無法工作——所以欢策,不要著急吆寨,慢慢來!我建議你用代碼來設(shè)置約束踩寇,這樣你就完全知道每個約束被加到了什么地方啄清,出問題的時候也更容易調(diào)試。特別是如果你利用好一些優(yōu)秀的開源庫俺孙,使用代碼設(shè)置約束可以變得和使用Interface Builder設(shè)置約束一樣簡單辣卒,而且更加強大。這里有一個我設(shè)計睛榄、維護和使用的開源庫:https://github.com/smileyborg/PureLayout
如果你用代碼來設(shè)置約束荣茫,應(yīng)該在UITableViewCell子類的updateConstraints方法里面一次性完成。注意场靴,updateConstraints可能不止被調(diào)用一次啡莉,因此要避免重復(fù)添加相同的約束。在updateConstraints中旨剥,可以將添加約束的代碼包在一個if語句中(比如使用一個叫didSetupConstraints的布爾屬性票罐,運行一次添加約束的代碼后就將其置為YES),以確保不重復(fù)添加相同的約束泞边。另外,更新已有約束的代碼(比如調(diào)整約束的constant屬性)疗杉,也應(yīng)該將它們放置在updateConstraints中阵谚,但是要在didSetupConstraints條件語句的外面,這樣才能保證每次調(diào)用的時候都被執(zhí)行烟具。
譯注:上面這段updateConstraints中添加約束的描述梢什,由于文章久遠,已經(jīng)不合時宜朝聋。蘋果官方在WWDC2015 session219中已經(jīng)給出了updateConstraints使用的新建議嗡午。
2. 確立唯一的Cell重用標示符
為cell中每一組獨特的約束,使用一個唯一的cell重用標示符冀痕。也就是說荔睹,如果cell有不止一種布局孔飒,每一種布局都應(yīng)當有其對應(yīng)的重用標示符燥撞。(當cell有多種布局包含不同數(shù)量的子視圖的時候或者子視圖以不同的方式布局的時候,你就需要使用一個新的重用標示符。)
例如宅此,要在一個cell中顯示一條email消息,可能會有4種不同的布局:第一種箕慧,只有主題歉胶;第二種,主題和正文劝篷;第三種哨鸭,主題和圖片附件;第四種娇妓,主題像鸡、正文和圖片附件。每一種布局都需要完全不同的約束才能實現(xiàn)峡蟋。因此坟桅,一旦cell被初始化并且約束被加到其中任意一種類型的cell上之后,cell應(yīng)當?shù)玫揭粋€唯一的重用標示符來指定該cell類型蕊蝗。這樣仅乓,當你dequeue重用cell的時候,該cell類型的約束已經(jīng)添加好了蓬戚,拿來即用夸楣。
注意,由于固有內(nèi)容尺寸的不同子漩,具有相同布局約束的cell仍然可能具有不同的高度豫喧!不要混淆了不同的布局(不同的約束)和由于不同的內(nèi)容尺寸而計算出(通過相同的約束來計算)的不同的視圖frame這兩個概念,它們本質(zhì)上是兩個完全不同的東西幢泼。(譯注:本段翻譯的不好紧显,如果有疑惑,可以看看原文缕棵。)
不要將擁有不同布局約束的cell丟到同一個重用池中(也就是使用相同的重用標示符)孵班,然后又在每次dequeue過后企圖將舊的約束移除后從頭開始重新添加約束。內(nèi)部自動布局引擎并沒有被設(shè)計來可以處理大規(guī)模的約束更改招驴,你會看到大量的性能問題篙程。
iOS8適用 - Self-Sizing Cells
3. 啟用行高估算
在iOS8上,蘋果將許多在之前你比較難實現(xiàn)的東西都內(nèi)置實現(xiàn)了别厘。為了讓cell實現(xiàn)自適應(yīng)(self-sizing)虱饿,必須先將tableView的rowHeight屬性設(shè)置為常量UITableViewAutomaticDimension。然后,只需將tableView的estimatedRowHeight屬性設(shè)置為一個非零值即可開啟行高估算功能氮发,例如:
12
self.tableView.rowHeight=UITableViewAutomaticDimension;self.tableView.estimatedRowHeight=44.0;// 設(shè)置為一個接近于行高“平均值”的數(shù)值
這樣就為tableView提供了一個還沒有被顯示在屏幕上的cell的臨時估算的行高渴肉。當cell即將滾入屏幕范圍內(nèi)的時候,會計算出真實的高度折柠。為了確定每一行的實際高度宾娜,tableView會自動讓每個cell基于其contentView的已知固定寬度(tableView的寬度減去其他額外的,像section index或accessoryView這些寬度)和被添加到contentView及其子視圖上的布局約束來計算contentView的高度扇售。真實的行高被計算出來之后前塔,舊的估算的行高會被更新為這個真實的行高(并且其他任何需要對tableView的contentSize或contentOffset的更改都自動替你完成)。
一般來說承冰,行高的估算值不需要太精確——它只是用來修正tableView中滾動條的尺寸的华弓,當你在屏幕上滑動cell的時候,即使估算值不準確困乒,tableView還是能很好地調(diào)節(jié)滾動條寂屏。將tableView的estimatedRowHeight屬性設(shè)置成(在viewDidLoad或類似的方法中)一個接近于行高“平均值”的常量值即可。僅在行高極端變化的時候(比如相差一個數(shù)量級)娜搂,滾動過程中才會產(chǎn)生滾動條的“跳躍”現(xiàn)象迁霎。這個時候,你才需要考慮實現(xiàn)tableView:estimatedHeightForRowAtIndexPath:方法百宇,為每一行返回一個更精確的估算值考廉。
iOS7支持(自己實現(xiàn)cell尺寸自適應(yīng)功能)
3. 完成一個完整的布局過程 & 獲得行高
首先,實例化一個離屏(offscreen)的cell實例携御,為每個重用標示符實例化一個與之對應(yīng)的cell實例昌粤,這些cell實例嚴格的僅用于高度計算。(離屏表示cell的引用被存儲在view controller的一個屬性或?qū)嵗兞恐凶纳玻⑶疫@個cell絕對不會被用作tableView:cellForRowAtIndexPath:方法的返回值顯示在屏幕上涮坐。)接下來,這個cell的內(nèi)容(例如誓军,文本袱讹、圖片等等)還必須被配置為與顯示在table view中的內(nèi)容完全一樣。
然后昵时,強制cell立即更新子視圖的布局廓译,再在cell的contentView上調(diào)用systemLayoutSizeFittingSize:方法以計算出cell所需的高度。使用UILayoutFittingCompressedSize參數(shù)得到適合cell中所有內(nèi)容所需的最小尺寸债查。然后將其高度作為tableView:heightForRowAtIndexPath:方法的返回值返回給table view。
4. 使用估算的行高
如果你的table view超過幾十行瓜挽,你會發(fā)現(xiàn)在第一次加載table view的時候會卡住主線程盹廷。因為,在第一次加載的過程中久橙,會對每一行調(diào)用tableView:heightForRowAtIndexPath:方法(為了計算滾動條的尺寸)俄占。
iOS7中管怠,你可以(也絕對應(yīng)當)使用table view的estimatedRowHeight屬性。這樣會為還不在屏幕范圍內(nèi)的cell提供一個臨時估算的行高值缸榄。然后渤弛,當這些cell即將要滾入屏幕范圍內(nèi)的時候,真實的行高值會被計算出來(通過tableView:heightForRowAtIndexPath:方法)甚带,估算的行高會被替換掉她肯。
一般來說,行高的估算值不需要太精確——它只是用來修正tableView中滾動條的尺寸的鹰贵,當你在屏幕上滑動cell的時候晴氨,即使估算值不準確,tableView還是能很好地調(diào)節(jié)滾動條碉输。將tableView的estimatedRowHeight屬性設(shè)置成(在viewDidLoad或類似的方法中)一個接近于行高“平均值”的常量值即可籽前。僅在行高極端變化的時候(比如相差一個數(shù)量級),滾動過程中才會產(chǎn)生滾動條的“跳躍”現(xiàn)象敷钾。這個時候枝哄,你才需要考慮實現(xiàn)tableView:estimatedHeightForRowAtIndexPath:方法,為每一行返回一個更精確的估算值阻荒。
5. 緩存行高(如果需要)
如果上面提到的你都做了挠锥,但是tableView:heightForRowAtIndexPath:的性能仍然慢的不可接受。非常不幸财松,這個時候你需要給行高做一些緩存(這是蘋果的工程師們給出的改進建議)瘪贱。大體的思路是,第一次計算時讓自動布局引擎解析布局約束計算行高辆毡,然后將計算出來的行高緩存起來菜秦,之后所有對該cell的高度請求都返回緩存值。當然舶掖,還要保證任何導(dǎo)致cell高度變化的情況發(fā)生時都要清除緩存的行高——這通常發(fā)生在cell的內(nèi)容變化時或其他重大事件發(fā)生的時候(比如用戶調(diào)節(jié)了動態(tài)類型文本大小(Dynamic Type text size)的滑動條)球昨。
iOS7示例代碼(包含詳細的注釋)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
-(UITableViewCell*)tableView:(UITableView*)tableViewcellForRowAtIndexPath:(NSIndexPath*)indexPath{// 判斷indexPath對應(yīng)cell的重用標示符,// 取決于特定的布局需求(可能只有一個眨攘,也或者有多個)NSString*reuseIdentifier=...;// 取出重用標示符對應(yīng)的cell主慰。// 注意,如果重用池(reuse pool)里面沒有可用的cell鲫售,這個方法會初始化并返回一個全新的cell共螺,// 因此無論怎樣,此行代碼過后情竹,你會得到一個布局約束已經(jīng)完全準備好藐不,可以直接使用的cell。UITableViewCell*cell=[tableViewdequeueReusableCellWithIdentifier:reuseIdentifier];// 用indexPath對應(yīng)的數(shù)據(jù)內(nèi)容來配置cell,例如:// cell.textLabel.text = someTextForThisCell;// ...// 確保cell的布局約束已經(jīng)被設(shè)置好雏蛮,因為它可能剛剛才被創(chuàng)建涎嚼。// 假設(shè)你已經(jīng)在cell的updateConstraints方法中設(shè)置好了約束,使用下面兩行代碼:[cellsetNeedsUpdateConstraints];[cellupdateConstraintsIfNeeded];// 如果你使用了多行的UILabel挑秉,不要忘了給label設(shè)置正確的preferredMaxLayoutWidth值法梯。// 如果你沒有在cell的layoutSubviews方法中設(shè)置,就需要在這里設(shè)置犀概。例如:// cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);returncell;}-(CGFloat)tableView:(UITableView*)tableViewheightForRowAtIndexPath:(NSIndexPath*)indexPath{// 判斷indexPath對應(yīng)cell的重用標示符立哑,NSString*reuseIdentifier=...;// 從緩存字典中取出重用標示符對應(yīng)的cell。如果沒有阱冶,就創(chuàng)建一個新的然后存儲在字典里面刁憋。// 警告:不要調(diào)用table view的dequeueReusableCellWithIdentifier:方法,因為這會導(dǎo)致cell被創(chuàng)建了但是又未曾被tableView:cellForRowAtIndexPath:方法返回木蹬,會造成內(nèi)存泄露至耻!// 譯注:原文這里說的dequeueReusableCellWithIdentifier:會造成內(nèi)存泄漏的說法是錯誤的,并不會造成內(nèi)存泄漏镊叁。UITableViewCell*cell=[self.offscreenCellsobjectForKey:reuseIdentifier];if(!cell){cell=[[YourTableViewCellClassalloc]init];[self.offscreenCellssetObject:cellforKey:reuseIdentifier];}// 用indexPath對應(yīng)的數(shù)據(jù)內(nèi)容來配置cell尘颓,例如:// cell.textLabel.text = someTextForThisCell;// ...// 確保cell的布局約束已經(jīng)被設(shè)置好,因為它可能剛剛才被創(chuàng)建晦譬。// 假設(shè)你已經(jīng)在cell的updateConstraints方法中設(shè)置好了約束疤苹,使用下面兩行代碼:[cellsetNeedsUpdateConstraints];[cellupdateConstraintsIfNeeded];// 將cell的寬度設(shè)置為與tableView的寬度一樣。// 這點很重要敛腌。// 如果cell的高度取決于table view的寬度(例如卧土,多行的UILabel通過單詞換行等方式換行),// 那么這使得對于不同寬度的table view像樊,我們都可以基于其寬度而得到cell的高度尤莺。// 但是,我們不需要在-[tableView:cellForRowAtIndexPath]方法中做相同的處理(設(shè)置寬度)生棍,// 因為颤霎,cell被用到table view中的時候,這一步是自動完成的涂滴。// 也要注意友酱,某些情況下,cell的最終寬度可能不等于table view的寬度柔纵。// 例如當table view的右邊顯示了section index的時候缔杉,必須要減去這個寬度。cell.bounds=CGRectMake(0.0f,0.0f,CGRectGetWidth(tableView.bounds),CGRectGetHeight(cell.bounds));// 觸發(fā)cell的布局過程搁料,會基于布局約束計算所有視圖的frame或详。// (注意进苍,你必須在cell的layoutSubviews方法中給多行的UILabel設(shè)置好preferredMaxLayoutWidth值;// 或者在下面2行代碼前手動設(shè)置Q夹稹)[cellsetNeedsLayout];[celllayoutIfNeeded];// 得到cell的contentView需要的真實高度CGFloatheight=[cell.contentViewsystemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;// 為cell的分割線加上額外的1pt高度。因為分隔線是被加在cell底邊與contentView底邊之間的拣宏。height+=1.0f;returnheight;}// 注意:除非行高極端變化并且你已經(jīng)明顯的覺察到了滾動時滾動條的“跳躍”現(xiàn)象沈贝,你才需要實現(xiàn)此方法;否則勋乾,直接用tableView的estimatedRowHeight屬性即可宋下。-(CGFloat)tableView:(UITableView*)tableViewestimatedHeightForRowAtIndexPath:(NSIndexPath*)indexPath{// 以最小計算量,返回實際高度數(shù)量級之內(nèi)的一個行高估算值辑莫。// 例如://if([selfisTallCellAtIndexPath:indexPath]){return350.0f;}else{return40.0f;}}
示例項目
iOS8的示例代碼- iOS8以上才支持
iOS7的示例代碼- iOS7+
最后学歧,推薦兩個相關(guān)的開源庫:
PureLayout:原文作者使用和開源的布局庫,用代碼寫布局約束的時候很方便各吨。
UITableView-CellHeightCalculation:根據(jù)本文思路封裝的UITableView動態(tài)行高計算和行高緩存庫枝笨,由本人開源和維護。