//非原創
看TableView的資料其實已經蠻久了,一直想寫點兒東西,卻總是因為各種原因拖延,今天晚上有時間靜下心來記錄一些最近學習的TableView的知識。下面進入正題,UITableView堪稱UIKit里面最復雜的一個控件了,使用起來不算難,但是要用好并不容易。當使用的時候我們必須要考慮到后臺數據的設計,tableViewCell的設計和重用以及tableView的效率等問題。
下面分9個方面進行介紹:
一、UITableView概述
UITableView繼承自UIScrollView,可以表現為Plain和Grouped兩種風格,分別如下圖所示:
其中左邊的是Plain風格的,右邊的是Grouped風格,這個區別還是很明顯的。
查看UITableView的幫助文檔我們會注意到UITableView有兩個Delegate分別為:dataSource和delegate。
dataSource是UITableViewDataSource類型,主要為UITableView提供顯示用的數據(UITableViewCell),指定UITableViewCell支持的編輯操作類型(insert,delete和reordering),并根據用戶的操作進行相應的數據更新操作,如果數據沒有更具操作進行正確的更新,可能會導致顯示異常,甚至crush。
delegate是UITableViewDelegate類型,主要提供一些可選的方法,用來控制tableView的選擇、指定section的頭和尾的顯示以及協助完成cell的刪除和排序等功能。
提到UITableView,就必須的說一說NSIndexPath。UITableView聲明了一個NSIndexPath的類別,主要用來標識當前cell的在tableView中的位置,該類別有section和row兩個屬性,前者標識當前cell處于第幾個section中,后者代表在該section中的第幾行。
UITableView只能有一列數據(cell),且只支持縱向滑動,當創建好的tablView第一次顯示的時候,我們需要調用其reloadData方法,強制刷新一次,從而使tableView的數據更新到最新狀態。
二、UITableViewController簡介
UITableViewController是系統提供的一個便利類,主要是為了方便我們使用UITableView,該類生成的時候就將自身設置成了其包含的tableView的dataSource和delegate,并創建了很多代理函數的框架,為我們大大的節省了時間,我們可以通過其tableView屬性獲取該controller內部維護的tableView對象。默認情況下使用UITableViewController創建的tableView是充滿全屏的,如果需要用到tableView是不充滿全屏的話,我們應該使用UIViewController自己創建和維護tableView。
UITableViewController提供一個初始化函數initWithStyle:,根據需要我們可以創建Plain或者Grouped類型的tableView,當我們使用其從UIViewController繼承來的init初始化函數的時候,默認將會我們創建一個Plain類型的tableView。
UITableViewController默認的會在viewWillAppear的時候,清空所有選中cell,我們可以通過設置self.clearsselectionOnViewWillAppear = NO,來禁用該功能,并在viewDidAppear中調用UIScrollView的FlashScrollIndicators方法讓滾動條閃動一次,從而提示用戶該控件是可以滑動的。
三、UITableViewCell介紹
UITableView中顯示的每一個單元都是一個UITableViewCell對象,看文檔的話我們會發現其初始化函數initWithStyle:reuseIdentifier:比較特別,跟我們平時看到的UIView的初始化函數不同。這個主要是為了效率考慮,因為在tableView快速滑動的滑動的過程中,頻繁的alloc對象是比較費時的,于是引入了cell的重用機制,這個也是我們在dataSource中要重點注意的地方,用好重用機制會讓我們的tableView滑動起來更加流暢。
我們可以通過cell的selectionStyle屬性指定cell選中時的顯示風格,以及通過accessoryType來指定cell右邊的顯示的內容,或者直接指定accessoryView來定制右邊顯示的view。
系統提供的UITableView也包含了四種風格的布局,分別是:
typedef enum { UITableViewCellStyleDefault, UITableViewCellStyleValue1, UITableViewCellStyleValue2, UITableViewCellStyleSubtitle} UITableViewCellStyle;
這幾種文檔中都有詳細描述,這兒就不在累贅。然而可以想象系統提供的只是最常用的幾種類型,當系統提供的風格不符合我們需要的時候,我們就需要對cell進行定制了,有以下兩種定制方式可選:
1、直接向cell的contentView上面添加subView
這是比較簡單的一種的,根據布局需要我們可以在不同的位置添加subView。但是此處需要注意:所有添加的subView都最好設置為不透明的,因為如果subView是半透明的話,view圖層的疊加將會花費一定的時間,這會嚴重影響到效率。同時如果每個cell上面添加的subView個數過多的話(通常超過3,4個),效率也會受到比較大的影響。
下面我們看一個例子:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ NSArray *sections = [SvTableViewDataModal sections]; SvSectionModal *sectionModal = [sections objectAtIndex:indexPath.section]; static NSString *reuseIdetify = @"SvTableViewCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdetify]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdetify]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; cell.showsReorderControl = YES; for (int i = 0; i < 6; ++i) { UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100 + 15 * i, 0, 30, 20)]; label.backgroundColor = [UIColor redColor]; label.text = [NSString stringWithFormat:@"%d", i]; [cell.contentView addSubview:label]; [label release]; } } cell.textLabel.backgroundColor = [UIColor clearColor]; cell.textLabel.text = [sectionModal.cityNames objectAtIndex:indexPath.row]; return cell;}
在上面這個例子中,我往每個cell中添加了6個subView,而且每個subView都是半透明(UIView默認是半透明的),這個時候滑動起來明顯就可以感覺到有點顫抖,不是很流暢。當把每一個subView的opaque屬性設置成YES的時候,滑動會比之前流暢一些,不過還是有點兒卡。
2、從UITableViewCell派生一個類
通過從UITableViewCell中派生一個類,可以更深度的定制一個cell,可以指定cell在進入edit模式的時候如何相應等等。最簡單的實現方式就是將所有要繪制的內容放到一個定制的subView中,并且重載該subView的drawRect方法直接把要顯示的內容繪制出來(這樣可以避免subView過多導致的性能瓶頸),最后再將該subView添加到cell派生類中的contentView中即可。但是這樣定制的cell需要注意在數據改變的時候,通過手動調用該subView的setNeedDisplay方法來刷新界面,這個例子可以在蘋果的幫助文檔中的TableViewSuite工程中找到,這兒就不舉例了。
觀看這兩種定制cell的方法,我們會發現subView都是添加在cell的contentView上面的,而不是直接加到cell上面,這樣寫也是有原因的。下面我們看一下cell在正常狀態下和編輯狀態下的構成圖:
cell在正常狀態下的構成圖如下:
進入編輯狀態下cell的構成圖如下:
通過觀察上面兩幅圖片我們可以看出來,當cell在進入編輯狀態的時候,contentView會自動的縮放來給Editing control騰出位置。這也就是說如果我們把subView添加到contentView上,如果設置autoresizingMask為更具父view自動縮放的話,cell默認的機制會幫我們處理進入編輯狀態的情況。而且在tableView是Grouped樣式的時候,會為cell設置一個背景色,如果我們直接添加在cell上面的話,就需要自己考慮到這個背景色的顯示問題,如果添加到contentView上,則可以通過view的疊加幫助我們完成該任務。綜上,subView最好還是添加到cell的contentView中。
四、Reordering
為了使UITableVeiew進入edit模式以后,如果該cell支持reordering的話,reordering控件就會臨時的把accessaryView覆蓋掉。為了顯示reordering控件,我們必須將cell的showsReorderControl屬性設置成YES,同時實現dataSource中的tableView:moveRowAtIndexPath:toIndexPath:方法。我們還可以同時通過實現dataSource中的 tableView:canMoveRowAtIndexPath:返回NO,來禁用某一些cell的reordering功能。
下面看蘋果官方的一個reordering流程圖:
上圖中當tableView進入到edit模式的時候,tableView會去對當前可見的cell逐個調用dataSource的tableView:canMoveRowAtIndexPath:方法(此處官方給出的流程圖有點兒問題),決定當前cell是否顯示reoedering控件,當開始進入拖動cell進行拖動的時候,每滑動過一個cell的時候,會去掉用delegate的tableView:targetIndexPathForMoveFromRowAtIndexPath:toPRoposedIndexPath:方法,去判斷當前劃過的cell位置是否可以被替換,如果不行則給出建議的位置。當用戶放手時本次reordering操作結束,調用dataSource中的tableView:moveRowAtIndexPath:toIndexPath:方法更新tableView對應的數據。
此處給個我寫demo中的更新數據的小例子:
// if you want show reordering control, you must implement moveRowAtndexPath, or the reordering control will not show // when use reordering end, this method is invoke - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{ // update DataModal NSArray *sections = [SvTableViewDataModal sections]; SvSectionModal *sourceSectionModal = [sections objectAtIndex:sourceIndexPath.section]; NSString *city = [[sourceSectionModal.cityNames objectAtIndex:sourceIndexPath.row] retain]; [sourceSectionModal.cityNames removeObject:city]; [SvTableViewDataModal replaceSectionAtIndex:sourceIndexPath.section withSection:sourceSectionModal]; SvSectionModal *desinationsSectionModal= [[SvTableViewDataModal sections] objectAtIndex:destinationIndexPath.section]; [desinationsSectionModal.cityNames insertObject:city atIndex:destinationIndexPath.row]; [SvTableViewDataModal replaceSectionAtIndex:destinationIndexPath.section withSection:desinationsSectionModal]; [city release];}
上面代碼中首先拿到源cell所處的section,然后從該section對應的數據中移除,然后拿到目標section的數據,然后將源cell的數據添加到目標section中,并更新回數據模型,如果我們沒有正確更新數據模型的話,顯示的內容將會出現異常。
五、Delete & Insert
cell的delete和insert操作大部分流程都是一樣的,當進入編輯模式的時候具體的顯示是delete還是insert
取決與該cell的editingStyle的值,editStyle的定義如下:
typedef enum { UITableViewCellEditingStyleNone, UITableViewCellEditingStyleDelete, UITableViewCellEditingStyleInsert} UITableViewCellEditingStyle;
當tableView進入編輯模式以后,cell上面顯示的delete還是insert除了跟cell的editStyle有關,還與 tableView的delegate的tableView:editingStyleForRowAtIndexPath:方法的返回值有關(在這里嘮叨一句,其實delegate提供了很多改變cell屬性的機會,如非必要,還是不要去實現這些方法,因為執行這些方法也造成一定的開銷)。
delete和insert的流程如下蘋果官方文檔中給出的圖所示:
下面是我寫的demo中刪除和添加部分的代碼:
#pragma mark -- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{ NSLog(@"commit editStyle: %d", editingStyle); if (editingStyle == UITableViewCellEditingStyleDelete) { NSArray *sections = [SvTableViewDataModal sections]; SvSectionModal *sourceSectionModal = [sections objectAtIndex:indexPath.section]; [sourceSectionModal.cityNames removeObjectAtIndex:indexPath.row]; [SvTableViewDataModal replaceSectionAtIndex:indexPath.section withSection:sourceSectionModal]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight]; } else { // do something for add it NSArray *sections = [SvTableViewDataModal sections]; SvSectionModal *sourceSectionModal = [sections objectAtIndex:indexPath.section]; [sourceSectionModal.cityNames insertObject:@"new City" atIndex:indexPath.row]; [SvTableViewDataModal replaceSectionAtIndex:indexPath.section withSection:sourceSectionModal]; [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight]; }}
代碼中首先判斷當前操作是delete操作還是insert操作,相應的更新數據,最后根據情況調用tableView的insertRowsAtIndexPaths:withRowAnimation:或者deleteRowsAtIndexPaths:withRowAnimation:方法,對tableView的視圖進行更新。cell的刪除和添加操作相對還是比較簡單的。
六、Cell的Select操作
當我們在tableView中點擊一個cell的時候,將會調用tableView的delegate中的tableView:didSelectRowAtIndexPath:方法。
關于tableView的cell的選中,蘋果官方有以下幾個建議:
1、不要使用selection來表明cell的選擇狀態,而應該使用accessaryView中的checkMark或者自定義accessaryView來顯示選中狀態。
2、當選中一個cell的時候,你應該取消前一個cell的選中。
3、如果cell選中的時候,進入下一級viewCOntroller,你應該在該級菜單從navigationStack上彈出的時候,取消該cell的選中。
這塊兒再提一點,當一個cell的accessaryType為UITableViewCellAccessoryDisclosureIndicator的時候,點擊該accessary區域通常會將消息繼續向下傳遞,即跟點擊cell的其他區域一樣,將會掉delegate的tableView:didSelectRowAtIndexPath:方法,當時如果accessaryView為 UITableViewCellAccessoryDetailDisclosureButton的時候,點擊accessaryView將會調用delegate的 tableView:accessoryButtonTappedForRowWithIndexPath:方法。
七、批量插入,刪除,部分更新操作
UITableView提供了一個批量操作的特性,這個功能在一次進行多個row或者scetion的刪除,插入,獲取更新多個cell內容的時候特別好用。所有的批量操作需要包含在beginUpdates和endUpdates塊中,否則會出現異常。
下面請看我demo中的一個批量操作的例子:
- (void)groupEdit:(UIBarButtonItem*)sender{ [_tableView beginUpdates]; // first update the data modal [_tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]] withRowAnimation:UITableViewRowAnimationTop]; [_tableView deleteSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationTop]; [SvTableViewDataModal deleteSectionAtIndex:0]; SvSectionModal *section = [[SvTableViewDataModal sections] objectAtIndex:0]; [section.cityNames insertObject:@"帝都" atIndex:0]; [SvTableViewDataModal replaceSectionAtIndex:0 withSection:section]; [_tableView endUpdates];}
上面的例子中我們可以看到先往tableView的第0個section的第0行添加一個cell,然后將第0個section刪掉。按照我們程序中寫的順序,那么新添加進去的“帝都”,將不在會顯示,因為包含它的整個section都已經被刪除了。
執行程序前后結果如下圖:
demo中第0個section是陜西省的城市,第1個section是北京。左邊是執行前的截圖,右邊是執行后的截圖,觀察發現結果并不像我們前面推測的那樣。那是因為在批量操作時,不管代碼中先寫的添加操作還是刪除操作,添加操作都會被推出執行,直到這個塊中所有的刪除操作都執行完以后,才會執行添加操作,這也就是上面蘋果官方圖片上要表達的意思。
蘋果官方文檔有一副圖可以幫助我們更好的理解這一點:
原圖中操作是:首先刪除section 0中的row 1,然后刪除section 1,再向section 1中添加一行。執行完批量更新以后就得到右半邊的結果。
八、IndexList
當我們tableView中section有很多,數據量比較大的時候我們可以引入indexList,來方便完成section的定位,例如系統的通訊錄程序。我們可以通過設置tableView的sectionIndexMinimumDisplayRowCount屬性來指定當tableView中多少行的時候開始顯示IndexList,默認的設置是NSIntegerMax,即默認是不顯示indexList的。
為了能夠使用indexlist我們還需要實現dataSource中一下兩個方法:
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView; - (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index;
第一個方法返回用于顯示在indexList中的內容的數組,通常為A,B,C...Z。第二個方法的主要作用是根據用戶在indexList中點擊的位置,返回相應的section的index值。這個例子可以在蘋果官方給出的TableViewSuite中找到,實現起來還是很簡單的。
九、其他
1、分割線
我們可以通過設置tableView的separatorStyle屬性來設置有無分割線以及分割線的風格,其中style定義如下:
typedef enum { UITableViewCellSeparatorStyleNone, UITableViewCellSeparatorStyleSingleLine, UITableViewCellSeparatorStyleSingleLineEtched} UITableViewCellSeparatorStyle;
同時還可以通過tableView的separatorColor屬性來設置分割線的顏色。
2、如何提高tableView的性能
a、重用cell
我們都知道申請內存是需要時間,特別是在一段時間內頻繁的申請內存將會造成很大的開銷,而且上tebleView中cell大部分情況下布局都是一樣的,這個時候我們可以通過回收重用機制來提高性能。
b、避免content的重新布局
盡量避免在重用cell時候,對cell的重新布局,一般情況在在創建cell的時候就將cell布局好。
c、使用不透明的subView
在定制cell的時候,將要添加的subView設置成不透明的會大大減少多個view層疊加時渲染所需要的時間。
d、如果方便,直接重載subView的drawRect方法
如果定制cell的過程中需要多個小的元素的話,最好直接對要顯示的多個項目進行繪制,而不是采用添加多個subView。
e、tableView的delegate的方法如非必要,盡量不要實現
tableView的delegate中的很多函數提供了對cell屬性的進一步控制,比如每個cell的高度,cell是否可以編輯,支持的edit風格等,如非必要最好不要實現這些方法因為快速的調用這些方法也會影響性能。
新聞熱點
疑難解答