通過storyboard自定不等高cell

我們在上一篇《通過代碼自定義不等高cell》中學習了tableView的相關知識,本文將在上文的基礎上,利用storyboard對自定義不等高cell部分做相應的修改。
和上文一樣,利用storyboard自定義不等高cell,視圖部分仍將是重點,控制器和模型部分的改動不是很大。為了保證邏輯上的清晰,我們還是按照上文的行文順序,分模塊逐一講解。
還是按照老習慣,新建一個工程,來到ViewController.h文件,讓ViewController繼承自UITableViewController,來到Main.storyboard,刪除View Controller控制器,拖一個UITableViewController,勾選"Is Initial View Controller",綁定類名為ViewController。至此,準備工作基本完成了,現在開始分模塊講解。
一、Model
具體操作步驟與《通過代碼自定義不等高cell》一樣,這里不做重復。
二、Controller
因為改變了創建cell的方式,所以控制器部分的代碼與《通過代碼自定義不等高cell》有些不一樣,我在這里直接給出詳細操作。
我們來到控制器部分,先聲明一個數組,用于處理模型數據:
// 用來加載來自statuses.plist文件中的字典數據,并且將其轉換為對應的模型 然后存儲起來@property(strong,nonatomic)NSArray*statusArr;
導入MJExtension框架,包含MJExtension和ESStatus的頭文件,然后重寫statusArr的getter方法:

- (NSArray*)statusArr {// 使用MJExtension框架if(!_statusArr) {// 將字典轉為模型_statusArr = [ESStatus mj_objectArrayWithFilename:@"statuses.plist"];    }return_statusArr;}

我們在上一篇《通過代碼自定義不等高cell》中學習了tableView的相關知識,本文將在上文的基礎上,利用storyboard對自定義不等高cell部分做相應的修改。
和上文一樣,利用storyboard自定義不等高cell,視圖部分仍將是重點,控制器和模型部分的改動不是很大。為了保證邏輯上的清晰,我們還是按照上文的行文順序,分模塊逐一講解。
還是按照老習慣,新建一個工程,來到ViewController.h文件,讓ViewController繼承自UITableViewController,來到Main.storyboard,刪除View Controller控制器,拖一個UITableViewController,勾選"Is Initial View Controller",綁定類名為ViewController。至此,準備工作基本完成了,現在開始分模塊講解。
一、Model
具體操作步驟與《通過代碼自定義不等高cell》一樣,這里不做重復。
二、Controller
因為改變了創建cell的方式,所以控制器部分的代碼與《通過代碼自定義不等高cell》有些不一樣,我在這里直接給出詳細操作。
我們來到控制器部分,先聲明一個數組,用于處理模型數據:
// 用來加載來自statuses.plist文件中的字典數據,并且將其轉換為對應的模型 然后存儲起來@property(strong,nonatomic)NSArray*statusArr;
導入MJExtension框架,包含MJExtension和ESStatus的頭文件,然后重寫statusArr的getter方法:

- (NSArray*)statusArr {// 使用MJExtension框架if(!_statusArr) {// 將字典轉為模型_statusArr = [ESStatus mj_objectArrayWithFilename:@"statuses.plist"];    }return_statusArr;}

實現數據源協議的- tableView: numberOfRowsInSection:方法,返回tableView中cell的行數。
// 返回tableView中cell行數- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {returnself.statusArr.count;// statusArr數組中元素的個數就是tableView中cell的行數}

新建一個繼承自UITableViewCell的ESStatusCell的類,來到Main.storyboard,展開View Controller Scene,選中Table View Cell,將其Class更改為ESStatusCell,如圖所示


綁定類名.png
繼續點擊Xcode右上角的"Show the Attributes Inspector",更改Table View Cell的Style為Custom,給Identifier綁定可重用標識符"status"。注意,這個可重用標識符可隨便寫,但是千萬不要與系統關鍵字重復,因為稍后后面還要用到。具體操作如下:


綁定cell循環利用標識符.png
包含ESStatusCell的頭文件,實現數據源協議的- tableView: cellForRowAtIndexPath:方法:
// 返回tableView中的cell- (UITableViewCell)tableView:--(UITableView)tableView cellForRowAtIndexPath:(NSIndexPath)indexPath {// 1.確立可重用標識符staticNSStringID =@"status";// 這個標識符一定要與前面Xcode中綁定的一致// 2. 根據可重用標識符去緩存池中取出可用的cellESStatusCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];// 3.將statusArr中的模型傳遞給視圖中的模型屬性cell.status=self.statusArr[indexPath.row];// 根據cell的行號取出模型數據returncell;}

至此,控制器部分的代碼暫時先告一段落,我們先去搞定視圖部分。
三、View
來到storyboard,展開View Controller Scene,選中status(也就是tableView的cell),將Prototype Cell拖大一點,方便往里面拖子控件。先拖入一個UIImageView控件,然后點擊Xcode右下角的Pin,彈出Add New Constraints界面,去掉Constrain to margins前面的勾,讓UIImageView頂部和左邊距離其父控件的距離為10,設置UIImageView的寬和高分別為30(一般情況下,大多數控件需要4條約束才能確定其位置和尺寸,不過也有例外,像UILabel只要有兩個約束就可以)。注意,對應約束條件的虛線變為紅色的實線,以及設置Width和Height前面打上勾才表示約束成功。然后點擊Update Frames后面的列表,選中Items of New Constraints,最后點擊Add 4 Constraints完成對UIImageView的約束。具體操作如下圖:


約束用戶頭像部分
關于通過storyboard來完成對子控件的AutoLayout約束,一般都比較簡單,網上有很多相關的教程,這里不做展開。按照操作步驟,我們再來布局剩下的子控件:


約束完以后大概是這個樣子
有必要提一下,在布局完微博正文子控件的時候,一定不要忘了點擊Xcode右上角的"Show the Attributes Inspector",設置Label的Lines屬性值為0,以保證微博正文文字在必要的時候完成換行。接下來,非常重要的一步,就是給已經布局完成子控件連線:


給cell上的子控件連線
在前面幾篇文章中,我們說過,視圖部分一般是三個步驟:創建子控件、給子控件設置位置和尺寸,以及給個子控件傳遞模型數據。完成連線,前面兩個步驟就算完成了,現在就是給子控件設置數據:

// 給子控件傳遞模型數據- (void)setStatus:(ESStatus *)status {    _status = status;self.iconImageView.image= [UIImageimageNamed:status.icon];// 給用戶頭像控件傳遞對應模型self.nameLabel.text= status.name;// 給用戶昵稱控件傳遞對應模型// 設置VIP圖標if(status.isVip) {// 如果是VIP用戶self.vipImageView.hidden=NO;// 顯示用戶VIP標識self.vipImageView.image= [UIImageimageNamed:@"vip"];// 設置VIP用的身份標識self.nameLabel.textColor= [UIColororangeColor];// VIP用戶昵稱用橙色標識}else{// 如果不是VIP用戶self.vipImageView.hidden=YES;// 隱藏VIP用戶標識self.nameLabel.textColor= [UIColorblackColor];// 非VIP用戶昵稱用黑色標識}self.text_label.text= status.text;// 給用戶微博文本控件傳遞模型數據// 用戶微博內容是否包含圖片if(status.picture) {// 如果用戶微博內容中包含圖片self.pictureImageView.hidden=NO;// 顯示用戶微博內容圖片控件self.pictureImageView.image= [UIImageimageNamed:status.picture];// 設置用戶微博正文配圖}else{// 如果用戶微博內容不包含圖片self.pictureImageView.hidden=YES;// 隱藏用戶微博內容圖片控件}}

我們運行程序看一下:


程序按照預期運行
從程序運行結果來看,UI界面的基本框架已經完成了,剩下的就是對cell的高度進行微調。
四、調整cell的高度
與通過代碼創建自定義cell不一樣,這回不需要我們手動去計算cell的高度了,只需要對相關代碼和約束做相應的調整就可以了。
首先來到控制器文件,在- viewDidLoad方法中設置tableView的rowHeight屬性為UITableViewAutomaticDimension,然后設置tableView的estimatedRowHeight為任意大于0的常量:

- (void)viewDidLoad {    [superviewDidLoad];self.tableView.rowHeight=UITableViewAutomaticDimension;// 表示cell高度自動計算,其中UITableViewAutomaticDimension是一個常量self.tableView.estimatedRowHeight=44;// 設置cell的估算高度,一般只要是大于0的常量就可以,但是習慣上設置它為系統默認的cell高度}

上面兩項設置號稱是蘋果的Self-Sizing Cells技術,功能非常強大,但是遺憾的是,它只支持iOS 8以后的項目,對于iOS 7就無能為力了。不過,在本項目中,它還不足以解決我們的問題。當然,這不是這項技術不行,主要是我們項目中的微博配圖控件在作怪。
因為微博配圖控件有時候顯示,有時候不顯示,所以我們應該對其做特殊處理。來到storyboard,展開微博配圖控件下面的Constraints約束,選中高度約束,往ESStatusCell.m文件中拖線,如下圖所示:


拿到微博配圖控件高度的約束
當然,光拿到微博配圖控件的高度約束還不夠。因為微博正文控件與微博配圖控件之間有10間距的約束,而微博配圖控件又與父控件之間有10的間距,所以,當微博配圖控件隱藏時,這兩個10的間距有一個必須跟著隱藏。我們拿到微博配圖控件與父控件之間間距為10的那條約束:


拿到微博配圖控件與父控件之間的約束
我們拿到上面這兩個控件以后,當微博配圖控件需要隱藏時,我們將其常量設置為0。當需要顯示微博配圖控件時,我們再恢復其常量的值:

// 用戶微博內容是否包含圖片if(status.picture) {// 如果用戶微博內容中包含圖片self.pictureImageView.hidden=NO;// 顯示用戶微博內容圖片控件self.pictureImageView.image= [UIImageimageNamed:status.picture];// 設置用戶微博正文配圖self.pictureHeight.constant=100;// 如果微博有配圖,則恢復其高度為100self.pictureBottom.constant=10;// 如果微博有配圖,則恢復其底部與父控件之間的距離為10}else{// 如果用戶微博內容不包含圖片self.pictureImageView.hidden=YES;// 隱藏用戶微博內容圖片控件self.pictureHeight.constant=0;// 如果微博沒有配圖,則將其高度設置為0self.pictureBottom.constant=0;// 如果微博沒有配圖,則設置其底部與父控件之間的距離為0}

運行程序瞅一瞅:


完美運行.gif
簡直是完美!O(∩_∩)O~。為了驗證上面所說的Self-Sizing Cells技術功能強大,我們可以將其注銷掉,然后再來運行項目看一下:


注銷自動計算代碼以后的運行效果.gif
注銷以后,程序的效果立馬就變挫了。與通過代碼自定義不等高cell比起來,用storyboard完成同樣的功能是不是簡單方便多了?但是,前面已經說過,這種技術只能應用在iOS 8及其以后的系統,如果要適配iOS 7怎么辦呢?作為負責任的好青年,下面就開始完成iOS 8之前的系統適配。
五、適配iOS 8之前的系統
首先來到storyboard,刪除微博配圖控件底部與父控件之間的那根約束。由于我們之前拿到過那根約束,所以在刪除之前先選中它,然后右擊,刪除它和pictureBottom之間的連線關系:


Snip20160821_86.png
刪除該約束與pictureBottom屬性之間的連線關系之后,可直接將其從Constraints中刪除了:


刪除微博配圖控件底部與父控件之間的約束.jpg
接著,刪除微博配圖控件的高度約束與pictureHeight屬性之間的連線關系(與上面不同,這次約束不能刪):


刪除微博配圖控件的高度約束與pictureHeight屬性之間的連線關系
兩約束與兩屬性之間的連線關系都沒了,這兩屬性也就沒用了。來到ESStatusCell.m文件,直接將其刪除:


刪除多余的屬性
多余的屬性也沒了,那么和該屬性有關的代碼也就沒用了,直接刪除解決系統報錯:


刪除沒用的代碼.png
我們在上一篇文章中講過,與tableView的cell高度相關的,除了rowHeight屬性之外,也就只剩下-tableView:heightForRowAtIndexPath:代理方法了。來到控制器,實現該方法:

#pragma mark -// 返回cell的高度- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {return250;// 250是一個比較吉利的數字,在這里可以幫你解決程序報錯}

要計算cell子控件的高度,就必須借助模型,在返回cell高度的方法中實現:

// 拿到indexPath.row這行cell對應的模型,借助模型獲取cell內部子控件的高度ESStatusCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];// 這里也要用到可重用標識符,建議將- tableView: cellForRowAtIndexPath:方法中聲明可重用標識符的代碼拿到外面去cell.status=self.statusArr[indexPath.row];

計算cell的高度。如果有微博配圖,我們只需要拿到微博配圖控件的最大y值,然后加上其底部與父控件之間10間距就可以;如果沒有微博配圖,我們只需拿到微博文本控件,然后利用其最大y值加上10間距就能計算出cell的高度。不過,我們之前將控件的屬性聲明在ESStatusCell的類擴展中,從控制器中是無法訪問的,為此,我們必須將其拿到它的頭文件中去。也就是說,現在整個視圖頭文件變成這樣:

#import@classESStatus;@interfaceESStatusCell:UITableViewCell/** 用于接收來自控制器傳入的模型數據 */@property(strong,nonatomic) ESStatus *status;// 模型屬性// 用戶頭像@property(weak,nonatomic)IBOutletUIImageView*iconImageView;// 不要與tableView的imageView屬性沖突// 用戶昵稱@property(weak,nonatomic)IBOutletUILabel*nameLabel;// 用戶的VIP標識@property(weak,nonatomic)IBOutletUIImageView*vipImageView;// 用戶發送的微博文字內容@property(weak,nonatomic)IBOutletUILabel*text_label;// 不要與tableView的textLabel屬性沖突// 用戶發送的微博配圖@property(weak,nonatomic)IBOutletUIImageView*pictureImageView;@end

我們再回到控制器中去,在- tableView:heightForRowAtIndexPath:方法中計算相關子控件的最大高度,然后將cell高度返回:

// 計算cell的高度CGFloatcellHeight =0;// 初始化cellHeight的值if(cell.status.picture) {// 如果微博有配圖cellHeight =CGRectGetMaxY(cell.pictureImageView.frame) +10;// 微博配圖控件的最大y值加上10間距就是cell的高度}else{// 如果沒有微博配圖cellHeight =CGRectGetMaxY(cell.text_label.frame) +10;// 微博文本控件的最大y值加上10間距就是cell的高度}returncellHeight;

理論上講,至此,我們的工作算是完成了,不過,我們先運行程序看一下:


待調試版.gif
從結果上看,這個并沒有達到我們預期的效果,有些地方間距很大,而有些地方,微博正文文字部分顯示不全。這是什么原因呢?想一下,我們計算cell高度的代碼,實際上是基于pictureImageView和text_label這兩個控件按照約束條件完全顯示出來以后這個前提條件的。可實際上,在我們計算cell高度的時候,這兩個控件并沒有按照相應的約束條件完全顯示出來。因此,我們所計算的cell高度的值,并不準確。
視圖中的子控件要先接收到數據,然后才能根據相應的約束條件計算出正確的frame,而子控件中的數據是通過- tableView: cellForRowAtIndexPath:這個方法傳遞過來的。另外,我們計算cell高度的代碼是在- tableView:heightForRowAtIndexPath:這個方法中進行的。為了說明問題,我們先來通過打印,查看一下這兩個方法的調用順序:


兩個方法的調用順序
從打印結果來看,- tableView:heightForRowAtIndexPath:這個方法的調用要比- tableView: cellForRowAtIndexPath:這個方法早。也就是說,子控件還沒有接收到數據,并且,在有數據的基礎上,依據提前設置的約束條件,計算好子控件的frame,我們就已經在- tableView:heightForRowAtIndexPath:這個方法中把cell的高度算完了,這樣結果當然不對了!為此,我們必須想辦法,要在計算cell高度的代碼之前,讓cell內部的子控件依據約束條件提前計算一遍。解決的辦法是強制刷新一下:
// 強制刷新[cell layoutIfNeeded];
來看一下強制刷新以后的結果:


強制刷新.gif
強制刷新以后,效果是好很多了,但是文字控件部分還是有問題。原因與上面一樣,還是因為文字控件最大寬度計算晚了。我們在強制刷新代碼之前,先手動計算文字控件的最大寬度:
// 計算文字控件的最大寬度cell.text_label.preferredMaxLayoutWidth= [UIScreenmainScreen].bounds.size.width-20;

再看一下程序運行的效果:


高度計算完畢.gif
六、性能優化
通過前面的學習,我們知道- tableView:heightForRowAtIndexPath:這個方法調用非常頻繁,出于程序性能方面的考慮,我們還需要對其做相應的優化工作。
1、優化- tableView:heightForRowAtIndexPath:方法中的cell
我們之所以要在- tableView:heightForRowAtIndexPath:方法中創建cell,目的是為了臨時給其傳遞數據,能提前計算其內部子控件的frame。在代碼優化之前,我們先來打印一下這個方法中所創建cell的內存地址:

系統創建了很多cell
從打印出來的cell內存地址,以及右邊的垂直拖動條可知,我們創建了非常多的cell。其實沒這個必要,我們只需要創建一個cell,然后給它賦值不同的數據就可以了。所以,我們應該將創建cell的代碼變成全局變量:

ESStatusCell *cell;// 聲明一個全局變量#pragma mark -
// 返回cell的高度- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {if(cell ==nil) {// 拿到indexPath.row這行cell對應的模型,借助模型獲取cell內部子控件的高度cell = [tableView dequeueReusableCellWithIdentifier:ID];    }    cell.status=self.statusArr[indexPath.row];// 計算文字控件的最大寬度cell.text_label.preferredMaxLayoutWidth= [UIScreenmainScreen].bounds.size.width-20;// 強制刷新[cell layoutIfNeeded];// 計算cell的高度CGFloatcellHeight =0;if(cell.status.picture) {// 如果微博有配圖cellHeight =CGRectGetMaxY(cell.pictureImageView.frame) +10;// 微博配圖控件的最大y值加上10間距就是cell的高度}else{// 如果沒有微博配圖cellHeight =CGRectGetMaxY(cell.text_label.frame) +10;// 微博文本控件的最大y值加上10間距就是cell的高度}returncellHeight;}

我們再來打印一下cell的內存地址:


其實只有一份cell
雖然還是打印了很多,但是從內存地址來看,其實這些cell都是同一個,這樣就達到了一個性能優化的目的。
2、優化- tableView:heightForRowAtIndexPath:方法
從之前的打印結果來看,- tableView:heightForRowAtIndexPath:方法調用非常的頻繁,每個cell都最少要調用一次,有些cell甚至會反復調用。那么,為什么要調用這么多次呢?這個還得從tableView的contentSize說起。
我們都知道,蘋果做事情是很注重用戶體驗的。從前面的動圖我們能看出,每次tableView出現以后,屏幕右邊會出來一個垂直的滾動條,用戶可以通過這個滾動條大致知道后面還有多少數據。而屏幕上滾動條的長短又跟tableView的contentSize有關。頻繁的調用- tableView:heightForRowAtIndexPath:方法,其目的就是為了設置tableView的contentSize。
這樣做,用戶體驗看起來是不錯,不過,當tableView有很多分組,且每組又有很多行時,那么這個方法的調用頻率將是非常可觀的。有時候一個App打開以后,屏幕會先出現白屏,然后再加載tableView的數據,或多或少是因為這個原因。蘋果也意識到了這個問題,所以后來就推出了自動估算技術。
自動估算技術既可以保證App加載時給tableView設置合適的contentSize,又能解決程序性能問題。接下來我們將研究一下自動估算技術。
我們在本文中間提到過,在設置tableView的estimatedRowHeight屬性的值時,只要大于0就可以,其實這個值的設置也是有一定技巧的。通常情況下,我們會給這個屬性賦值44,因為系統的cell默認高度就是44。但是,作為本項目而言,我們完全可以將它的值設大一點。
我們先通過打印了解一下,在未設置estimatedRowHeight屬性值時的情況:

未設置estimatedRowHeight屬性的值之前.png
從結果上看,打印是非常之多。我們再來看一下,將estimatedRowHeight屬性的值設置為44以后的情況:


只有9條數據
從打印結果來看,打印明顯減少了,只有9條。我們再來看一下,將estimatedRowHeight屬性的值設置為100以后的情況:


打印結果進一步減少到7條數據.png
打印結果更少了,只有7條數據。我們再來看一下,將estimatedRowHeight屬性的值設置為200以后的情況:


打印結果只有5條.png
我們看到,打印結果只剩5條了。我們再來看一下,將estimatedRowHeight屬性的值設置為1000以后的情況:


打印結果還是5條.png
我之所以如此執著于打印,并不是因為我是打印狂魔,更不是為了湊篇幅,而是為了說明問題。沒有發現,我每次上打印截圖的時候,都把控制器屏幕一起截在內?其實,這個estimatedRowHeight屬性的值是跟我們的屏幕高度,以及tableView上面cell的實際高度有關的!
我用的模擬器是iPhone 6s,它的屏幕高度為667,而屏幕上現在能看到的cell數量剛好是5個。我們第一次設置estimatedRowHeight屬性的值是44,667 / 44 = 15.1,理論上講,應該會打印16條信息,但是因為我們的tableView總共只有9條cell,因此只打印9條信息。第二次我們設置estimatedRowHeight屬性的值為100以后,屏幕打印出7條信息,是因為667 / 100 = 6.67,所以打印7條信息。但是,只要我們estimatedRowHeight屬性的值大于667 / 5 = 133.4以后,控制臺打印出來的信息永遠都只有5條,那是因為我們我們屏幕上現在顯示的剛好是5條。
通過上面的分析,我們可以得出這樣一個結論:合理的設置estimatedRowHeight屬性的值,可以有效減少- tableView:heightForRowAtIndexPath:方法的調用次數,從而達到優化程序性能的目的。但是,這個屬性值的大小與屏幕高度,以及cell的實際高度相關,既不是越小越好,也不是越大越好。
除了可以通過tableView的estimatedRowHeight屬性實現自動估算技術之外,還可以實現代理的- tableView: estimatedHeightForRowAtIndexPath:方法達到同樣的目的。
3、優化計算cell高度的設計方案
我們的代碼還有另外一個問題。回想一下我們之前所學到的知識,就是一個控件內部私有的屬性,最好是聲明在類擴展中,不要暴露在外面。但是,我們在上面計算cell高度時,恰恰違反了這個原則。為了能在控制器中拿到視圖內部的相關控件,我們將ESStatusCell的私有屬性移到了ESStatusCell.h文件中,這樣做是不安全的。為此,我們必須想一個辦法,在既不違反設計原則的情況下,又能讓外面獲取cell的真實高度。
解決案是,把計算cell真實高度的那部分代碼封裝在ESStatusCell這個類里面,然后再將計算結果返回,以供外界調用。具體操作是,在ESStatusCell.h文件中聲明一個cellHeight屬性,然后在ESStatusCell.m文件中重寫cellHeight的getter方法,最后,控制器可以通過訪問cell的cellHeight屬性獲取相應的結果。
所以,ESStatusCell.h文件最終的代碼應該是:

#import@classESStatus;@interfaceESStatusCell:UITableViewCell/** 用于接收來自控制器傳入的模型數據 */@property(strong,nonatomic) ESStatus *status;// 模型屬性// 提供計算cell高度的借口@property(assign,nonatomic)CGFloatcellHeight;@end

重寫cellHeight的getter方法:

// 返回cell的高度。因為- tableView:heightForRowAtIndexPath:方法的關系,這個調用也很頻繁- (CGFloat)cellHeight {// 計算文字控件的最大寬度// self.text_label.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width - 20;// 強制刷新[selflayoutIfNeeded];// 計算cell的高度CGFloatcellHeight =0;if(self.status.picture) {// 如果微博有配圖cellHeight =CGRectGetMaxY(self.pictureImageView.frame) +10;// 微博配圖控件的最大y值加上10間距就是cell的高度}else{// 如果沒有微博配圖cellHeight =CGRectGetMaxY(self.text_label.frame) +10;// 微博文本控件的最大y值加上10間距就是cell的高度}returncellHeight;}

不過要注意,因為- tableView:heightForRowAtIndexPath:方法的關系,- cellHeight這個方法的調用也很頻繁,而計算微博文本控件的最大寬度只需要計算一次,所以可以考慮將其放在- awakeFromNib方法中執行:

- (void)awakeFromNib {// 計算文字控件的最大寬度self.text_label.preferredMaxLayoutWidth= [UIScreenmainScreen].bounds.size.width-20;}
  • tableView:heightForRowAtIndexPath:方法在外面通過訪問cell的cellHeight屬性,獲取cell的真實高度:
ESStatusCell *cell;#pragma mark -// 返回cell的高度- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {if(cell ==nil) {// 拿到indexPath.row這行cell對應的模型,借助模型獲取cell內部子控件的高度cell = [tableView dequeueReusableCellWithIdentifier:ID];    }    cell.status=self.statusArr[indexPath.row];returncell.cellHeight;// 返回cell的高度}

原文鏈接:http://www.lxweimin.com/p/35f5b68bbe4a
實現數據源協議的- tableView: numberOfRowsInSection:方法,返回tableView中cell的行數。
// 返回tableView中cell行數- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section {returnself.statusArr.count;// statusArr數組中元素的個數就是tableView中cell的行數}

新建一個繼承自UITableViewCell的ESStatusCell的類,來到Main.storyboard,展開View Controller Scene,選中Table View Cell,將其Class更改為ESStatusCell,如圖所示


綁定類名.png
繼續點擊Xcode右上角的"Show the Attributes Inspector",更改Table View Cell的Style為Custom,給Identifier綁定可重用標識符"status"。注意,這個可重用標識符可隨便寫,但是千萬不要與系統關鍵字重復,因為稍后后面還要用到。具體操作如下:


綁定cell循環利用標識符.png
包含ESStatusCell的頭文件,實現數據源協議的- tableView: cellForRowAtIndexPath:方法:

// 返回tableView中的cell- (UITableViewCell*)tableView:--(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {// 1.確立可重用標識符staticNSString*ID =@"status";// 這個標識符一定要與前面Xcode中綁定的一致// 2. 根據可重用標識符去緩存池中取出可用的cellESStatusCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];// 3.將statusArr中的模型傳遞給視圖中的模型屬性cell.status=self.statusArr[indexPath.row];// 根據cell的行號取出模型數據returncell;}

至此,控制器部分的代碼暫時先告一段落,我們先去搞定視圖部分。
三、View
來到storyboard,展開View Controller Scene,選中status(也就是tableView的cell),將Prototype Cell拖大一點,方便往里面拖子控件。先拖入一個UIImageView控件,然后點擊Xcode右下角的Pin,彈出Add New Constraints界面,去掉Constrain to margins前面的勾,讓UIImageView頂部和左邊距離其父控件的距離為10,設置UIImageView的寬和高分別為30(一般情況下,大多數控件需要4條約束才能確定其位置和尺寸,不過也有例外,像UILabel只要有兩個約束就可以)。注意,對應約束條件的虛線變為紅色的實線,以及設置Width和Height前面打上勾才表示約束成功。然后點擊Update Frames后面的列表,選中Items of New Constraints,最后點擊Add 4 Constraints完成對UIImageView的約束。具體操作如下圖:


約束用戶頭像部分
關于通過storyboard來完成對子控件的AutoLayout約束,一般都比較簡單,網上有很多相關的教程,這里不做展開。按照操作步驟,我們再來布局剩下的子控件:


約束完以后大概是這個樣子
有必要提一下,在布局完微博正文子控件的時候,一定不要忘了點擊Xcode右上角的"Show the Attributes Inspector",設置Label的Lines屬性值為0,以保證微博正文文字在必要的時候完成換行。接下來,非常重要的一步,就是給已經布局完成子控件連線:


給cell上的子控件連線
在前面幾篇文章中,我們說過,視圖部分一般是三個步驟:創建子控件、給子控件設置位置和尺寸,以及給個子控件傳遞模型數據。完成連線,前面兩個步驟就算完成了,現在就是給子控件設置數據:

// 給子控件傳遞模型數據- (void)setStatus:(ESStatus *)status {    _status = status;self.iconImageView.image= [UIImageimageNamed:status.icon];// 給用戶頭像控件傳遞對應模型self.nameLabel.text= status.name;// 給用戶昵稱控件傳遞對應模型// 設置VIP圖標if(status.isVip) {// 如果是VIP用戶self.vipImageView.hidden=NO;// 顯示用戶VIP標識self.vipImageView.image= [UIImageimageNamed:@"vip"];// 設置VIP用的身份標識self.nameLabel.textColor= [UIColororangeColor];// VIP用戶昵稱用橙色標識}else{// 如果不是VIP用戶self.vipImageView.hidden=YES;// 隱藏VIP用戶標識self.nameLabel.textColor= [UIColorblackColor];// 非VIP用戶昵稱用黑色標識}self.text_label.text= status.text;// 給用戶微博文本控件傳遞模型數據// 用戶微博內容是否包含圖片if(status.picture) {// 如果用戶微博內容中包含圖片self.pictureImageView.hidden=NO;// 顯示用戶微博內容圖片控件self.pictureImageView.image= [UIImageimageNamed:status.picture];// 設置用戶微博正文配圖}else{// 如果用戶微博內容不包含圖片self.pictureImageView.hidden=YES;// 隱藏用戶微博內容圖片控件}}

我們運行程序看一下:


程序按照預期運行
從程序運行結果來看,UI界面的基本框架已經完成了,剩下的就是對cell的高度進行微調。
四、調整cell的高度
與通過代碼創建自定義cell不一樣,這回不需要我們手動去計算cell的高度了,只需要對相關代碼和約束做相應的調整就可以了。
首先來到控制器文件,在- viewDidLoad方法中設置tableView的rowHeight屬性為UITableViewAutomaticDimension,然后設置tableView的estimatedRowHeight為任意大于0的常量:

- (void)viewDidLoad {    [superviewDidLoad];self.tableView.rowHeight=UITableViewAutomaticDimension;// 表示cell高度自動計算,其中UITableViewAutomaticDimension是一個常量self.tableView.estimatedRowHeight=44;// 設置cell的估算高度,一般只要是大于0的常量就可以,但是習慣上設置它為系統默認的cell高度}

上面兩項設置號稱是蘋果的Self-Sizing Cells技術,功能非常強大,但是遺憾的是,它只支持iOS 8以后的項目,對于iOS 7就無能為力了。不過,在本項目中,它還不足以解決我們的問題。當然,這不是這項技術不行,主要是我們項目中的微博配圖控件在作怪。
因為微博配圖控件有時候顯示,有時候不顯示,所以我們應該對其做特殊處理。來到storyboard,展開微博配圖控件下面的Constraints約束,選中高度約束,往ESStatusCell.m文件中拖線,如下圖所示:


拿到微博配圖控件高度的約束
當然,光拿到微博配圖控件的高度約束還不夠。因為微博正文控件與微博配圖控件之間有10間距的約束,而微博配圖控件又與父控件之間有10的間距,所以,當微博配圖控件隱藏時,這兩個10的間距有一個必須跟著隱藏。我們拿到微博配圖控件與父控件之間間距為10的那條約束:


拿到微博配圖控件與父控件之間的約束
我們拿到上面這兩個控件以后,當微博配圖控件需要隱藏時,我們將其常量設置為0。當需要顯示微博配圖控件時,我們再恢復其常量的值:

// 用戶微博內容是否包含圖片if(status.picture) {// 如果用戶微博內容中包含圖片self.pictureImageView.hidden=NO;// 顯示用戶微博內容圖片控件self.pictureImageView.image= [UIImageimageNamed:status.picture];// 設置用戶微博正文配圖self.pictureHeight.constant=100;// 如果微博有配圖,則恢復其高度為100self.pictureBottom.constant=10;// 如果微博有配圖,則恢復其底部與父控件之間的距離為10}else{// 如果用戶微博內容不包含圖片self.pictureImageView.hidden=YES;// 隱藏用戶微博內容圖片控件self.pictureHeight.constant=0;// 如果微博沒有配圖,則將其高度設置為0self.pictureBottom.constant=0;// 如果微博沒有配圖,則設置其底部與父控件之間的距離為0}

運行程序瞅一瞅:


完美運行.gif
簡直是完美!O(∩_∩)O~。為了驗證上面所說的Self-Sizing Cells技術功能強大,我們可以將其注銷掉,然后再來運行項目看一下:
[圖片上傳中。。。(10)]

注銷自動計算代碼以后的運行效果.gif
注銷以后,程序的效果立馬就變挫了。與通過代碼自定義不等高cell比起來,用storyboard完成同樣的功能是不是簡單方便多了?但是,前面已經說過,這種技術只能應用在iOS 8及其以后的系統,如果要適配iOS 7怎么辦呢?作為負責任的好青年,下面就開始完成iOS 8之前的系統適配。
五、適配iOS 8之前的系統
首先來到storyboard,刪除微博配圖控件底部與父控件之間的那根約束。由于我們之前拿到過那根約束,所以在刪除之前先選中它,然后右擊,刪除它和pictureBottom之間的連線關系:


Snip20160821_86.png
刪除該約束與pictureBottom屬性之間的連線關系之后,可直接將其從Constraints中刪除了:


刪除微博配圖控件底部與父控件之間的約束.jpg
接著,刪除微博配圖控件的高度約束與pictureHeight屬性之間的連線關系(與上面不同,這次約束不能刪):


刪除微博配圖控件的高度約束與pictureHeight屬性之間的連線關系
兩約束與兩屬性之間的連線關系都沒了,這兩屬性也就沒用了。來到ESStatusCell.m文件,直接將其刪除:


刪除多余的屬性
多余的屬性也沒了,那么和該屬性有關的代碼也就沒用了,直接刪除解決系統報錯:


刪除沒用的代碼.png
我們在上一篇文章中講過,與tableView的cell高度相關的,除了rowHeight屬性之外,也就只剩下-tableView:heightForRowAtIndexPath:代理方法了。來到控制器,實現該方法:

#pragma mark -// 返回cell的高度- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {return250;// 250是一個比較吉利的數字,在這里可以幫你解決程序報錯}

要計算cell子控件的高度,就必須借助模型,在返回cell高度的方法中實現:

// 拿到indexPath.row這行cell對應的模型,借助模型獲取cell內部子控件的高度ESStatusCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];// 這里也要用到可重用標識符,建議將- tableView: cellForRowAtIndexPath:方法中聲明可重用標識符的代碼拿到外面去cell.status=self.statusArr[indexPath.row];

計算cell的高度。如果有微博配圖,我們只需要拿到微博配圖控件的最大y值,然后加上其底部與父控件之間10間距就可以;如果沒有微博配圖,我們只需拿到微博文本控件,然后利用其最大y值加上10間距就能計算出cell的高度。不過,我們之前將控件的屬性聲明在ESStatusCell的類擴展中,從控制器中是無法訪問的,為此,我們必須將其拿到它的頭文件中去。也就是說,現在整個視圖頭文件變成這樣:

#import@classESStatus;@interfaceESStatusCell:UITableViewCell/** 用于接收來自控制器傳入的模型數據 */@property(strong,nonatomic) ESStatus *status;// 模型屬性// 用戶頭像@property(weak,nonatomic)IBOutletUIImageView*iconImageView;// 不要與tableView的imageView屬性沖突// 用戶昵稱@property(weak,nonatomic)IBOutletUILabel*nameLabel;// 用戶的VIP標識@property(weak,nonatomic)IBOutletUIImageView*vipImageView;// 用戶發送的微博文字內容@property(weak,nonatomic)IBOutletUILabel*text_label;// 不要與tableView的textLabel屬性沖突// 用戶發送的微博配圖@property(weak,nonatomic)IBOutletUIImageView*pictureImageView;@end

我們再回到控制器中去,在- tableView:heightForRowAtIndexPath:方法中計算相關子控件的最大高度,然后將cell高度返回:

// 計算cell的高度CGFloatcellHeight =0;// 初始化cellHeight的值if(cell.status.picture) {// 如果微博有配圖cellHeight =CGRectGetMaxY(cell.pictureImageView.frame) +10;// 微博配圖控件的最大y值加上10間距就是cell的高度}else{// 如果沒有微博配圖cellHeight =CGRectGetMaxY(cell.text_label.frame) +10;// 微博文本控件的最大y值加上10間距就是cell的高度}returncellHeight;

理論上講,至此,我們的工作算是完成了,不過,我們先運行程序看一下:


待調試版.gif
從結果上看,這個并沒有達到我們預期的效果,有些地方間距很大,而有些地方,微博正文文字部分顯示不全。這是什么原因呢?想一下,我們計算cell高度的代碼,實際上是基于pictureImageView和text_label這兩個控件按照約束條件完全顯示出來以后這個前提條件的。可實際上,在我們計算cell高度的時候,這兩個控件并沒有按照相應的約束條件完全顯示出來。因此,我們所計算的cell高度的值,并不準確。
視圖中的子控件要先接收到數據,然后才能根據相應的約束條件計算出正確的frame,而子控件中的數據是通過- tableView: cellForRowAtIndexPath:這個方法傳遞過來的。另外,我們計算cell高度的代碼是在- tableView:heightForRowAtIndexPath:這個方法中進行的。為了說明問題,我們先來通過打印,查看一下這兩個方法的調用順序:


兩個方法的調用順序
從打印結果來看,- tableView:heightForRowAtIndexPath:這個方法的調用要比- tableView: cellForRowAtIndexPath:這個方法早。也就是說,子控件還沒有接收到數據,并且,在有數據的基礎上,依據提前設置的約束條件,計算好子控件的frame,我們就已經在- tableView:heightForRowAtIndexPath:這個方法中把cell的高度算完了,這樣結果當然不對了!為此,我們必須想辦法,要在計算cell高度的代碼之前,讓cell內部的子控件依據約束條件提前計算一遍。解決的辦法是強制刷新一下:
// 強制刷新[cell layoutIfNeeded];
來看一下強制刷新以后的結果:


強制刷新.gif
強制刷新以后,效果是好很多了,但是文字控件部分還是有問題。原因與上面一樣,還是因為文字控件最大寬度計算晚了。我們在強制刷新代碼之前,先手動計算文字控件的最大寬度:

// 計算文字控件的最大寬度cell.text_label.preferredMaxLayoutWidth= [UIScreenmainScreen].bounds.size.width-20;

再看一下程序運行的效果:


高度計算完畢.gif
六、性能優化
通過前面的學習,我們知道- tableView:heightForRowAtIndexPath:這個方法調用非常頻繁,出于程序性能方面的考慮,我們還需要對其做相應的優化工作。
1、優化- tableView:heightForRowAtIndexPath:方法中的cell
我們之所以要在- tableView:heightForRowAtIndexPath:方法中創建cell,目的是為了臨時給其傳遞數據,能提前計算其內部子控件的frame。在代碼優化之前,我們先來打印一下這個方法中所創建cell的內存地址:

系統創建了很多cell
從打印出來的cell內存地址,以及右邊的垂直拖動條可知,我們創建了非常多的cell。其實沒這個必要,我們只需要創建一個cell,然后給它賦值不同的數據就可以了。所以,我們應該將創建cell的代碼變成全局變量:

ESStatusCell *cell;// 聲明一個全局變量#pragma mark -
// 返回cell的高度- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {if(cell ==nil) {// 拿到indexPath.row這行cell對應的模型,借助模型獲取cell內部子控件的高度cell = [tableView dequeueReusableCellWithIdentifier:ID];    }    cell.status=self.statusArr[indexPath.row];// 計算文字控件的最大寬度cell.text_label.preferredMaxLayoutWidth= [UIScreenmainScreen].bounds.size.width-20;// 強制刷新[cell layoutIfNeeded];// 計算cell的高度CGFloatcellHeight =0;if(cell.status.picture) {// 如果微博有配圖cellHeight =CGRectGetMaxY(cell.pictureImageView.frame) +10;// 微博配圖控件的最大y值加上10間距就是cell的高度}else{// 如果沒有微博配圖cellHeight =CGRectGetMaxY(cell.text_label.frame) +10;// 微博文本控件的最大y值加上10間距就是cell的高度}returncellHeight;}

我們再來打印一下cell的內存地址:


其實只有一份cell
雖然還是打印了很多,但是從內存地址來看,其實這些cell都是同一個,這樣就達到了一個性能優化的目的。
2、優化- tableView:heightForRowAtIndexPath:方法
從之前的打印結果來看,- tableView:heightForRowAtIndexPath:方法調用非常的頻繁,每個cell都最少要調用一次,有些cell甚至會反復調用。那么,為什么要調用這么多次呢?這個還得從tableView的contentSize說起。
我們都知道,蘋果做事情是很注重用戶體驗的。從前面的動圖我們能看出,每次tableView出現以后,屏幕右邊會出來一個垂直的滾動條,用戶可以通過這個滾動條大致知道后面還有多少數據。而屏幕上滾動條的長短又跟tableView的contentSize有關。頻繁的調用- tableView:heightForRowAtIndexPath:方法,其目的就是為了設置tableView的contentSize。
這樣做,用戶體驗看起來是不錯,不過,當tableView有很多分組,且每組又有很多行時,那么這個方法的調用頻率將是非常可觀的。有時候一個App打開以后,屏幕會先出現白屏,然后再加載tableView的數據,或多或少是因為這個原因。蘋果也意識到了這個問題,所以后來就推出了自動估算技術。
自動估算技術既可以保證App加載時給tableView設置合適的contentSize,又能解決程序性能問題。接下來我們將研究一下自動估算技術。
我們在本文中間提到過,在設置tableView的estimatedRowHeight屬性的值時,只要大于0就可以,其實這個值的設置也是有一定技巧的。通常情況下,我們會給這個屬性賦值44,因為系統的cell默認高度就是44。但是,作為本項目而言,我們完全可以將它的值設大一點。
我們先通過打印了解一下,在未設置estimatedRowHeight屬性值時的情況:

未設置estimatedRowHeight屬性的值之前.png
從結果上看,打印是非常之多。我們再來看一下,將estimatedRowHeight屬性的值設置為44以后的情況:


只有9條數據
從打印結果來看,打印明顯減少了,只有9條。我們再來看一下,將estimatedRowHeight屬性的值設置為100以后的情況:


打印結果進一步減少到7條數據.png
打印結果更少了,只有7條數據。我們再來看一下,將estimatedRowHeight屬性的值設置為200以后的情況:


打印結果只有5條.png
我們看到,打印結果只剩5條了。我們再來看一下,將estimatedRowHeight屬性的值設置為1000以后的情況:


打印結果還是5條.png
我之所以如此執著于打印,并不是因為我是打印狂魔,更不是為了湊篇幅,而是為了說明問題。沒有發現,我每次上打印截圖的時候,都把控制器屏幕一起截在內?其實,這個estimatedRowHeight屬性的值是跟我們的屏幕高度,以及tableView上面cell的實際高度有關的!
我用的模擬器是iPhone 6s,它的屏幕高度為667,而屏幕上現在能看到的cell數量剛好是5個。我們第一次設置estimatedRowHeight屬性的值是44,667 / 44 = 15.1,理論上講,應該會打印16條信息,但是因為我們的tableView總共只有9條cell,因此只打印9條信息。第二次我們設置estimatedRowHeight屬性的值為100以后,屏幕打印出7條信息,是因為667 / 100 = 6.67,所以打印7條信息。但是,只要我們estimatedRowHeight屬性的值大于667 / 5 = 133.4以后,控制臺打印出來的信息永遠都只有5條,那是因為我們我們屏幕上現在顯示的剛好是5條。
通過上面的分析,我們可以得出這樣一個結論:合理的設置estimatedRowHeight屬性的值,可以有效減少- tableView:heightForRowAtIndexPath:方法的調用次數,從而達到優化程序性能的目的。但是,這個屬性值的大小與屏幕高度,以及cell的實際高度相關,既不是越小越好,也不是越大越好。
除了可以通過tableView的estimatedRowHeight屬性實現自動估算技術之外,還可以實現代理的- tableView: estimatedHeightForRowAtIndexPath:方法達到同樣的目的。
3、優化計算cell高度的設計方案
我們的代碼還有另外一個問題。回想一下我們之前所學到的知識,就是一個控件內部私有的屬性,最好是聲明在類擴展中,不要暴露在外面。但是,我們在上面計算cell高度時,恰恰違反了這個原則。為了能在控制器中拿到視圖內部的相關控件,我們將ESStatusCell的私有屬性移到了ESStatusCell.h文件中,這樣做是不安全的。為此,我們必須想一個辦法,在既不違反設計原則的情況下,又能讓外面獲取cell的真實高度。
解決案是,把計算cell真實高度的那部分代碼封裝在ESStatusCell這個類里面,然后再將計算結果返回,以供外界調用。具體操作是,在ESStatusCell.h文件中聲明一個cellHeight屬性,然后在ESStatusCell.m文件中重寫cellHeight的getter方法,最后,控制器可以通過訪問cell的cellHeight屬性獲取相應的結果。
所以,ESStatusCell.h文件最終的代碼應該是:

#import@classESStatus;@interfaceESStatusCell:UITableViewCell/** 用于接收來自控制器傳入的模型數據 */@property(strong,nonatomic) ESStatus *status;// 模型屬性// 提供計算cell高度的借口@property(assign,nonatomic)CGFloatcellHeight;@end

重寫cellHeight的getter方法:

// 返回cell的高度。因為- tableView:heightForRowAtIndexPath:方法的關系,這個調用也很頻繁- (CGFloat)cellHeight {// 計算文字控件的最大寬度// self.text_label.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width - 20;// 強制刷新[selflayoutIfNeeded];// 計算cell的高度CGFloatcellHeight =0;if(self.status.picture) {// 如果微博有配圖cellHeight =CGRectGetMaxY(self.pictureImageView.frame) +10;// 微博配圖控件的最大y值加上10間距就是cell的高度}else{// 如果沒有微博配圖cellHeight =CGRectGetMaxY(self.text_label.frame) +10;// 微博文本控件的最大y值加上10間距就是cell的高度}returncellHeight;}

不過要注意,因為- tableView:heightForRowAtIndexPath:方法的關系,- cellHeight這個方法的調用也很頻繁,而計算微博文本控件的最大寬度只需要計算一次,所以可以考慮將其放在- awakeFromNib方法中執行:

- (void)awakeFromNib {// 計算文字控件的最大寬度self.text_label.preferredMaxLayoutWidth= [UIScreenmainScreen].bounds.size.width-20;}
  • tableView:heightForRowAtIndexPath:方法在外面通過訪問cell的cellHeight屬性,獲取cell的真實高度:
ESStatusCell *cell;#pragma mark -// 返回cell的高度- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {if(cell ==nil) {// 拿到indexPath.row這行cell對應的模型,借助模型獲取cell內部子控件的高度cell = [tableView dequeueReusableCellWithIdentifier:ID];    }    cell.status=self.statusArr[indexPath.row];returncell.cellHeight;// 返回cell的高度}

原文鏈接:http://www.lxweimin.com/p/35f5b68bbe4a

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容