今天講的是TableViews,它可用於呈現動態數據列表,也可用於靜態數據。
UITableView
tableView是個一維表,這是一個UIScrollView的子類,所以它是一個滾動列表。它可以高度定制化,它從它的兩個不同的delegation中獲取所有的定制化信息,有data source和delegate這兩個不同的properties,data source負責提供表中的數據,delegate負責數據顯示。如果想顯示多維數據,就是有行和列,可以使用sections或者可以把它放進一個navigation controller。
這個是plain風格的tableVeiw的樣子,頂部的F是section header,底部的東西是tab bar controller,和tableVeiw沒關系。
這是group風格,group風格往往是為固定tableVeiw使用。
listView往往用來查詢,查看動態數據。
描述plain風格的tableVeiw的各個部分的術語:

最頂上的東西叫header,那是一個UIView,可以添加到表中,它可以是任何你想要的東西。在底部有個footer,也是個UIView。這些被分組的東西叫section,藍條叫section header,可以用字符串設置或者可以用view,表中的每個row都是一個UIView,叫這個UIView為tableVeiw cell。
使用完全相同的術語描述group風格的tableVeiw:

當有section時,建立tableVeiw並實現data source時得告訴tableVeiw有多少section,然后它會問你每個section里有多少row。
cell有四個基本顯示類型:

Subtitle有粗體的標題,然后下面有灰色的副標題;basic類型就是下面沒有東西;right detail和subtitle一樣,只是東西的排列不同,這是側面和藍色的;left detail也一樣,只是左右換一下。
創建TableView MVC
tableVeiw來自xcode里的一個UITableViewController類,所以ios有controller的類,然后還有view的類,把它們作為一個單元從object library里拖出來。通常不會在一個通用的UITableViewController里用這個東西,通常會子類它,讓子類controller成為delegate里的data source還有實現這些方法。這就是讓tableVeiw做你想做的事情。
如何創建一個新類並使這個TableViewController不是一個通用的UITableViewController?去new file點擊UIViewController,接着得確保你設置你自定義的controller類的父類為tableVeiw,ios中的UITableViewController類做了一些事情來幫助你的tableVeiw掛接到你的子類,然后還要確保在storyboard,你inspect該controller的identity inspect,並設置了正確的類。
在選中cell時,可以控制出現在cell右側的小東西即accessory,accessory為Detail Disclosure Accessory時,把藍色小按鈕連接起來的方式是你的tableVeiw delegate,你得實現這個方法:
- (void)tableView:(UITableView *)tv accessoryTappedForRowAtIndexPath:(NSIndexPath *)ip;
當有人點擊藍色小按鈕,這個方法會被調用。
在做動態時,有個非常重要的區域叫做reuse identifier,你需要在代碼中指定,它才知道要創建的副本的原型是什么。為什么它還需要該字符串?可能有多個場景或tableVeiw,你拖動的UITableViewController實例來自同一個自定義子類,但它們可能有不同的cell原型,因此為cell命名。通常情況下,我們會把reuse identifier用來形容這個cell是什么。
UITableViewDataSource
這一切是如何工作的?如何得到這個UI?數據是如何來回流動的?這些都是通過protocols。tableVeiw有兩種不同的delegate,一個叫delegate,一個叫data source,它們都是protocols。UITableViewController類會自動設置內部tableVeiw的delegate和data source,因此當我們拖出TableViewController,它已經有一個tableVeiw了,子類controller是默認的delegate和data source。這幾乎總是你使用tableVeiw的方式。為什么做這個delegation?因為view不能和它們的controller對話,除了通過不可見通訊,也就是protocol,通過protocol可以來回發消息。所以tableVeiw是這個controller的view,它只能回應target action或delegate的對話,UITableViewController有個property指向這個tableVeiw。
要成為動態的,要實現此data source protocol。那么在這個data source protocol里都有什么方法?有三個要實現的非常重要的方法,一個是表明表里有多少section,二是每個section有多少row,第三個是返回要繪制的每個row的UITableView cell。來看最后一個方法,這是該方法的的樣子:
- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath { // get a cell to use (instance of UITableViewCell) UITableViewCell *cell; cell = [self.tableView dequeueReusableCellWithIdentifier:@“My Table View Cell”]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@“My Table View Cell”]; } cell.textLabel.text = [self getMyDataForRow:indexPath.row inSection:indexPath.section]; return cell; }
tableVeiw把自己作為第一個參數傳遞,然后第二個參數是一個indexPath。靜態的cell不用實現這個方法。NSIndexPath要做的就是封裝section和row,因此它有兩個屬性,一個叫section,一個叫row,section會告訴你當前是什么section,row會告訴你這個是當前section里的哪個row,因此這個方法只是說,給我一個用來畫這個section里的這個row的UITableView。
這個方法中的代碼通常有兩部分:第一部分,讓自己得到一個cell,然后設置cell里的property,tableVeiw有個神奇的方法叫做dequeueReusableCellWithIdentifier,這是為了效率,tableVeiw就像一個管理這些UITableViewCell的池子,當UITableVeiw離開屏幕,它就把它們放進池子,然后其中一個需要去到屏幕上時,它就進入池中找出一個來,這就是它是如何重用它們。當它們進出屏幕,我們只是一直在重用和復位,有關重用,reuse identifier指定了要用的池子的名字,當我們做了xcode原型cell,這里我們鍵入它的名字,因為當你做一個xcode的原型cell,如果它到達到重用池而池子是空的,比如第一次啟動的時候,它會創建一個,並用原型把它放進去,這就是原型cell的作用,當重用池是空的時候,它會填進去,只要它是空的,就由原型的副本填充。所以這個字符串必須和xcode里的一樣,如果你想要填充原型的話。如果這里返回nil,會發生什么?我們沒有指定與xcode中相同的字符串,因此它不能使用原型副本,所以不能得到任何東西,它返回nil。接着會放些安全代碼在這,alloc/init一個cell。
接下來只要設置property,比如cell有個property叫做text label,這里寫了個方法getMyDataForRow:inSection:可以用來獲取字符串之類的事情。然后返回這個cell。可以有交替的cell機制,但需要兩個不同的池。
在tableVeiw中要有多少section和row,它有兩個簡單的方法,問它的data source這個tableVeiw里有多少section和這個section里有多少row,你只要回答這些問題:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)sender;
- (NSInteger)tableView:(UITableView *)sender numberOfRowsInSection:(NSInteger)section;
通常是沒有section的,也就是整體就是一個大的section。但是section里的row數量沒有默認值,你必須給出section有多少row。靜態表不必實現任何這些方法。
UITableViewDelegate
UITableView delegate控制如何繪制表,不是表中的數據,而是如何顯示,比如像cell多高之類。常常data source和delegate是同一個對象,是這個UITableViewController,delegate有很多did/will happen方法,最重要的是它會通知你,當有人點擊row的時候。
當有人點擊row,我們可以做兩件事:一是segueing,可以control drag一個row,甚至是prototype row,到其他東西,然后segue。如果從prototype cell處control drag,所有的cell都會做一樣的事,所以我們就必須確保並根據選擇的row准備segue的viewController,該cell被點擊了,並得到一個delegate方法,如果不做segue或自己想做segue,就用手動segue這個方法。每當cell被點擊didSelectRowAtIndexPath都會被調用,它會傳遞indexPath,你基於給予的信息做些什么:
- (void)tableView:(UITableView *)sender didSelectRowAtIndexPath:(NSIndexPath *)path { // go do something based on information // about my data structure corresponding to indexPath.row in indexPath.section }
Table View Segues
如果表有個原型cell,直接control drag到一些其他的viewController,那么會問想要什么樣的segue。當你prepare for segue,所以你在做segue,prepareForSegue會被發送到你的TableViewController,你要准備segueing的東西:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { NSIndexPath *indexPath = [self.tableView indexPathForCell:sender]; // prepare segue.destinationController to display based on information // about my data structure corresponding to indexPath.row in indexPath.section }
通常會使用indexPathForCell這個方法,因為prepareForSegue里的sender是這個cell,被點擊的UITableViewCell,所以通常會調用indexPathForCell得到indexPath,基於被點擊的row,去model里查找要傳遞的數據。
如果model改變了呢?可以調用方法reloadData,reloadData重新加載整個表,它會知道表里有多少section及每個section有多少row,它會為所有的section調用此方法,然后它會為每個可見的cell調用cellForRowAtIndexPath:方法,所以reloadData不是輕量級的。
Demo
做一個計算器的例子,主要包括:
1.在NSUserDefaults存儲一個property list;
2.建立一個UITableViewController及其自定義子類;
3.實現data source protocol;
4.創建一個新的delegate,在delegate實現的地方做一件事:如果有一個popover,通過popover放一個viewController,在popover里發生了什么,它需要回過去和controller通信,它不能直接和controller通信,由於popover是view的一部分,view不能直接回應它的controller,它必須使用delegation;
5.在graph view里添加一個按鈕,它要做的是拿起這個graph,把它添加到NSUserDefaults里的一個列表里;
6.在graph view里添加另一個按鈕,它要使用popover segue帶來了一個全新的MVC,這是一個tableVeiw驅動的MVC,就是popover里有一個表,表里是一個其他所有favorite program的列表,當你點擊其中一個,它會更新graph,顯示favorite graph。

CalculatorGraphViewController.m文件的代碼:
#import "CalculatorGraphViewController.h" #import "CalculatorBrain.h" #import "CalculatorProgramsTableViewController.h" @interface CalculatorGraphViewController() <CalculatorProgramsTableViewControllerDelegate> @property (nonatomic, strong) UIPopoverController *popoverController; // added after lecture to prevent multiple popovers @end @implementation CalculatorGraphViewController @synthesize popoverController; #define FAVORITES_KEY @"CalculatorGraphViewController.Favorites" - (IBAction)addToFavorites:(id)sender { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; NSMutableArray *favorites = [[defaults objectForKey:FAVORITES_KEY] mutableCopy]; if (!favorites) favorites = [NSMutableArray array]; [favorites addObject:self.calculatorProgram]; [defaults setObject:favorites forKey:FAVORITES_KEY]; [defaults synchronize]; } - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"Show Favorite Graphs"]) { // this if statement added after lecture to prevent multiple popovers // appearing if the user keeps touching the Favorites button over and over // simply remove the last one we put up each time we segue to a new one if ([segue isKindOfClass:[UIStoryboardPopoverSegue class]]) { UIStoryboardPopoverSegue *popoverSegue = (UIStoryboardPopoverSegue *)segue; [self.popoverController dismissPopoverAnimated:YES]; self.popoverController = popoverSegue.popoverController; // might want to be popover's delegate and self.popoverController = nil on dismiss? } NSArray *programs = [[NSUserDefaults standardUserDefaults] objectForKey:FAVORITES_KEY]; [segue.destinationViewController setPrograms:programs]; [segue.destinationViewController setDelegate:self]; } } - (void)calculatorProgramsTableViewController:(CalculatorProgramsTableViewController *)sender choseProgram:(id)program { self.calculatorProgram = program; // if you wanted to close the popover when a graph was selected // you could uncomment the following line // you'd probably want to set self.popoverController = nil after doing so // [self.popoverController dismissPopoverAnimated:YES]; [self.navigationController popViewControllerAnimated:YES]; // added after lecture to support iPhone } // added after lecture to support deletion from the table // deletes the given program from NSUserDefaults (including duplicates) // then resets the Model of the sender - (void)calculatorProgramsTableViewController:(CalculatorProgramsTableViewController *)sender deletedProgram:(id)program { NSString *deletedProgramDescription = [CalculatorBrain descriptionOfProgram:program]; NSMutableArray *favorites = [NSMutableArray array]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; for (id program in [defaults objectForKey:FAVORITES_KEY]) { if (![[CalculatorBrain descriptionOfProgram:program] isEqualToString:deletedProgramDescription]) { [favorites addObject:program]; } } [defaults setObject:favorites forKey:FAVORITES_KEY]; [defaults synchronize]; sender.programs = favorites; } @end
在storyboard中拖出TableViewController,接着創建一個自定義子類,這是一個UITableViewController子類,叫做CalculatorProgramsTableViewController,接下來在storyboard里去到identity inspector為TableViewController指定類為CalculatorProgramsTableViewController。
創建一個按鈕segue到TableViewController,拖出一個barButton到graphView,然后從它control drag到TableViewController,選擇popover segue,設置它的identifier為Show Favorite Graphs。
在自定義的TableViewController里面,設置cell屬性,將style修改為basic,將identifier設為Calculator Program Description。如果不希望TableViewController出現在popover時太大,需要選中這個controller,使popover屬性是200x200。
delegation的5個步驟:
1.創建protocol,是會被用來描述protocol,還有要干嘛;
2.添加property,不管是data source或delegate,通常都在公共接口里;
3.在delegator的實現內部使用這個delegate property,它需要那里的信息或它要和其他對象通信;
4.要在delegate里設置delegate property,是delegate而不是delegator,是接收這些消息的人;
5.它需要設置自己為delegate,它需要實現protocol里它需要的方法。
CalculatorProgramsTableViewController.h文件的代碼:
#import <UIKit/UIKit.h> @class CalculatorProgramsTableViewController; @protocol CalculatorProgramsTableViewControllerDelegate <NSObject> // added <NSObject> after lecture so we can do respondsToSelector: on the delegate @optional - (void)calculatorProgramsTableViewController:(CalculatorProgramsTableViewController *)sender choseProgram:(id)program; - (void)calculatorProgramsTableViewController:(CalculatorProgramsTableViewController *)sender deletedProgram:(id)program; // added after lecture to support deleting from table @end @interface CalculatorProgramsTableViewController : UITableViewController @property (nonatomic, strong) NSArray *programs; // of CalculatorBrain programs @property (nonatomic, weak) id <CalculatorProgramsTableViewControllerDelegate> delegate; @end
CalculatorProgramsTableViewController.m文件的代碼:
#import "CalculatorProgramsTableViewController.h" #import "CalculatorBrain.h" @implementation CalculatorProgramsTableViewController @synthesize programs = _programs; @synthesize delegate = _delegate; // added after lecture to be sure table gets reloaded if Model changes // you should always do this (i.e. reload table when Model changes) // the Model getting out of synch with the contents of the table is bad - (void)setPrograms:(NSArray *)programs { _programs = programs; [self.tableView reloadData]; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { return YES; } #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.programs count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Calculator Program Description"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell... id program = [self.programs objectAtIndex:indexPath.row]; cell.textLabel.text = [@"y = " stringByAppendingString:[CalculatorBrain descriptionOfProgram:program]]; return cell; } // this method added after lecture to support deletion // simply delegates deletion - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { id program = [self.programs objectAtIndex:indexPath.row]; [self.delegate calculatorProgramsTableViewController:self deletedProgram:program]; } } // added after lecture // don't allow deletion if the delegate does not support it too! - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return [self.delegate respondsToSelector:@selector(calculatorProgramsTableViewController:deletedProgram:)]; } #pragma mark - UITableViewDelegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { id program = [self.programs objectAtIndex:indexPath.row]; [self.delegate calculatorProgramsTableViewController:self choseProgram:program]; } @end
