使用Autolayout實現(xiàn)UITableView的Cell動態(tài)布局和高度動態(tài)改變

原文地址:http://codingobjc.com/blog/2014/10/15/shi-yong-autolayoutshi-xian-uitableviewde-celldong-tai-bu-ju-he-ke-bian-xing-gao/index.html


本文翻譯自: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)行高計算和行高緩存庫枝笨,由本人開源和維護。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末揭蜒,一起剝皮案震驚了整個濱河市横浑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌屉更,老刑警劉巖徙融,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異瑰谜,居然都是意外死亡欺冀,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門萨脑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來隐轩,“玉大人,你說我怎么就攤上這事砚哗×” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵蛛芥,是天一觀的道長提鸟。 經(jīng)常有香客問我,道長仅淑,這世上最難降的妖魔是什么称勋? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮涯竟,結(jié)果婚禮上赡鲜,老公的妹妹穿的比我還像新娘空厌。我一直安慰自己,他們只是感情好银酬,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布嘲更。 她就那樣靜靜地躺著,像睡著了一般揩瞪。 火紅的嫁衣襯著肌膚如雪赋朦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天李破,我揣著相機與錄音宠哄,去河邊找鬼。 笑死嗤攻,一個胖子當著我的面吹牛毛嫉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播妇菱,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼承粤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了恶耽?” 一聲冷哼從身側(cè)響起密任,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎偷俭,沒想到半個月后浪讳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡涌萤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年淹遵,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片负溪。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡透揣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出川抡,到底是詐尸還是另有隱情辐真,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布崖堤,位于F島的核電站侍咱,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏密幔。R本人自食惡果不足惜楔脯,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胯甩。 院中可真熱鬧昧廷,春花似錦堪嫂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至眉枕,卻和暖如春愚战,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背齐遵。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留塔插,地道東北人梗摇。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像想许,于是被迫代替她去往敵國和親伶授。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容