译自《iOS 5 by tutorials》
在上一章,你已经学习了故事板的基本用法。包括如何向故事板中添加 View Controller,通过 segues 切换 View Controller,以及轻松创建定制的表单元格。
在本章,我们将向你展示更多的关于 iOS 5 故事板的新特性。例如如何让用户在应用程序中编辑玩家资料,为场景添加多个 segues,定制 segues,在 iPad 中使用故事板等等。
接下来,用 Xcode 打开你的 Ratings 工程,让我们一起开始吧!
编辑已有的玩家资料
应该让用户能够编辑他们输入的数据。在这一节,我们会修改PlayerDetailsViewControlelr,在原有的增加新玩家的基础上,扩展出编辑玩家的功能。
用右键(ctrl+左键)从Players 的模板cell 拖一条线到与AddPlayer 关联的 Navigation Controller 上,并创建一个 modal segue。命名 segue 为 EditPlayer。这样,在这两个场景间会存在两个segue。
我们通过二者的名称(AddPlayer 和 EditPlayer)来区分两个segue。如果搞不清正在操作哪一个的时候,可以点击 segue 图标,它会以蓝色高亮的形式显示。
在 PlayersViewController.m中,修改 prepareForSegue 为:
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
if ([segue.identifierisEqualToString:@"AddPlayer"])
{
UINavigationController*navigationController =
segue.destinationViewController;
PlayerDetailsViewController *playerDetailsViewController=
[[navigationController viewControllers]objectAtIndex:0];
playerDetailsViewController.delegate = self;
}else if ([segue.identifierisEqualToString:@"EditPlayer"]) {
UINavigationController*navigationController =
segue.destinationViewController;
PlayerDetailsViewController*playerDetailsViewController =
[[navigationControllerviewControllers] objectAtIndex:0];
playerDetailsViewController.delegate = self;
NSIndexPath *indexPath=
[self.tableView indexPathForCell:sender];
Player *player= [self.players objectAtIndex:indexPath.row];
playerDetailsViewController.playerToEdit = player;
}
}
在 if 语句中,我们增加了检查 segue 是否为 EditPlayer的检测。除了传递了一个 Player 对象给 playerToEdit 属性,其他跟我们在判断 segue 是否为 AddPlayer 中所做的完全相同。
我们使用下句来查找当前触摸的单元格的 IndexPath:
NSIndexPath*indexPath =
[self.tableView indexPathForCell:sender];
prepareForSegue 方法的“sender”参数是触发该 segue的控件的指针。对于名为 AddPlayer 的 segue,sender 是一个 UIBarButtonItem,但对于名为 EditPlayer 的segue,sender 实际上是一个 TableViewCell。我们是在模板 cell 上创建的 segue,也就是说它会被任何模板cell的拷贝所触发。故事板会在场景后面自动完成这一切。
在 PlayerDetailsViewController.h 中增加一个属性声明:
@property(strong, nonatomic) Player *playerToEdit;
然后在 PlayerDetailsViewController.m 中加入:@synthesize playerToEdit;
修改 viewDidLoad 方法:
- (void)viewDidLoad
{
[super viewDidLoad];
if (self.playerToEdit !=nil)
{
self.title =@"Edit Player";
self.nameTextField.text = self.playerToEdit.name;
game = self.playerToEdit.game;
}
self.detailLabel.text =game;
}
如果 playerToEdit 属性不为空,ViewController就不是添加玩家窗口,而是编辑玩家窗口。我们会用 playerToEdit 对象的值填充玩家姓名以及游戏名称。
运行程序,点击一个玩家打开编辑玩家界面。
当然,由于我们实际上还没有完成修改功能,如果此时点击 Done 按钮,结果仍然是向列表中添加一个新玩家而不是修改已有的玩家。我们应该修改其中的逻辑,使其能够修改已有玩家。
首先,在委托协议中定义一个新方法:
PlayerDetailsViewController.h:
- (void)playerDetailsViewController: (PlayerDetailsViewController *)controllerdidEditPlayer:(Player *)player;
然后在 PlayerDetailsViewController.m中修改 done 按钮的 action 方法:
- (IBAction)done:(id)sender
{
if (self.playerToEdit != nil){
self.playerToEdit.name =self.nameTextField.text;
self.playerToEdit.game= game;
[self.delegate playerDetailsViewController:self
didEditPlayer:self.playerToEdit];
else {
Player*player = [[Playeralloc] init];
player.name = self.nameTextField.text;
player.game = game;
player.rating= 1;
[self.delegate playerDetailsViewController:self didAddPlayer:player];
}
}
然后在 PlayersViewController.m中实现这个委托方法:
- (void)playerDetailsViewController: (PlayerDetailsViewController *)controller
didEditPlayer:(Player *)player
{
NSUInteger index= [self.players indexOfObject:player];
NSIndexPath *indexPath=
[NSIndexPath indexPathForRow:indexinSection:0];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]withRowAnimation:UITableViewRowAnimationAutomatic];
[self dismissViewControllerAnimated:YES completion:nil];
}
这里,我们 reload 了该玩家的单元格以刷新其上的标签,然后关闭编辑窗口。
只需几个小小的改变,我们就可以重用原有的 PlayerDetailViewController类。现在,可以通过两个 segue 到达这个场景,AddPlayer 和 EditPlayer,到底是采取添加的方式还是编辑的方式进入,这取决于哪个segue 被触发。记住,执行每个 seque 时都会创建全新的目标 Viewcontroller 对象,如果你先添加玩家又编辑该玩家,你实际上是在同不同的 Viewcotnroller对象进行交互。
我反复重复这一点,prepareForSegue 在 viewDidLoad方法之前调用。恰恰是利用这一点,我们在 prepareForSegue 方法中设置了 playerToEdit 属性。在 viewDidLoad 方法中我们就可以获取playerToEdit 并用于渲染标签中的文本。
注意:在目标ViewController 上,并没有一个叫做 didPerformFromSeque 的方法。事实上,ViewController 根本不知道 segue。要告诉目标ViewController 它是被 segue 所触发的,只能通过 prepareForSegue 方法——要么设置它的某个属性,要么调用它的某个方法。你可以重载目标ViewController的属性 setter 方法,例如:
- (void)setPlayerToEdit:(Player*)newPlayerToEdit
{
if (playerToEdit!= newPlayerToEdit) {
playerToEdit= newPlayerToEdit;
}
}
// 额外的设置 // ...
self.invokedFromSegue= YES;
评分窗口
这个程序叫做“Rating”,但除了显示几个星形图标外根本就没打分的功能。现在,我们将增加一个ViewController 用于给玩家评分:
拖一个 ViewController 到画布,放到AddPlayer 下面。一个普通的 ViewController就可以,不是 UITableViewController。
出现一个问题,我们想通过玩家列表来调用这个新的评分窗口,但单元格上已经有个segue 用于打开玩家编辑窗口。我们必须用一种方法来区分这两个操作。我们将采用的办法是:用触摸单元格来弹出评分窗口,而用触摸打开细按钮来弹出玩家编辑窗口。
首先选中Players 中的模板cell,修改它的 accessory 属性为Detail Disclosure,这样它的 accessory 会变成一个蓝色的小圆按钮。
删除名为 EditPlayer 的 segue。我们本想从 detaildisclosure 按钮创建一个新的 segue 添加玩家/编辑玩家场景。不幸的是,故事板编辑器并不支持这种操作。我们只能在 ViewController 上创建一个指向它自身的segue 然后在代码中定制它。
从 dock 上的 ViewController 图标上用右键拖一条线到NavigationController ,创建一个 Modal segue,命名为 EditPlayer。注意,这个 segue 是连接到 Players 窗口自身的,而不是连接到它里面的某个控件。
从模板 cell 创建一个 segue 到新加的 ViewController,命名为RatePlayer。现在,在 Players 窗口上有 3 条 segue。双击 Navigation Bar,将新窗口的标题设置为 Rate Player。
回到 disclosure 按钮的问题上来。你应该知道 TableView有一个特殊的委托方法用于处理 diclosure 按钮事件的。我们将在 PlayersViewController.m 中使用这个方法来手动触发EditPlayer segue。
- (void)tableView:(UITableView*)tableView
accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
[self performSegueWithIdentifier:@"EditPlayer" sender:indexPath];
}
通过故事板(及其包含的 NavigationController)加载PlayerDetailsViewController 就是如此简单。当然,这仍然会调用 prepareForSegue。我们需要稍微修改一下这个方法。之前,sender参数指向了触发 segue 的 UITableViewCell,但现在没有 TableViewCell,而是一个 NSIndexPath(因为在 performSegueWithIdentifier方法中我们传递的只是一个 IndexPath)。
在 prepareForSegue 方法中,找到这行:
NSIndexPath*indexPath = [self.tableView indexPathForCell:sender];
修改为:
NSIndexPath *indexPath= sender;
当你通过 performSegueWithIdentifier 方法手动触发segue 时,你可以发送任何想要的参数。
我只所以传递一个 IndexPath 参数,是为了省事(你也可以发送一个Player 对象)。
运行程序,触摸dislcosure 按钮,弹出玩家编辑窗口(模式)。触摸表格行,则转到评分窗口(Push)。
继续设计给玩家打分窗口。添加一个 UIViewController 子类到项目中,命名为RatePlayerViewController(记住,它仅仅是一个常规的 ViewController,不是一个 TableViewController)。
在 Identity 面板中设置Rate Player窗口的类为 RatePlayerViewController。这是我经常容易忘记的步骤,因此我不得不花两分钟去奇怪为什么窗口不显示,到最后才想起我忘记做什么了。
编辑 RatePlayerViewController.h的内容:
@class RatePlayerViewController;@class Player;
@protocolRatePlayerViewControllerDelegate <NSObject>
- (void)ratePlayerViewController:
(RatePlayerViewController *)controller
didPickRatingForPlayer:(Player *)player;
@end
@interfaceRatePlayerViewController : UIViewController
@property(nonatomic, weak)
id <RatePlayerViewControllerDelegate>delegate;
@property(nonatomic, strong) Player *player;
- (IBAction)rateAction:(UIButton*)sender;
@end
看起来应该很熟悉。我们再次使用了委托模式,用于返回给源 ViewController。
RatePlayerViewController.m的顶部内容如下。只是导入与属性合成语句:
#import"RatePlayerViewController.h"
#import "Player.h"
@implementationRatePlayerViewController @synthesizedelegate;
@synthesizeplayer;
修改 viewDidLoad 方法为:
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = self.player.name;
}
用所选玩家的姓名取代原来的导航栏标题(Rate Player)。
值得注意的是 rateAction 方法:
- (IBAction)rateAction:(UIButton*)sender
{
self.player.rating =sender.tag;
[self.delegate ratePlayerViewController:self
didPickRatingForPlayer:self.player];
}
设置 Player 对象的 rating 属性,然后调用委托方法。rating属性是使用 sender.tag 进行赋值的,而 sender 实际上是 UIButton。我们会在 ViewController 上加入 5 个 UIButton——1颗星,2颗星……等等——每个的tag 被设置为 1 到 5。所有的按钮都使用同一个 action 方法。这是一种非常简单的办法。
向 Rate Player 窗口拖入 5 个按钮,并且进行适当的布局。
按钮使用的图片已经在项目中了(Images 文件夹下),1Star.png,2Star.png……等等。每个按钮的Touch Up Inside 事件与 rateAction 方法进行连接。设置它们的tag属性为 1-5。tag 属性的值与按钮的星数相对应。
我也将场景的背景色设置为淡灰色,这样按钮图片会显得更显眼一些。
这个窗口用不着在导航栏中放 Cancel 按钮和 Done 按钮,因为它是通过Push 的方式弹出的。
最后一步,设置 delegate 属性,以便这些按钮能够发送消息给委托对象。修改
PlayersViewController.h:
#import"RatePlayerViewController.h"
@interfacePlayersViewController : UITableViewController<PlayerDetailsViewControllerDelegate,RatePlayerViewControllerDelegate>
修改PlayersViewController.m:
#pragma mark -RatePlayerViewControllerDelegate
- (void)ratePlayerViewController: (RatePlayerViewController *)controller
didPickRatingForPlayer:(Player *)player
{
NSUInteger index= [self.players indexOfObject:player];
NSIndexPath *indexPath=
[NSIndexPath indexPathForRow:indexinSection:0];
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]withRowAnimation:UITableViewRowAnimationAutomatic];
[self.navigationController popViewControllerAnimated:YES];
}
当 Rate Player 窗口关闭,玩家对象被修改,我们再次刷新了 TableViewCell的显示。当然,别忘了还有 prepareForSegue 方法:
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender {
// ...原来的代码...
else if ([segue.identifier isEqualToString:@"RatePlayer"]) {
RatePlayerViewController *ratePlayerViewController=
segue.destinationViewController;
ratePlayerViewController.delegate = self;
NSIndexPath*indexPath = [self.tableView indexPathForCell:sender];
Player *player= [self.players objectAtIndex:indexPath.row]; ratePlayerViewController.player = player;
}
}
由于 Players 窗口有 3 个 segue,因此有 3 个 if 语句。
运行程序,检验效果。
手势
前面我们曾经忽略了程序的第2个 tab 窗口。现在让我们在其中添加一些东西。项目中有一个类叫做ViewController,那是 Xcode 模板为我们生成的,我们一直没有用到。将 ViewController 重命名为GestureViewConroller。
在故事版中,选择 ViewController ,将它连接到第 2 个tab 并将它的类设为 GestureViewController。
拖一些 Label 和一个 NavigationBar 到上面,最终如下图所示:
由于这个场景并不会 Push 新的窗口,因此我们就不将它嵌到导航控制器中了。只需要放一个导航栏到顶端就够了。
正如 Label 中 文本所示,我们将添加两个手势。向右扫,我们将弹出列有最佳玩家(五星)列表的窗口;双击,弹出最差(1星)玩家列表窗口。我们需要一个TableViewController 来做这个。
从Library 中拖一个导航控制器到画布中。这会创建出两个新的场景:一个Navigation Controller 和一个与之关联的 Root ViewContorller。我们并不需要 Root View Controller,请删除它。
拖一个 TableViewController 到新的导航控制器旁边。右键,从导航控制器拖一条线到TableViewController,然后选择“Relationship - rootViewController”。为了方便操作,你可以对画布中的场景进行调整。
我们准备用新的 TableViewController 作为游戏排行榜窗口。通过它的NavigationItem 设置它的标题,这样我们就能在一堆场景中将它一眼识出。现在,故事板已经包含了太多的东西。
回到手势窗口,通过手势触发一个 segue 其实非常简单。在对象库面板中有几种不同的手势识别器,拖一个Swipe Gesture Recognizer 到 Gesture 窗口。这会在 Dock 中加入一个手势识别器图标:
在这个图标上右键,拖到旁边的 Navigation Controller,选择“Modalsegue”。将segue 命名为 BestPlayers。从 Library 中拖一个 Tap Gesture Recoginizer ,同样创建一个名为WorstPlayers 的 segue。在 Tap Gesture Recognizer 的属性面板中,设置 number of taps 属性为2,以便侦测双击动作。
运行程序,执行手势,要么 segue 不会触发,要么程序崩溃 :]