iOS Storyboard全解析
来源:http://iaiai.iteye.com/blog/1493956
Storyboard)是一个能够节省你很多设计手机App界面时间的新特性,下面,为了简明的说明Storyboard的效果,我贴上本教程所完成的Storyboard的截图:
现 在,你就可以清楚的看到这个应用究竟是干些什么的,也可以清楚的看到其中的各种关系,这就是Storyboard的强大之处了。如果你要制作一个页面很多 很复杂的App,Storyboard可以帮助你解决写很多重复的跳转方法的麻烦,节省很多时间,以便你能够完全的专注于核心功能的实现上。
开始
首先启动Xcode,新建一个工程,我们在这里使用Single View App Template,这个模板会提供一个类和一个Storyboard,免去我们自己创建的麻烦。
创建完成之后,Xcode的界面大概是这样的:
这 个新的工程由两个类:AppDelegate和ViewController以及一个Storyboard组成(如果你选择了两个设备会有两个 Storyboard),注意这个项目没有xib文件,让我们首先看看Storyboard是什么样的,双击Storyboard打开他:
Storyboard 的样子和工作方式都和Interface Builder(以下简称为IB)像极了,你可以从左下方的控件库中拖动控件到你的View之中并且组织他们的排放顺序,唯一不同的地方就 是,Storyboard不止是包含一个视图控件,而是所有的视图控件以及他们之间的关系。
Storyboard对一个视图的官方术语是一个场景,但是一个场景其实就是一个ViewController,在iPhone中一次只能够展示一个场景,而在iPad中一次可以展示多个场景,比如Mail应用程序。
通过尝试添加一些控件,你可以感受一下Storyboard的工作方式。
这个是数据显示器,显示所有场景及其控件的结构。
在IB中,这个位置显示的是你的NIB文件中的文件,而在Storyboard中这里显示的是ViewController,目前这里只有一个ViewController,我们接下来可能会增加一些。
这是一个文档管理器的缩小版,叫做dock。
Dock 展示场景中第一级的控件,每个场景至少有一个ViewController和一个FirstReponder,但是也可以有其他的控件,Dock还用来简 单的连接控件,如果你需要向ViewController传递一个关系时,只需要将其按住Ctrl键拖到ViewController上就可以了。
Note:你大概不会太长使用FirstResponder,因为它只是一个代理控件,代表着当前你所使用的控件。
现在运行这个应用,他会向我们设计的界面一样。
如果你以前制作过NIB型的应用的话,你也许回去寻找MainWindow.xib ,这个文件包括所有的ViewController,Appdelegate等等,但是在Storyboard中这个特性已经被废止了。
那么,没有这个文件,应用从那里起始呢?
让我们打开AppDelegate文件,看看那上面是怎么说的:
- #import <UIKit/UIKit.h>
- @interface AppDelegate : UIResponder <UIApplicationDelegate>
- @property (strong, nonatomic) UIWindow *window;
- @end
如果要使用Storyboard特性,那么AppDelegate必须继承自UIResponder类, 之前则是继承自NSObject类的,而且必须有一个不是UIOutlet类的Window属性声明才可以。
如 果你再去看AppDelegate的执行文件,里面大概什么都没有,甚至连 application:didFinishLaunchingWithOptions: 也只是返回了一个 YES,而之前,这里则需声明一个ViewController并且将他设置成起始页面,但是现在这些都没有了。
秘密就在info.plist文件中, 打开Ratings-Info.plist (在 Supporting Files group里) 你就会看到这些:
在 NIB为UI的应用里,info.plist文件中有一个键兼做NSMainNibFile,或者叫做Main nib file base name,他用来指示UIApplication载入MainWindow.xib,并且将他与应用链接起来,而现在这个键值消失了。
而 Storyboard应用则利用 UIMainStoryboardFile,或者 “Main storyboard file base name” 键值来表示当App初始化时的Storyboard名称,当程序运行时,UIApplication会使用 MainStoryboard.sotryboard作为第一加载项,并且将他的UIWindow展示在屏幕上,不需要任何编程工作。
在项目总结面板上,你也可以看到并且编辑这些信息:
如果你还想设置nib文件的话,另外有地方去设置的。
为了完成这个实验性的小程序,我们打开main.m,加入
- #import <UIKit/UIKit.h>
- #import "AppDelegate.h"
- int main(int argc, char *argv[])
- {
- @autoreleasepool {
- return UIApplicationMain(argc, argv, nil,
- NSStringFromClass([AppDelegate class]));
- }
- }
之前是UIApplicationMain()的函数现在是空的, 变成了 NSStringFromClass([AppDelegate class]).
与 之前使用MainWindow.xib的一个最大的不同是:现在app delegate已经不是Storyboard的一部分了,这是因为app delegate不再从nib文件中,而侍从Storyboard中加载了,我们必须告诉 UIApplicationMain 我们的app delegate类的名字是什么,否则他将无法找到。
制作一个Tab类型的应用
本教程中的Rating App拥有两个Tab,在Storyboard中,很轻松就能够做出一个Tab视图。
回到MainStoryboard.storyboard中,直接从左边的Library拖进来一个TabViewController就可以了。
新 的Tab Bar Controller附带了两个View controller,分别作为Tab的视图使用,UITabBarController被称为包含视图,因为他包含这其他一些View,其他常见的包含 视图还有那vi嘎提鸥鸟 Controller和SplitView Controller。
在iOS 5中,你还可以自己写一个自定义的Controller,这在以前是做不到的。
包含关系在Storyboard中用一下这种箭头表示。
拉一个Label控件到第一个子试图中,命名为“First Tab”,再在第二个子视图中添加一个Label,命名为“Second Tab”。
注意:当屏幕的缩放大于100%时,你无法在单个场景中添加控件。
选中Tab Bar Controller,进入属性检查器,选中“作为起始场景”,如下图:
现在那个没有头的虚虚的小箭头指向了Tab Bar Controller,说明他是起始场景。
这意味着,当你启动这个应用的时候,UIApplication将会将这个场景作为应用的主屏幕。
Storyboard一定要有一个场景是起始场景才行。
现在运行试试吧
code专门为创造这种Tab Bar的应用准备了一个模板,我们也可以使用他,但是自己有能力不用模板自己做一个Tab Bar也是不错的事。
如果你添加了多于五个子视图到一个TabBarcontroller的话,并不会创造五个Tab,第四个tab会自动变成More标签,不错吧
制作一个表格视图
目前连接到Tab bar Controller的视图都是普通的View Controller,现在,我要用一个TableViewController来代替其中的一个ViewController。
单击第一个视图并删除,从Library中拖出一个TableViewController。
在选中这个TableViewController的前提下,从Library中拖出一个NavController,将会直接附着在上面。
当然也可以调换顺序,我完全没意见。
由于NavController和TabBarController一样也是一个包含控制器视图,所以他也必须包含另一个视图,你可以看到同样的箭头连接者这两个View。
请注意所有嵌套在NavController下的View都会有一个Navigation Bar,你无法移除它,因为他是一个虚拟的Bar。
如果你检视属性检测器,你就会发现所有bar的属性都在一起:
“Inferred”是Storyboard中的默认设置,他意味着继承的关系,但是你也可以改变他。但是请注意这些设置都是为了让你更好的进行设计和这样设置的,随意修改默认设置会带来不可遇见的后果,施主自重。
现在让我们把这个新的场景连接到Tab Bar Controller中,按住Ctrl拖动,或者右键。
当你放手的时候,一个提示框会出现。
当然是选第一个了,Relationship – viewControllers ,这将自动创建两个场景之间的关系。
直接拖动就可以改变Tab Item的顺序,同时也会改变显示Tab的顺序,放在最左边的Tab会第一个显示。
现在运行试试看吧
在我们在这个应用中加入任何实质性的功能之前,我们先来清理一下Storyboard,你不需要改变TabBarController中的任何内容而只需要改变他的子视图就可以了。
每当你连接一个新的视图到TabBarController中的时候,他就会自动增加一个Tab Item,你可以使用他的子视图来修改该Item的图片和名称。
在NavController中选中Tab Item并且在属性编辑其中将其修改为Player。
将第二个Tab Item命名为“Gesture”
我们接下来把自定义的图片加入到这些item中, 源码 中包含一个名为“Image”的文件夹,在那里你可以找到我们用到的资源。
接下来,将NavController的title改为Player,也可以使用代码··
运行看一看,难以置信吧,你到现在也没写一条代码。
原型表格单元
你也许已经注意到了,自从我们加入了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的数值也会自动改变。
现在运行看看,好多了吧
为原型单元格设置子类
我们的表格视图已经相当像模像样了,但是我并不是很喜欢使用tag来访问label,要是我们能够把这些lable连接到输出口,之后在回应属性中使用他们,该多好,而且不出所料,我们可以这样做。
使用 Objective-C class模板新建一个文件,命名为PlayerCell,继承UITableViewCell。
修改PlayerCell.h
- @interface PlayerCell : UITableViewCell
- @property (nonatomic, strong) IBOutlet UILabel *nameLabel;
- @property (nonatomic, strong) IBOutlet UILabel *gameLabel;
- @property (nonatomic, strong) IBOutlet UIImageView
- *ratingImageView;
- @end
修改PlayerCell.m
- #import "PlayerCell.h"
- @implementation PlayerCell
- @synthesize nameLabel;
- @synthesize gameLabel;
- @synthesize ratingImageView;
- @end
这个类本身并不其很大的作用,只是为nameLabel、gameLabel和ratingImageView声明了属性。
回 到MainStoryboard.storyboard选中原型单元格,将他的class属性修改为“PlayerCell”,现在当你向table view请求dequeueReusableCellWithIdentifier,他会返回一个PlayerCell实例而不是一个普通的 UITableViewCell实例。
请注意我将这个类和reuse Indetifier的名字命名的一样,只是营卫我喜欢这样哦亲,这两个之间其实没啥关系。
现在你可以将标签和image view连接到输出口去了,选中或者将他从链接检查器拖动到table view cell。
请注意:要把这个control连接到table view cell而不是view controller哦亲,别选错了。
现在我们把一切都链接好了,只需要加入数据源的代码就可以了。
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- PlayerCell *cell = (PlayerCell *)[tableView
- dequeueReusableCellWithIdentifier:@"PlayerCell"];
- Player *player = [self.players objectAtIndex:indexPath.row];
- cell.nameLabel.text = player.name;
- cell.gameLabel.text = player.game;
- cell.ratingImageView.image = [self
- imageForRating:player.rating];
- return cell;
- }
我们现在将接收到 dequeueReusableCellWithIdentifier 的控件指派到PlayerCell,只需要简单的使用已经链接labels和image view到设置好的属性上就可以了,这会让这个设计看上去更加好控制,更加简明。
当然,在PlayerCell前要引入资源:
- #import "PlayerCell.h"
试着运行,你会发现其实什么都没有变化,可是我们都知道,内部已经有了变化。
在这相同的场景下面,我们可是在使用子类呢。
这里还有一些设计小窍门:第一点:一定要设置标签被选中时的颜色。
第二点,确保你加入单元格的字符大小是可以变化的,这样,当单元格大小变化时,他的内容的大小也会跟着变化,比如说:
在PlayersViewController.m中加入如下方法:
- - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
- {
- if (editingStyle == UITableViewCellEditingStyleDelete)
- {
- [self.players removeObjectAtIndex:indexPath.row];
- [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
- }
- }
这个方法加入好了之后,用手指轻扫一行单元格,会出现一个删除键,试试看
Delete按钮出现在右边,遮住了一部分评级图片,怎么解决呢?
打开MainStoryBoard.storyboard,选中table view cell中的image view,在大小检查器中修改Autosizing属性,是它能够跟随上级view的边缘。
为labels设置同样的属性。
加入了这些变动之后,删除按钮如我们意料的出现了:
其实,最好的做法是让这些星星在出现delete按钮的时候消失,不过这只是一个练习,不要太较真哦亲
在上一篇《Storyboard全解析-第一部分》中,我们介绍了如何使用storyboard来制作多种场景和如何将这些场景链接起来,我们还学习了如何自定义一个表格视图。
接下来这部分,也是最后一部分,我们将讲解联线(segue),静态单元格等内容,我们还将加入一个选手详细内容页面,和一个游戏选择页面。
Segues的介绍
现在,让我们创建一个场景使用户可以自己增加新的选手进入列表。
在Players界面中拖入一个Bar Button,放置在导航栏的右侧,在属性监视器中将他的Identifier改为“add”,这样他就会显示一个加号的按钮,当用户点击这个按钮时,他就会弹出一个新的场景让用户对新的内容进行编辑或添加。
在编辑器中拖入一个新的Table View Controller,放置在Players场景的右边,然后按住ctrl,拉动加号键到新的场景中,这样,这个场景就会自动和这个按钮建立联系,从而自动归入Navigation View Controller中。
放开鼠标之后,会出现如下选项:
选中Modal,你可以注意到出现了一种新的箭头形式:
这 种链接形式被官方称为segue(pronounce: seg-way),我叫它联线,(其实是转换的意思)这种形式的联线是表示从一种场景转换到另外一种场景中,之前我们使用的连接都是描述一种场景包含另一 种场景的。而对于联线来说,它会改变屏幕中显示的内容,而且必须由交互动作触发:如轻点,或其他手势。
联线真正了不起的地方在于:你不再需要写任何代码来转入一个新的场景,也不用在将你的按钮和IBAction连接到一起,我们刚才做的,直接将按钮和场景链接起来,就能够完成这项工作。
运行这个app,按下 + 键,会发现出现了一个新的列表。
这种叫做 “modal” segue(模态转换),新的场景完全盖住了旧的那个。用户无法再与上一个场景交互,除非他们先关闭这个场景,过一会我们会讨论 push segue,这种segue会把场景推入导航栈。
新的场景现在还没有什么用,你甚至不能把他关闭呢。
联 线只能够把你送到新的场景,你要是想回来,就得使用delegate pattern,代理模式。我们必须首先给这个新的场景设置一个独有的类,新建一个继承UITableViewController的类,命为 PlayerDetailsViewController。
为了把它和storyboard相连,回到MainStoryBoard,选择新建的那个Table View Contrller,将他的类设置喂PlayerDetailViewController,千万不要忘记这一步,这很重要。
做完这一步之后,把新场景的标题改为“Add Player”,分别加入“Done”和“Cancel”两个导航栏按钮。
修改PlayerDetailsViewController.h 如下:
- @class PlayerDetailsViewController;
- @protocol PlayerDetailsViewControllerDelegate <NSObject>
- - (void)playerDetailsViewControllerDidCancel:
- (PlayerDetailsViewController *)controller;
- - (void)playerDetailsViewControllerDidSave:
- (PlayerDetailsViewController *)controller;
- @end
- @interface PlayerDetailsViewController : UITableViewController
- @property (nonatomic, weak) id <PlayerDetailsViewControllerDelegate> delegate;
- - (IBAction)cancel:(id)sender;
- - (IBAction)done:(id)sender;
- @end
这会声明一个新的代理机制,当用户点击Cancel或者done按钮时,我们将用它来交互Add Player场景和主场景通讯。
回到故事版编辑器,将Cancel和Done按钮分别与动作方法连接,一种方式是,按住Ctrl拖动到ViewController上,之后选择正确的动作。
在 PlayerDetailsViewController.m,加入如下代码:
- - (IBAction)cancel:(id)sender
- {
- [self.delegate playerDetailsViewControllerDidCancel:self];
- }
- - (IBAction)done:(id)sender
- {
- [self.delegate playerDetailsViewControllerDidSave:self];
- }
这是两个导航栏按钮要使用的方法,现在只需要让代理知道我们刚才加入了代码,而真正关闭场景只是代理的事情。
一般来说一定要为代理制定一个对象参数,这样他才知道向那里发送信息。
不要忘记加入Synthesize语句。
- @synthesize delegate;
现在我们已经为PlayerDetailsViewController设置了一个代理协议,我们需要将这个协议的实现方法 (implement)写在什么地方,很明显应该写在PlayerViewController因为这个vc代表了Add Player场景。在PlayersViewController.h中加入如下代码:
- #import "PlayerDetailsViewController.h"
- @interface PlayersViewController : UITableViewController <PlayerDetailsViewControllerDelegate>
并在PlayersViewController.m的结尾加入:
- #pragma mark - PlayerDetailsViewControllerDelegate
- - (void)playerDetailsViewControllerDidCancel:
- (PlayerDetailsViewController *)controller
- {
- [self dismissViewControllerAnimated:YES completion:nil];
- }
- - (void)playerDetailsViewControllerDidSave:
- (PlayerDetailsViewController *)controller
- {
- [self dismissViewControllerAnimated:YES completion:nil];
- }
目前这个代理方法只能够跳转到这个新的场景中,接下来我们来让他做一些更为强大的事情。
iOS 5 SDK中新添加的dismissViewControllerAnimated:completion: 方法可以被用来关闭一个场景。
最后还有一件事情需要做,就是Players场景需要告诉PlayerDetailsVC他的代理在哪里,听上去这种工作在故事版编辑其中一拖就行了,实际上,你得使用代码才能完成。
将以下方法加入到 PlayersViewController 中
- - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
- {
- if ([segue.identifier isEqualToString:@"AddPlayer"])
- {
- UINavigationController *navigationController =
- segue.destinationViewController;
- PlayerDetailsViewController
- *playerDetailsViewController =
- [[navigationController viewControllers]
- objectAtIndex:0];
- playerDetailsViewController.delegate = self;
- }
- }
当使用Segue的时候,就必须加入这个名叫 prepareForSegue 的方法,这个新的ViewController在被加载的时候还是不可见的,我们可以利用这个机会来向他发送数据。
请注意,这个segue的最终目标是Navigation Controller,因为这个是我们链接在导航栏上的按钮,为了获取PlayerDetailsViewController实例,我们必须通过NavController的属性来获取。
试着运行一下这个应用,单击 + 键,然后试着关闭Add Player场景,仍然不管用。
这是因为我们没有给Segue指定一个identifier,而parepareForSegu需要检查AddPlayer的身份证,这是必须的,因为你有可能会同时使用多个联线。
为了解决这个问题,进入Storyboard的编辑器,点击Players场景和NavgationViewController场景之间的联线,你会注意到与这个连线相关的按钮会自动亮起来。
在属性监视器中,将Identifier设置喂“AddPlayer”
如果这是你再次运行这个应用,点击“Cancel”或者“Done”按钮,这个场景就会自动关闭并且返回到上一级场景。
注 意:从modal场景调用dismissViewControllerAnimated:completion方法是我们在这里使用的,但是这并不意味着 你必须这样做。但是,如果你不是代理来完成这个关闭窗口的工作的话,唯一需要注意的是,如果你之前使用了 [self.parentViewController dismissModalViewControllerAnimated:YES] 语句来关闭窗口的话,那么这个语句就不会正常工作了。
顺便说一下,属性检查器中有一个Transition的选项,在这里你可以选择场景转换是的动画效果。
试着运行一下,看看那种动画你最喜欢吧,但事情不要改变Style这个选项,如果你改变了,这个app可能会crash哦。
我们接下来在这个教程中还会用到几次代理方法,下面我们来列一下为了完成一个连线,你需要做的几件事情。
- 首先,从起始的控件做一条联线到目标场景。
- 将这个联线制定一个独特的Identifier。
- 为目标场景制作一个代理方法。
- 在Cancel和Done按钮,以及所有其他你需要和原始场景交流的地方调用代理方法。
- 在原始场景执行代理方法,这将会在用户按下按钮后关闭场景。
- 在原始场景执行prepareForSegue方法。
我们在这里必须使用代理,是因为根本没有反向联线这种东西,当sugue被启动之后,他将会创造出一个目标场景的新实例。你当然可以做一个从目标场景回到原始场景的联线,但是结果可能与你希望的大相径庭。
距离来说吧,如果你做一条从cancel按钮回到原始场景的连线的话,他并不会关闭当前场景并返回原始场景,而是会创建一个原始场景的新实例,这种情况会不停循环,知道把内存耗尽为止。
所以请记住:segue只用于打开新的场景。
静态单元格
当我们全部完成之后,Add Player场景会看上去象下面的一样:
这是一种分组表格视图,但是不同的是,我们并不需要为这个表哥创建一个数据源,我们可以在故事版编辑器中直接设计这个视图,而不需要重写cellForRowAtIndex方法,使得我们可以这样做的秘诀就是静态单元格。
选中Add Player场景,之后在属性检查器中,将Content属性改为StaticCell,将Style to Grouped属性修改为2。
当你修改Section属性时,编辑器会复制一个现有的组。你也可以自己选中一个组后选择Duplicate。
我们的这个场景每个组只需要用一个行,所以选中上面的那个行之后删除。
选中顶行,修改Header的值为:“Player Name”.
拖一个新的Text Field进入这个组的单元格里,把它的边界删除掉,使用System 17字体,取消Adjust to Fit选项。
我 们现在在PlayerDetailsViewController中使用Assistant Editor这个Xcode 4.x的新特性来创建一个输出 口给这个Text Field,在工具栏的按钮中打开Assistant Editor,那玩意看起来像个外星人,我指的是按钮。
选中text field,按住Ctrl,将他拖到打开的文件之中。
放开鼠标,会出现一个选单。
将这个新的书出口命名为nameTextField,在你确定链接之后,Xcode会自动创建下列代码:
- @property (strong, nonatomic) IBOutlet UITextField *nameTextField;
他还会自动创建Synthesize语句,并同时在viewDidLoad文件中创建方法。
永远别在动态表格中使用这种拖来拖去的方法,但是对于静态单元格来说就OK,对于每个静态单元格来说都必须创建一个新的实例。
将第二个组的静态单元格的Style设置为Right Detail,这将会创建一个标准的单元格,把左侧的label的内容修改为Game,设置一个Disclosure Indicator,为右侧Detail的label设置一个输出口。
最终的设计完成后是这样的:
当 你使用静态单元格的时候,你的Table View Controller就不需要一个数据源了,但是因为我们使用了Xcode的模板来创造 PlayerDetailsViewController这个类,他里面仍然有一些默认的数据源设置代码,让我们来删除之。在以下这个标志
- #pragma mark - Table view data source
和这个标志之间的代码全部删除。
- #pragma mark - Table view delegate
现在运行这个App,效果不错吧,请注意我们不但一行代码也没写,还删除了好些。
但是我们并不能够完全避免写任何代码,你可能已经注意到了,在文本框和单元格周围有一些空间,用户在完成编辑之后单击这些区域并不会结束键盘什么的,怎么避免这个问题呢?用下面的代码代替tableView:didSelectRowatIndex方法。
- - (void)tableView:(UITableView *)tableView
- didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- {
- if (indexPath.section == 0)
- [self.nameTextField becomeFirstResponder];
- }
这些代码就是说:如果用户点击第一个单元格后我们激活text field控件,这虽然是细节,但是细节决定成败。
同时你也需要在属性检查器的Selection Style选项改为None。
OK,我们的设计全部完成了。
增加一个选手吧
现在我们暂时先忽略Game这一行,先让用户能够编辑选手的情况之后再说。
当用户单击Cancel键的时候,不管作出什么修改都会被弃置,场景也会关闭并返回上一级菜单。这一块的程序我们已经做好了,也就是我们刚才做得一个代理方法,它接收到did cancel这个方法之后就会关闭这个视图。
但是当用户单击“Done”这个按钮时,我们应该创建一个新的选手项目然后加入他的属性,之后我们还需要通知代理器我们新增了一个选手,以便它能够更新上一级菜单。
在 PlayerDetailsViewController.m,把完成的方法改成:
- - (IBAction)done:(id)sender
- {
- Player *player = [[Player alloc] init];
- player.name = self.nameTextField.text;
- player.game = @"Chess";
- player.rating = 1;
- [self.delegate playerDetailsViewController:self
- didAddPlayer:player];
- }
这需要我们引进Player的头文件:
- #import "Player.h"
这个完成方法会创建一个新的Player实例,并把它发送给代理器,由于目前代理器还没有这个方法,所以我们需要在PlayerDetailsViewController的头文件中修改如下代码:
- @class Player;
- @protocol PlayerDetailsViewControllerDelegate <NSObject>
- - (void)playerDetailsViewControllerDidCancel:
- (PlayerDetailsViewController *)controller;
- - (void)playerDetailsViewController:
- (PlayerDetailsViewController *)controller
- didAddPlayer:(Player *)player;
- @end
这个“Did Save”的方法的声明没有了,我们加入一个“didAddPlayer”方法。
下面我们需要在执行文件中加入执行的方法,打开PlayersViewController.m,加入:
- - (void)playerDetailsViewController:
- (PlayerDetailsViewController *)controller
- didAddPlayer:(Player *)player
- {
- [self.players addObject:player];
- NSIndexPath *indexPath =
- [NSIndexPath indexPathForRow:[self.players count] - 1
- inSection:0];
- [self.tableView insertRowsAtIndexPaths:
- [NSArray arrayWithObject:indexPath]
- withRowAnimation:UITableViewRowAnimationAutomatic];
- [self dismissViewControllerAnimated:YES completion:nil];
- }
第一个语句向players的数组中加入新的Player对象,之后他会通知表格视图:一个新的行已经被创建,这是因为table view和他的数据源必须一直是同步的才行,我们其实也可以使用[self.tableView reloadData]这个语句,但是重新创建一个单元格会有随之而来的动画,看起来更好看一些。 UITableViewRowAnimationAutomatic是一个iOS 5的新特性,使各行自动选择合适的动画效果出现,非常好用。
现在试试看,你应该可以使用按钮加入新行到表视图中了。
如 果你已经开始担心storyboard的性能了,那么不用担心。就算是将所有的场景都一块载入的话,也不会消耗多少资源的。storyboard不会一下 子加载所有的ViewController,而是会加载起始场景,在这里是Tab View,再从起始场景加载其他与起始场景相关的场景。
但是其他场景知道联线到他们之前是不会被加载的。而这些场景在你返回之后都会卸载,所以只有当前场景会在内存中,就像你之前在用分开的nib文件一样的。
我们通过实验来看一看。在PlayerDetailsViewController.m中加入下面的方法:
- - (id)initWithCoder:(NSCoder *)aDecoder
- {
- if ((self = [super initWithCoder:aDecoder]))
- {
- NSLog(@"init PlayerDetailsViewController");
- }
- return self;
- }
- - (void)dealloc
- {
- NSLog(@"dealloc PlayerDetailsViewController");
- }
我们重写了initWithCoder和dealloc方法,使得debug控制台输出一个很长的信息。这时候运行这个app,你会发现除非按下segue的按钮,否则新的场景不会被初始化,放心了吧。
还有一件关于静态单元格的事情需要注意,那就是他们只能够在UITableViewController的子类下使用,如果他的父类不是UITableViewController,Xcode会提示下面的错误:
“Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances”.
原型单元格,虽然可以在普通的View Controller中使用,但是不能够在Interface Builder中使用,
很少会出现有人会想要在一个表中用静态单元格和原型单元格混合起来,目前iOS SDK还不能很好的支持这种方法。
游戏选择器场景
在Add Player场景中单击Game的单元格会打开一个新的场景,让你能够从一个列表中选择一个游戏,这意味着我们需要加入一个新的表格视图,不过不同的是,我们这次会使用push到Navigation的栈之中,而不是直接跳转。
拖拉一个新的TableViewController到编辑器中,在Add Player场景中选择一个单元格按住ctrl键拉到新的场景中,创建一个连线,选择Push,之后把新segue的identifier命名为“PickGame”。
双击导航栏,修改标题为“Choose Game”,修改原型单元格的Style为Basic,修改他的Identifier为“GameCell”,我们的试图设计就到这里。
新建一个UITableViewController的子类,命名为GamePickerViewController,在storyboard中也要设置好哦。
首先我们给这个新的场景一些数据来显示,在GamePickerViewController.h中加入下列变量:
- @interface GamePickerViewController : UITableViewController {
- NSArray * games;
- }
之后转到GamePickerViewController.m,在viewDidLoad方法中加入数组的内容。
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- games = [NSArray arrayWithObjects:
- @"Angry Birds",
- @"Chess",
- @"Russian Roulette",
- @"Spin the Bottle",
- @"Texas Hold’em Poker",
- @"Tic-Tac-Toe",
- nil];
- }
由于在viewDidLoad方法中加载了数组,所以需要在viewDidUnload中卸载之。
- - (void)viewDidUnload
- {
- [super viewDidUnload];
- games = nil;
- }
将模板中的数据源方法修改为如下代码:
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- {
- return 1;
- }
- - (NSInteger)tableView:(UITableView *)tableView
- numberOfRowsInSection:(NSInteger)section
- {
- return [games count];
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView
- cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:@"GameCell"];
- cell.textLabel.text = [games objectAtIndex:indexPath.row];
- return cell;
- }
这样我们就完成了家在数据源的方法,这时候运行这个app,之后在Add Player场景中单击Game栏,就会转入这个视图了,但这时候单击这里的单元格并不会有什么作用。
这时候,由于我们使用push方式将这个场景推进了Navigation的栈中,所以这时候我们单击返回按钮就会自动返回到上一级界面。不错吧!
当然了,如果这个场景不输送任何数据回到上一级场景的话,那他就什么用也没有了,所以我们要创造一个新的代理器来完成这项任务。在GamePickerViewController.h中加入:
- @class GamePickerViewController;
- @protocol GamePickerViewControllerDelegate <NSObject>
- - (void)gamePickerViewController:
- (GamePickerViewController *)controller
- didSelectGame:(NSString *)game;
- @end
- @interface GamePickerViewController : UITableViewController
- @property (nonatomic, weak) id <GamePickerViewControllerDelegate> delegate;
- @property (nonatomic, strong) NSString *game;
- @end
我们加入了一个代理方法,其中只有一个方法和一个用于乘放目前选择的游戏的名字的属性。
现在,我们修改GamePickerViewController.m的开头:
- @implementation GamePickerViewController
- {
- NSArray *games;
- NSUInteger selectedIndex;
- }
- @synthesize delegate;
- @synthesize game;
这些代码新建了一个数组,一个选中项目的整数,并且synthesize了这些项目。
在viewDidLoad中加入如下代码:
- selectedIndex = [games indexOfObject:self.game];
选中的游戏名字会设置在self.game中,这里我们设置我们在表格中到底选中了哪个游戏。在这里,在场景加载之前必须首先填充self.game,由于我们在viewDidLoad之前设置了prepareForSegue这个方法,所以我们现在这么做没问题。
修改cellForRowAtIndexPath方法:
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:@"GameCell"];
- cell.textLabel.text = [games objectAtIndex:indexPath.row];
- if (indexPath.row == selectedIndex)
- cell.accessoryType =
- UITableViewCellAccessoryCheckmark;
- else
- cell.accessoryType = UITableViewCellAccessoryNone;
- return cell;
- }
这个方法会在选中的项目的右边加上一个选中的对勾。
将 didSelectRowAtIndexPath 修改为:
- - (void)tableView:(UITableView *)tableView
- didSelectRowAtIndexPath:(NSIndexPath *)indexPath
- {
- [tableView deselectRowAtIndexPath:indexPath animated:YES];
- if (selectedIndex != NSNotFound)
- {
- UITableViewCell *cell = [tableView
- cellForRowAtIndexPath:[NSIndexPath
- indexPathForRow:selectedIndex inSection:0]];
- cell.accessoryType = UITableViewCellAccessoryNone;
- }
- selectedIndex = indexPath.row;
- UITableViewCell *cell =
- [tableView cellForRowAtIndexPath:indexPath];
- cell.accessoryType = UITableViewCellAccessoryCheckmark;
- NSString *theGame = [games objectAtIndex:indexPath.row];
- [self.delegate gamePickerViewController:self
- didSelectGame:theGame];
- }
首先我们取消之前点击的那一行的选中状态,这将把它的蓝色变会正常的白色,之后将对勾删除掉,之后将对勾放置在刚刚选中的那一行上,最后,我们把选中的那一行返回给代理。
现在运行这个app测试一下效果,单击一个game的名字,将会出现一个对勾,单击另一个行,对勾的位置就会改变,但是返回上一级菜单之后发现我们的修改没有保存下来,为什么?因为我们还没有将代理真正的链接起来。
在 PlayerDetailsViewController.h 中,引入
- #import "GamePickerViewController.h"
之后在 @interface 行之后加入:
- @interface PlayerDetailsViewController : UITableViewController <GamePickerViewControllerDelegate>
在PlayerDetailsViewController.m加入prepareForSegue方法:
- - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
- {
- if ([segue.identifier isEqualToString:@"PickGame"])
- {
- GamePickerViewController *gamePickerViewController =
- segue.destinationViewController;
- gamePickerViewController.delegate = self;
- gamePickerViewController.game = game;
- }
- }
这和我们之前做过的很相似,但是这次的目标view Controller使game picker场景了,请记住,这个方法必须在GamePickerViewController初始化之后但是还没有加载view的时候调用。
“game”变量是新的,我们必须声明他:
- @implementation PlayerDetailsViewController
- {
- NSString *game;
- }
我们使用这个变量来记录到底选择了哪个Game,我们得给这个String设置一个默认值,可以用initWithCoder方法来完成。
- - (id)initWithCoder:(NSCoder *)aDecoder
- {
- if ((self = [super initWithCoder:aDecoder]))
- {
- NSLog(@"init PlayerDetailsViewController");
- game = @"Chess";
- }
- return self;
- }
如果你之前是用过nibs的话,那么initWithCode可能会对你很熟悉,这部分在storyboard是一样的。
修改 viewDidLoad 方法如下,以便单元格能够显示选中的Game名称:
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- self.detailLabel.text = game;
- }
最后要做的就是执行代理方法:
- #pragma mark - GamePickerViewControllerDelegate
- - (void)gamePickerViewController:
- (GamePickerViewController *)controller
- didSelectGame:(NSString *)theGame
- {
- game = theGame;
- self.detailLabel.text = game;
- [self.navigationController popViewControllerAnimated:YES];
- }
这行代码很好懂,我就不多讲了。
我们的结束方法将会把选中的游戏的名字加入到新建的Player对象中。
- - (IBAction)done:(id)sender
- {
- Player *player = [[Player alloc] init];
- player.name = self.nameTextField.text;
- player.game = game;
- player.rating = 1;
- [self.delegate playerDetailsViewController:self didAddPlayer:player];
- }
OK,到这里我们就完成了游戏选择器的场景,不错吧。