这篇文章还可以在这里找到 英语, 法语, 日语, 俄语, 阿拉伯语
来自Ray: 这是 iOS 5盛宴 中的第二篇教程, 这篇教程是我们的新书 iOS 5 By Tutorials 的一个免费预览章节, Matthijs Hollemans 写了这个章节,也是 iOS Apprentice Series 的作者
这篇教程来自iOS 教程团队成员 Matthijs Hollemans, 一个资深的iOS开发者和设计师
如果你想了解关于 iOS 5 中 Storyboard 更多的特性, 那么你来对地方了!
在本教程的第一部分, 我们介绍了用 storyboard 编辑器在控制器之间创建连接,还有如何利用 storyboard 编辑器创建自定义的单元格。
在这个系列的第二部分也是最后一部分, 我们将会探讨 segues, 静态单元格, 增加玩家信息界面, 游戏选择界面!
我们将接着上一篇教程继续开始, 打开你上一次的项目, 或者先去看看上一篇教程
好了,让我们研究一下Storyboard的其他一些炫酷功能吧。
现在是时候为我们的 Storyboard 添加更多的控制器了。 我们将要创建一个新界面,用来让用户增加新的玩家到应用中。
在Players界面上,拖动一个 Bar Button Item 到导航栏里面的右边。 在 Attributes Inspector 中修改它的 Identifier 为 Add, 让它变成一个标准的 + 按钮。 当你点击这个按钮时, 我们将弹出一个模态界面让你来输入新用户的详细信息。
拖出一个新的 Table View Controller 到主面板上, 放在 Players 界面的右边。 记住你可以在面板上双击鼠标来缩放面板, 这样可以给你大的工作区域。
让这个新的 Table View Controller 处于选中状态,并且将他嵌入到一个 Navigation Controller 中(如果你忘了怎么操作,在菜单栏中选择 EditorEmbed InNavigation Controller).).
这里有一个小技巧, 选中我们刚刚加入到Players界面中的 + 按钮, 然后按住Ctrl拖动到新的 Navigation Controller:
松开鼠标按键, 一个小弹出框就出现了:
选择 Modal。 这样就在 Players 界面和这个 Navigation Controller 之间创建了一个新类型的箭头:
这种类型的连接被称之为 segue(发音为:seg-way)),它表示了从一个界面切换到另一个界面。 目前为止我们建立的连接都是表示控制器之间的关系的。 而Segue, 和我们之前建立的连接不同, 它改变了屏幕上显示的界面。 他们可以通过点击按钮,单元格,手势等方式来触发。
segue最酷的一件事就是,你不再需要为展现新界面写任何代码了,也不需要为你的按钮绑定IBAction了。 我们刚才做的,从 Bar Button Item 拖动到下一个界面, 这就足以创建这个界面切换了。 (如果你的控件已经绑定了 IBAction 事件, 那么 segue 会覆盖它)。
运行应用, 然后点击这个 + 按钮,一个新的Table View 将会从屏幕上滑出来。
这就是 “Modal” segus. 新的界面完全遮盖住了前一个界面。 直到关闭这个 Modal 界面,否则用户是不能操作前一个界面的。 在后面, 我们还会看到 “Push” segue, 它会在导航栏的栈中压入一个新的界面。
这个新界面还不是很有用 — 你甚至不能关闭它回到主界面!
Segue 是单向的, 从 Players 界面到这个新界面。 如果要返回, 我们必须用代理模式。 为了这样, 我们首先必须为这个新界面定义它自己的类。 增加一个新的 UITableViewController 子类到项目中, 文件名叫做 PlayerDetailsViewController。
我们要绑定它到storyboard。 切换回 MainStoryboard.storyboard, 选中刚才那个新建的 Table View Controller, 并且在 Identity Inspector 中设置 Class 为 PlayerDetailsViewController。 我总是忘记这个重要的一步。 所以, 为了确保你不会这样, 我将会一直提到它。
把这个界面的 title 修改成 “Add Player” (双击导航条)。 增加两个 Bar Button Item 到导航条上面, 在 Attributes Inspector 中, 分别设置两个设置按钮的 Identifier 属性为 Cancel 和 Done。
然后修改 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 |
这里定义了一个新的代理协议, 当用户点击取消和完成按钮时, 我们用它来从 Add Player 界面向主界面通信。
切换回 Storyboard 编辑器, 并将取消和完成按钮分别绑定到它们的事件方法中去。 一个方法是, 按住 Ctrl 然后从按钮拖动到控制器, 然后从弹出框中找到相应的动作名称。
在 PlayerDetailsViewController.m 文件底部中,添加如下两个方法:
- (IBAction)cancel:(id)sender { [self.delegate playerDetailsViewControllerDidCancel:self]; } - (IBAction)done:(id)sender { [self.delegate playerDetailsViewControllerDidSave:self]; } |
设置两个导航栏按钮的动作方法。 现在,它们只是让代理对象知道发生了什么事情。 具体要怎么相应这些事件,就是代理对象的事了。 (这不是必须的,不过是我喜欢的一种方式。 还有,你也可以让 Add Player 界面在通知它的代理之前,先关闭自己)。
注意一下,代理方法通常会用第一个参数来引用调用它的哪个对象, 在我们这里是 PlayerDetailsViewController。 这个方式能让代理知道是哪个对象给它发送的消息。
别忘了用 synthesize 定义一下 delegate 属性。
@synthesize delegate; |
现在我们给 PlayerDetailsViewController 添加了一个代理协议,我们还需要在一些地方实现他们。 很明显, 应该在 PlayersViewController 中实现, 因为是它展现的 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]; } |
现在,代理方法只是简单的关闭当前界面。 稍后我们会让它做一些有趣的事情。
dismissViewControllerAnimated:completion: 这个方法是 iOS 5 新增的。 以前你可能用过 dismissModalViewControllerAnimated: 方法。 这个方法现在还可以继续使用, 不过这个新版本的方法应该是首选(因为他能让你在界面消失后,执行一些附加的代码)。
还有最后一件事要做: Players 界面必须告诉 PlayerDetailsViewController 它是它的代理。 简单看起来, 这件事好像在 Storyboard 编辑器中拖动一条线就能完成。很不幸, 这样不行。 使用segue的过程中,我们需要写一些代码才能传送数据到新的控制器。
在 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 方法。 在这个方法调用时, 新的控制器已经被加载出来了,但是还没有显示出来, 我们可以利用这个机会来把数据传递给它。 (你不能自己调用 prepareForSegue, 这是 UIKit 用来通知你 segue 被触发时调用的方法)。
注意这个 segue 的目标是 Navigation Controller, 因为它是我们从 Bar Button Item 连接过去的。 要得到 PlayerDetailsViewController 的实例, 我们必须深度遍历 Navigation Controller 的 viewControllers 属性。
运行应用,按下 + 按钮, 然后尝试关闭 Add Player 界面, 它还是不能正常工作!
这是因为我们没有给Segue设置过标识(identifier)。 prepareForSegue 中的代码检测的是标识为 AddPlayer 的 segue。 建议每次都进行这样的检测, 因为你可能在一个控制器中有多个segue, 并且你需要区分他们(后面我们会做到这个)。
为了解决这个问题, 我们进入Storyboard编辑器,并且点击在 Players 界面和 Navigation Controller 之间的 segue。 注意到 Bar Button Item 被高亮显示了, 所以你可以清楚的看到是哪个控件触发的这个segue。
在 Attributes Inspector 中,设置 Identifier 属性为 “AddPlayer”:
再次运行这个应用, 点击 Cancel 或者 Done 按钮, 就会按照预期关闭当前界面并且返回到玩家列表界面。
注意: 完全可以在弹出的模态界面中调用 dismissViewControllerAnimated:completion: 方法。 并不是必须要在代理中使用这个方法。 不过,我个人比较喜欢把它放在代理中, 但是,如果你向让模态界面来关闭它自己也是没问题的。 有一点你需要注意的: 如果你使用以前的这个方法 [self.parentViewController dismissModalViewControllerAnimated:YES] 来关闭界面的话,它不会正常工作了。 现在可以用 self.presentingViewController 而不是 self.parentViewController 来调用这个方法, 这是iOS 5 新增加的一个属性。
顺便说一下,segue 的 Attributes Inspector 中, 有一个 Transition 属性, 你可以选择不同的切换动画:
可以都试试他们,找到哪个是你最喜欢的。 但是不要改变 Style 设置。 因为这个界面是模态的,修改其他选项会导致应用崩溃。
我们将会在教程中多次用到代理模式。 这里是在两个 scene 之间建立连接的步骤。
因为没有反向的 Segue, 所以代理是必须要有的。 当 Segue 被触发时,会创建一个新的目标控制器。 你当然可以从目标控制器用 Segue 返回源控制器, 但是这样不会得到你期望的效果。
例如,如果你在 Cancel 按钮上,创建了一个返回 Players 界面的 Segue, 它不会关闭新建玩家的界面,也不会返回到 Players 界面, 它会创建一个新的 Players 界面。 这样你就陷入了一个死循环中, 直到应用的内存用尽。
记住:Segue 只能是单向的, 他们只能用于打开一个新界面。 如果要返回或者关闭界面(或者从导航控件的栈中弹出),通常都是用代理来做的。 Segue 仅仅听从于源控制器, 而目标控制器甚至都不知道自己是被 Segue 打开的。
当我们完成这些后, 增加玩家的界面应该是这样:
当然,这是一个分组的Table View,但不同的是,这次我们不用为这个Table View 创建数据源了。 我们可以直接在 Storyboard 编辑器中设计它, 不需要 cellForRowAtIndexPath 方法了。 静态单元格提供的特性,让这成为了可能。
在 Add Player 界面中选择Table View, 并且在 Attributes Inspector 中修改 Content 的值为 Static Cells。 设置 Style 属性为 Grouped, Sections 属性为 2。
当你修改了 Sections 属性, 编辑器会复制现有的 section。 (你也可以在左边的文档大纲中,选择一个特定的 Section, 然后复制它)。
我们的界面中,每个 Section 只需要一行数据, 删除掉那些多余的单元格吧。
选中最上面的 section, 在 Attributes Inspector 中设置 Header 属性为 “Player Name”。
拖动一个新的 Text Field 到这个 Section 的单元格中。 删除它的边框,这样你就不能看到文本是从何处开始和结束的了。 设置字体为 System 17, 并且取消 Adjust to Fit。
我们将在 PlayerDetailsViewController 中用 Xcode 的 Assistant Editor 功能为这个文本框创建一个outlet。 用工具栏上的按钮打开 Assistant Editor*(那个看起来想一个燕尾服的按钮)。 它应该会自动打开 PlayerDetailsViewController.h 。
选中文本框, 然后按住 Ctrl 拖动到 .h 文件中:
松开鼠标按键, 会出现一个弹出框:
给这个新的 outlet 命名为 nameTextField。 点击 Connect 按钮后, Xcode 会为 PlayerDetailsViewController.h 增加如下的属性:
@property (strong, nonatomic) IBOutlet UITextField *nameTextField; |
它还会自动 synthesize 这个属性,并把它添加到 .m 文件的 viewDidUnload 方法中。
我告诉过你,这种方式在原型单元格中是不能用的, 但是在静态单元格中是可以的。 因为每个静态单元格只有一个实例(不像原型单元格, 他们从来不会被复制), 所以,将它们的子视图连接到控制器上也是完全可以的。
设置第二个 Section 中的静态单元格的 Style 属性为 Right Detail。 这给了我们一个标准的样式去操作。 修改左边的 Label 的文字为 “Game”, 并且为这个单元格设置一个向右指示的箭头。 为右边的 Label(文本为 “Detail” 的那个)创建一个 outlet, 并且命名为 detailLabel。 这个单元格上面的 Label 只是普通的 UILabel 对象。
Add Player 界面的最终设计如下:
当你使用静态单元格时, 你的 Table View 控制器不需要数据源。 因为我们是用 Xcode 模板创建的 PlayerDetailsViewController 类, 它仍然会有数据源相关的默认代码, 所以让我们把这些代码删除掉吧, 现在我们不需要它了。 删除下面两个代码断之间的所有代码:
#pragma mark - Table view data source |
和
#pragma mark - Table view delegate |
这样应该会去掉 Xcode 关于这个类的所有警告消息。
运行应用, 看到新的界面是静态单元格构成的。 所有这些都没有写过一行代码 — 事实上, 我们还删除了很多代码。
我们应该把代码写完整, 当你把文本框添加到第一个单元格时, 你可能注意到它并没有完全适应界面, 在文本框的周围,有一个小小的外边距。 用户不能看到文本框是何处起始和结束的, 所以如果用户点击到外边距区域,键盘就不会弹出来,这样会给用户造成迷惑。 为了避免这种情况,我们应该在点击单元格的任何区域时,都让键盘弹出来。 这非常简单, 仅仅需要覆盖 tableView:didSelectRowAtIndexPath 方法:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0) [self.nameTextField becomeFirstResponder]; } |
这个的意思是, 如果用户点击了第一个单元格, 我们就激活文本框(因为每个单元格只有一行, 所以我们只需要判断 section 的索引)。 这样会自动的弹出键盘。 这只是一个小技巧, 但是这避免了用户的迷惑。
你还应该在 Attributes Inspector, 中设置这个单元格的 Selection Style 属性为 None(而不是 Blue), 否则当用户点击文本框的外边距部分时,单元格的背景会变成蓝色。
好了, 这就是 Add Player 的设计部分了, 现在让它来实际运转起来吧。
到现在,我们忽略了 Game 这行, 仅仅让用户在这里输入玩家的名称,没做任何其他的事情。
当用户按下 Cancel 按钮, 这个界面应该关闭, 这里输入的任何数据都将会丢失。 这个功能已经可以了。 代理对象(Players 界面)接受到 “did cancel” 消息, 然后直接关闭控制器。
当用户按下 Done, 我们应该创建一个新的 Player 对象,然后设置好它的属性。 然后, 我们应该告诉代理对象, 我们增加了一个新的玩家, 这样, 它就可以更新它自己的界面了。
PlayerDetailsViewController.m 中,修改 done 方法:
- (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" |
done 方法,现在创建了一个新的 Player 实例并且把它发送给代理。 代理协议现在还没有这个方法, 所以,我们修改一些 PlayerDetailsViewController.h 的定义:
@class Player; @protocol PlayerDetailsViewControllerDelegate <NSObject> - (void)playerDetailsViewControllerDidCancel: (PlayerDetailsViewController *)controller; - (void)playerDetailsViewController: (PlayerDetailsViewController *)controller didAddPlayer:(Player *)player; @end |
“didSave” 方法的定义去掉了, 而我们现在有了 “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]; } |
首先,把这个新的 Player 对象添加到 players 数组中。 然后告诉 Table View 增加了一行新数据(在最下面), 因为 Table View 和它的数据源必须时刻保持同步。 我们可以直接用 [self.tableView reloadData] 方法, 但是用一个动画插入新的一行,看起来会更好。 UITableViewRowAnimationAutomatic 是 iOS 5 中新提供的一个常量, 它会根据你要插入的行的位置,自动找到一个合适的动画,非常方便。
试一下吧, 你现在应该可以把新的玩家增加到主界面的列表中了!
如果你对 storyboard 的性能好奇, 你应该知道一点,一次性加载整个 storyboard 不是一个大问题。 Storyboard 不会一次性的实例化它里面的所有控制器, 只会实例化初始控制器。 因为我们的初始控制器是一个 Tab Bar Controller, 它里面包含的两个子控制器也会被加载(Players Scene 和 第二个 Tab 中的 Scene)。
直到你用 segue 打开他们, 否则其他的控制器是不会被加载的。 当你关闭控制器后,他们又会被释放掉, 所以仅有当前正在使用的控制器才会在内存中, 和你用单独的 nib 是一样的。
让我们实践一下, 在 PlayerDetailsViewController.m 中增加如下方法:
- (id)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { NSLog(@"init PlayerDetailsViewController"); } return self; } - (void)dealloc { NSLog(@"dealloc PlayerDetailsViewController"); } |
我们重写了 initWithCoder 和 dealloc 方法,在里面增加了一些日志输出。 现在,再次运行应用,并且打开 Add Player 界面。你应该会看到,这个控制器在这个时候,才会被初始化。 当你关闭 Add Player 界面后,按下 Cancel 或者 Done按钮, 你应该会看到 dealloc 中的 NSLog 输出。如果你再次打开这个界面,你应该还会再看见 initWithCoder 中输出的消息。 这样会保证你的控制器只有在需要时才会被加载, 就和手动的加载 nib 文件是一样的。
关于静态单元格另外一点,他们只能使用在 UITableViewController 中。 Storyboard 编辑器可以让你把它们添加到普通的 UIViewController 中的 Table View 上面, 但是这个在运行时不会正常工作。 原因是, UITableViewController 内部有一些特殊的机制来管理静态单元格的数据源。 Xcode 甚至通过显示错误消息 “Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances” 来防止你这样做。
另一方面, 原型单元格可以在 UIViewController 中很好的工作。 不过,这两种单元格,都不能用在 Interface Builder 中。 目前,如果你想使用原型单元格或者是静态单元格, 那么你就必须使用 Storyboard。
你可能会想在同一个Table View 中同时使用静态单元格和动态单元格, 但 SDK 还没有很好的支持这项功能。 如果你确实想这样做, 那么看看这里的一个解决方案。
注意: 如果你要创建一个有很多静态单元格的界面 — 超出了可显示区域 — 那么你可以在 Storyboard 编辑器中使用鼠标或触摸板(双指滑动)上面的滚动手势。 这个可能不是很明显, 但是它确实可以用。
点击 Add Player 界面中的 Game 那行应该打开一个新的界面, 来让用户从一个列表中选择一个游戏。 这就是说,我们需要添加一个新的 Table View Controller, 然而这次我们将把它压入导航栏的栈中,而不是模态的显示它。
拖动一个新的 Table View Controller 到 storyboard 上面。 在 Add Player 界面中选择 Game 单元格(要确保选中的是整个单元格, 而不是里面的 Label), 然后按住 Ctrl 拖动到新的 Table View Controller 上, 在他们之间创建一个 segue。 创建一个 Push segue 并且给它的标识命名为 “PickGame”。
双击导航条,设置它的名称为 “Choose Game”。 设置原型单元格的 Style 属性为 Basic, 并且设置 Reuse Identifier 为 “GameCell”。 这就是所有我们需要对这个界面进行的设计:
增加一个新的 UITableViewController 子类,并且名称为 GamePickerViewController。 不要忘记把 storyboard 中的 Table View Controller 和这个类关联起来。
首先,我们要给这个新的界面一些数据来显示,为 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; } |
即便实际上 viewDidUnload 不会被这个界面调用(我们没有用另外一个视图盖住它), 不过这是一个好的实践, 总是要平衡内存的分配和释放。
替换用模板生成的数据源方法:
- (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; } |
只要数据源连接上,就会做这些事情。 运行应用, 然后点击 Game 那行单元格。 这个新的选择游戏的界面就会滑出来。 现在点击单元格不会有任何反应,但是这个界面展现在了导航栏栈上面,你可以按下返回按钮来回到 Add Player 界面。
这非常的酷吧? 我们不需要写一行代码,就可以打开一个新界面。 我们仅仅从这个静态单元格拖动到了新的 Scene上面,这就行了。(注意,当你点击 Game 单元格时,PlayerDetailsViewController 中的代理方法 didSelectRowAtIndexPath 还是会被调用, 所以要确保你这里的代码不会和 segue 冲突)。
当然, 如果这个新界面不往回发送数据的话,那它一点用处也没有, 所以我们必须为它增加一个新的代理。 在 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; |
这里增加了一个新的 ivar, selectedIndex , 并且用 synthesize 声明了这个属性。
然后,在 viewDidLoad 底部增加这几行:
selectedIndex = [games indexOfObject:self.game]; |
选中的游戏的名称,会被设置到 self.game 中。 这里我们找到这个游戏在 games 数组中的索引。 我们将用这个索引来设置单元格的选中状态。 为了能正常运行, 必须在视图加载完成之前 self.game 赋值。 因为我们是在调用者的 prepareForSegue 方法中给他赋值,这个方法会在 viewDidLoad 之前执行, 所以这就不成问题了。
Change cellForRowAtIndexPath to:
- (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]; } |
在单元格被点击时,我们首先反选了它。 这让它从高亮的蓝色变成常规的白色。 然后我们删除之前选中的单元格的选中符号, 并且把它放到我们刚刚点击的单元格上面。 最后, 我们把选中的游戏的名称传递给代理对象。
运行应用,测试一下这个。 点击一个游戏的名称,它所在的单元格会得到一个选中符号。 点击另一个游戏的名称, 选中符号就会移动到它上面。 这个界面应该在你点击任何一行后马上消失, 但是它没有这样, 因为你还没有绑定代理对象。
在 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; } } |
这和我们之前做的很像。 这次目标控制器是一个 Game Picker 界面。 记住这里的代码会在 GamePickerViewController 初始化之后, 和它的视图被加载之前执行。
game 变量是一个新的变量:
@implementation PlayerDetailsViewController { NSString *game; } |
我们用这个 ivar 来记录选中的游戏,所以我们可以稍后再把它存放到 Player 对象中。 我们应该给它一个默认值。 initWithCoder 方法是做这个事情的好地方。
- (id)initWithCoder:(NSCoder *)aDecoder { if ((self = [super initWithCoder:aDecoder])) { NSLog(@"init PlayerDetailsViewController"); game = @"Chess"; } return self; } |
如果你以前用过 nib, 你应该会比较熟悉 initWithCoder 这个方法。 在 storyboard 中它还是同样的功能。 initWithCoder,awakeFromNib 和 viewDidLoad 这些方法仍然会被使用。 你可以把 Storyboard 想象成一个很多 nib 的集合, 并且附带了控制器间如何切换,和他们之间的关系的信息。 但是storyboard中的视图和视图控制器,还是以同样的方式编码和解码。
修改 viewDidLoad 方法, 用来把游戏的名称显示到单元格上:
- (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]; } |
这个看起来很直观, 我们把新选择的游戏的名称赋值给 game 实例变量和单元格上面的 Label, 然后我们关闭了游戏选择界面。 因为它是一个 push segue, 我们必须把它从导航栏的栈中弹出来关闭它。
我们的 done 方法,现在可以把选择好的游戏的名称赋值给 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]; } |
棒极了,我们现在有了一个功能齐全的游戏选择界面!
这里是包含我们这篇教程所有代码的示例项目
祝贺你,你现在已经知道了 Storyboard 的基本使用方法, 并且可以创建有多可控制器并且通过 segue 在他们之间切换的应用!
如果你想更多的了解 iOS 5 中的 Storyboard, 看看这本新书iOS 5 By Tutorials. 它里面还有另外一章 “Storyboard 进阶” ,里面包含了:
如果有关于这个教程或者任何关于 Storyboard 问题或建议,请加入我们的论坛一起讨论!
这篇教程发自 iOS 教程团队成员 Matthijs Hollemans, 一个资深的iOS开发者和设计师.