你也许已经注意到了,自从我们加入了Table View Controller之后,Xcode便会现实下面这样一条警告。
这条警告是:“Unsupported Configuration: Prototype table cells must have reuse identifiers”意思是,原型表格单元必须有一个身份证(意译啦)
原型单元格是另一个Storyboard的好特性之一。在之前,如果你想要自定义一个Table Cell,那么你就不得不用代码来实现,要么就要单独创建一个Nib文件来表示单元格内容,现在你也可以这样做,不过原型单元格可以帮你把这一过程大大的简化,你现在可以直接在Storyboard设计器中完成这一过程。
Table View现在默认的会带有一个空白的原型单元格,选中他,在属性控制器中将他的Style改为subtitle,这样的话,每一格就会有两行字。
将附件设置为Disclosure Indicator并且将这个原型单元格的Reuse Identifier 设置喂“PlayerCell”,这将会解决Xcode所警告的问题。
试着运行一个,发现什么都没变,这并不奇怪,因为我们还没有给这个表格设置一个数据来源(DataSource),用以显示。
新建一个文件,使用UIViewContoller模板,命名为 PlayersViewController ,设置喂UITableViewController的子类,不要勾选建立XIB文件。
回到Storyboard编辑器,选择Table View Controller,在身份控制器中,把他的类设置为PlayerViewController,这对于把Storyboard中的场景和你自定义的子类挂钩是十分重要的。要是不这么做,你的子类根本没用。
现在起,当你运行这个应用时,table view controller其实是PlayersViewContoller的一个实例。
在 PlayersViewController.h 中声明一个MutableArray(可变数组)
#import <UIKit/UIKit.h> @interface PlayersViewController : UITableViewController @property (nonatomic, strong) NSMutableArray *players; @end |
这个数组将会包含我们的应用的主要数据模型。我们现在加一些东西到这个数组之中,新建一个使用Obj-c模板的文件,命名为player,设置喂NSObject的子类,这将会作为数组的数据容器。
编写Player.h如下:
@interface Player : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, copy) NSString *game; @property (nonatomic, assign) int rating; @end |
编写Player.m如下:
#import "Player.h" @implementation Player @synthesize name; @synthesize game; @synthesize rating; @end |
这里没有什么复杂的,Player类只是一个容器罢了,包含三个内容:选手的名字、项目和他的评级。
接下来我们在App Delegate中声明数组和一些Player对象,并把他们分配给PlayerViewController的players属性。
在AppDelegate.m中,分别引入(import)Player和PlayerViewController这两个类,之后新增一个名叫players的可变数组。
#import "AppDelegate.h" #import "Player.h" #import "PlayersViewController.h" @implementation AppDelegate { NSMutableArray *players; } // Rest of file... |
修改didFinishLaunchingWithOptions方法如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { players = [NSMutableArray arrayWithCapacity:20]; Player *player = [[Player alloc] init]; player.name = @"Bill Evans"; player.game = @"Tic-Tac-Toe"; player.rating = 4; [players addObject:player]; player = [[Player alloc] init]; player.name = @"Oscar Peterson"; player.game = @"Spin the Bottle"; player.rating = 5; [players addObject:player]; player = [[Player alloc] init]; player.name = @"Dave Brubeck"; player.game = @"Texas Hold’em Poker"; player.rating = 2; [players addObject:player]; UITabBarController *tabBarController = (UITabBarController *)self.window.rootViewController; UINavigationController *navigationController = [[tabBarController viewControllers] objectAtIndex:0]; PlayersViewController *playersViewController = [[navigationController viewControllers] objectAtIndex:0]; playersViewController.players = players; return YES; } |
这将会创造一些Player对象并把他们加到数组中去。之后在加入:
UITabBarController *tabBarController = (UITabBarController *) self.window.rootViewController; UINavigationController *navigationController = [[tabBarController viewControllers] objectAtIndex:0]; PlayersViewController *playersViewController = [[navigationController viewControllers] objectAtIndex:0]; playersViewController.players = players; |
咦,这是什么?目前的情况是:我们希望能够将players数组连接到PlayersViewController的players属性之中以便让这个VC能够用做数据来源。但是app delegate根本不了解PlayerViewController究竟是什么,他将需要在storyboard中寻找它。
这是一个我不是很喜欢storyboard特性,在IB中,你在MainWindow.xib中总是会有一个指向App delegate的选项,在那里你可以在顶级的ViewController中向Appdelegate设置输出口,但是在Storyboard中目前这还不可能,目前只能通过代码来做这样的事情。
UITabBarController *tabBarController = (UITabBarController *) self.window.rootViewController; |
我们知道storyboard的起始场景是Tab Bar Controller,所以我们可以直接到这个场景的第一个子场景来设置数据源。
PlayersViewController 在一个NavController的框架之中,所以我们先看一看UINavigationController类:
UINavigationController *navigationController = [[tabBarController viewControllers] objectAtIndex:0]; |
然后询问它的根试图控制器,哪一个是我们要找的PlayersViewController:
PlayersViewController *playersViewController = [[navigationController viewControllers] objectAtIndex:0]; |
但是,UIViewController根本就没有一个rootViewController属性,所以我们不能把数组加入进去,他又一个topViewController但是指向最上层的视图,与我们这里的意图没有关系。
现在我们有了一个装在了players物体合集的数组,我们继续为PlayersViewController设置数据源。
打开PlayersViewController.m,加入以下数据源方法:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.players count]; } |
真正起作用的代码在cellForRowAtIndexPath方法里,默认的模板是如下这样的:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell... return cell; } |
无疑这就是以前设置一个表格视图的方法,不过现在已经革新了,把这些代码修改如下:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PlayerCell"]; Player *player = [self.players objectAtIndex:indexPath.row]; cell.textLabel.text = player.name; cell.detailTextLabel.text = player.game; return cell; } |
这看上去简单多了,为了新建单元格,你只需使用如下代码:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PlayerCell"]; |
如果没有现存的单元格可以回收,程序会自动创造一个原型单元格的复制品之后返回给你,你只需要提供你之前在Storyboard编辑视图中设置的身份证就可以的,在这里就是“PlayerCell”,如果不设置这个,这个程序就无法工作。
由于这个类对于Player容器目前一无所知,所以我们需要在文件的开头加入一个引入来源
#import "Player.h" |
记得要创建synthesize语句哦亲
@synthesize players; |
现在运行应用,会看到Table里有着players容器。
请注意:我们这里只使用一种单元格原型,如果你需要使用不同类型的单元格的话,只需要在storyboard中另外加入一个单元格原型就可以了,不过不要忘记给他们指派不同的身份证。
对于很多应用来说,使用默认的单元格风格就OK了,但是我偏偏要在每一个单元格的右边加上一个一个图片来表示选手的评级,但是添加图片对于默认类型的单元格来说并不支持,我们需要自定义一个设计。
让我们转回MainStoryboard.storyboard,选中table view中的prototype cell,把它的Style attribute改为Custom,所有默认的标签都会消失。
首先把单元格变得更高一些,你可以直接拉它,也可以在大小控制器中修改数字,我在这里使用55点的高度。
从 Objects Library中拖出两个标签物体,按照之前的样式安插到单元格里,记得设置label的Highlighted颜色为白色,那样的话当单元格被选中的时候会看起来更好看一些。
之后添加一个Image View对象,将它放置在单元格的右边,设置他的宽度为81点,高度并不重要,在属性检查器中设置模式为置中。
我把标签设置为210点长以确保他不会和ImageView重合,最后整体的设计会看起来象下面这样:
由于这是一个自定义的单元格,所以我们不能够使用UITableView默认的textLabel和detailLabel来设置数据,这些属性也不再指向我们的单元格了,我们使用标签(tags)来指定标签。
将Name标签的tag设置为100,Game的设置喂101,image的设置喂102,在属性检查器里设置哦亲。
之后打开 PlayersViewController.m ,在PlayersViewcontroller中将cellForRowatIndexPath修改为:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PlayerCell"]; Player *player = [self.players objectAtIndex:indexPath.row]; UILabel *nameLabel = (UILabel *)[cell viewWithTag:100]; nameLabel.text = player.name; UILabel *gameLabel = (UILabel *)[cell viewWithTag:101]; gameLabel.text = player.name; UIImageView * ratingImageView = (UIImageView *) [cell viewWithTag:102]; ratingImageView.image = [self imageForRating:player.rating]; return cell; } |
这里是用了一个新的方法,叫做ImageRating,在 cellForRowAtIndexPath方法之前加入这个方法:
- (UIImage *)imageForRating:(int)rating { switch (rating) { case 1: return [UIImage imageNamed:@"1StarSmall.png"]; case 2: return [UIImage imageNamed:@"2StarsSmall.png"]; case 3: return [UIImage imageNamed:@"3StarsSmall.png"]; case 4: return [UIImage imageNamed:@"4StarsSmall.png"]; case 5: return [UIImage imageNamed:@"5StarsSmall.png"]; } return nil; } |
这就完成了,运行看看:
这和我们想象的结果并不是很符合,我们修改了原型单元格的属性和高度,但是table view却没有考虑进去,有两种方法可以修复它,我们可以改变table view的行高或者加入 heightForRowAtIndexPath 方法来修改,地一种方法更简单,我们就用他。
注意:在一下两种情况下,你应该使用 heightForRowAtIndexPath 方法:一是,你不能预先知道你的单元格的高度,二是不同的单元格会有不同的高度。
回到MainStoryboard.storyboard,在大小检查器中将高度设置为55:
通过这种方式的话,如果之前你是使用拖动而不是键入数值的方式改变高度的属性的话,则table view的数值也会自动改变。
现在运行看看,好多了吧