Storyboard入门

来源:互联网 发布:到期备案域名 编辑:程序博客网 时间:2024/05/16 06:14

Storyboard 是iOS 5 中令人兴奋的一个新特性,他将为你在创建用户界面上节省很多时间。 那么究竟什么是Storyboard呢?我将用一幅图片来向你展示: 下面这个就是本教程中即将用到的Storyboard。

The full storyboard we'll be making in this tutorial.

你或许不能精确的知道这个应用是做什么的,但是你可以清楚的看到它有哪些屏幕界面,这些屏幕界面之间是怎样互相关联的。这就是Storyboard的强大之处。

如果你的应用有很多个不同的屏幕界面,Storyboard则可以减少那些用于在这些界面之前来回切换的中间代码(glue code)。 现在你的应用,用一个Storyboard就可以包含所有控制器的界面设计和他们之间的关系, 而不再需要为每一个控制器分别再创建一个nib文件。

Storyboard和普通的nib相比有很多优点。

  • 借助Storyboard,你可以对你应用中所有的界面和它们之间的联系有一个更好的概念上的总览。 因为所有的设计都在单个文件中,而不是分布成许多nib文件, 可以更加容易的找到任何东西。
  • Storyboard表明了各个界面之间的切换规则。 这些切换规则叫做“segues”, 按住ctrl键,从一个控制器拖动到另一个就可以创建它们。多亏了segues,能让你用更少的代码来处理UI。
  • Storyboard让UITableView使用起来更加简单, 它提供了原型单元格(prototype cells)和静态单元格(static cells). 你几乎可以完全在Storyboard编辑器中来设计你的UITableView,大大减少了你的代码量。

并不是所有的事情都那么完美,当然,Storyboard也有一些局限性。 Storyboard编辑器还没有像Interface Builder那么强大,还有少数的一些功能,IB可以实现,但是Storyboard编辑器不能完成。你也需要一个大显示器,特别是在设计iPad应用 时。

如果你是那种讨厌使用Interface Builder,只愿意编码实现整个UI的人, 那么Storyboard大概不是为你准备的。 从我个人来说,我更希望代码量越少越好,特别是UI代码,所以这个工具对我来说可是个好东西。

你还可以在iOS 5 和Xcode 4.2中使用nib文件。 虽然我们现在有了Storyboard,使用Interface Builder也不是不可以。 如果你要继续用nib并且一直用下去, 但是你也可以将Storyboard和nib一起使用。 这不是一个必须二选一个问题。

在本教程中,我们将会看到你可以用Storyboard来做什么。 我们将要创建的应用可能会看起来没什么意义,但是它向你展示Storyboard最常用的那些操作。

 

开始吧

打开Xcode并且创建一个新项目。 使用Single View Application模板作为我们的起点。然后从这里开始构建我们的应用。

Xcode template options

如下填写模板选项:

  • Product Name: Ratings
  • Company Identifier: the identifier that you use for your apps, in reverse domain notation
  • Class Prefix: leave this empty
  • Device Family: iPhone
  • Use Storyboard: check this
  • Use Automatic Reference Counting: check this
  • Include Unit Tests: this should be unchecked

Xcode创建好项目后,Xcode主窗口显示如下:

Xcode window after creating project

我们的新项目里有两个类,AppDelegate和ViewController, 还有这个教程的主角 MainStoryboard.storyboard。 注意了,这次项目中没有 .xib 文件,也没有MainWindow.xib。

让我们看一看Storyboard。 在左边的项目导航中点击MainStoryboard.storyboard 来打开Storyboard编辑器:

Storyboard editor

Storyboard编辑器看起来很像Interface Builder。 你可以从Object Library(屏幕右下角)中拖动控件到View Controller中来设计布局。 不同的是,Storyboard不是仅仅包含一个控制器,而是你应用中所有的控制器。

控制器,在Storyboard的官方术语中叫做”scene”, 不过 “scene“ 其实就是一个视图控制器。 在这之前,你会为每一个Scene/控制器来创建一个nib文件, 但是现在所有的这些都包含在一个Storyboard中。

在iPhone中一次只能显示这些scene中的一个, 但是在iPad中,你可以同时显示多个, 例如UISplitView或者是Popover。

为了进一步的感受编辑器是如何工作的, 拖动一下控件到控制器的空白区域吧。

Dragging controls from Object Library

左边栏是文档的大纲:

Document outline

在Interface Builder中,这个区域列出了nib中的组件,但是在Storyboard编辑器中,他显示了所有的视图控制器中的内容。现在我们的Storyboard中仅仅有一个视图控制器,但是随着教程的演进,我们将会添加更多的控制器。

在 Scene 下方,有一个迷你版的文档大纲,叫做Dock:

The dock in the Storyboard Editor

Dock显示了scene中最顶级的组件。 每个scene至少有一个First Responder和一个View Controller对象, 但是他也可以拥有其他的顶级组件。 后面会更多的讲解。 Dock可以方便的建立连接。 如果你想将一些其他的东西连接到控制器, 你可以把他的图标拖动到Dock上面。

注意: 你或许没怎么用过 First Responder 。 这是一个代理对象,用于指向当前作为第一事件响应者(first responder)的对象。 它也出现在 Interface Builder 中, 你看可能从来没有用过它。 举个例子,你可以将一个按钮的 Touch Up Inside 事件绑定到 First Responder 的 cut: selector 上。 如果在某一时刻,一个文本框得到了焦点, 那么你就可以按下那个按钮,让这个作为First Responder 的文本框,来剪切它里面的文本到剪贴板上。

运行这个应用, 你应该可以看到它显示的内容和我们在编辑器中设计的完全一样:

App with objects

如果你之前创建过基于nib的应用,那么你会一直有一个MainWindow.xib文件。 这个nib文件包含了一个顶级元素UIWindow, 一个App Delegate的引用,和一个或多个视图控制器。 当你将应用的UI搬到Storyboard后, 就不再需要MainWindow.xib文件了。

No more MainWindow.xib

那么,如果没有MainWindow.xib文件, Storyboard是如何被加载的呢?

让我们来看一下应用的delegate文件, 打开AppDelegate.h, 你将会看到如下内容:

#import <UIKit/UIKit.h>@interface AppDelegate : UIResponder <UIApplicationDelegate>@property (strong, nonatomic) UIWindow *window;@end

要使用Storyboard,你的应用代理对象就必须继承UIResponder(以前都是直接继承自NSObject),并且还有一个UIWindow属性(和以前相比,这个属性不再是一个IBOutlet)。

如果你再看一下 AppDelegate.m , 你会发现它没有做任何事情, 所有的方法都是空的。 即使是 application:didFinishLaunchingWithOptions: 方法, 也只不过简单的返回了一个YES。 在以前,这里会把主视图控制器的视图添加到window上面,或者将window设置到rootViewController属性上面,但是现在,这些都 不需要了。

这个秘密就在于Info.plist文件中, 点击Ratings-Info.plist文件(在Supporting Files分组中), 你将会看到下面的内容:

Setting the main storyboard file base name in Info.plist

在基于nib的项目中,这里会有一个名为NSMainNibFile键,或者叫”Main nib file base name”, 它会告诉UIApplication去加载MainWindow.xib, 然后把它关联到应用中。 而我们现在的Info.plist已经没有这些设置了。

Storyboard应用会使用一个叫做UIMainStoryboardFile的键,或者叫做“Main storyboard file base name”, 来指定应用启动时要加载的Storyboard名称。 当检测到这个设置后,UIApplication将会加载 MainStoryboard.storyboard 文件,并且自动实例化其中的第一个视图控制器, 同时把它的所有视图放到一个新的UIWindow对象中。 不需要写任何代码。

你也可以在Target Summary中看到这些:

Setting Main Storyboard in Target summary

这里面有一个新的iPhone/iPod Deployment Info选项让你来选择是使用Storyboard还是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() 函数的最后一个参数是一个nil值,现在他是NSStringFromClass([AppDelegate class])。

和使用MainWindow.xib最大的不同就是,应用代理不是Storyboard的一部分。因为应用代理不再从nib文件中加载,我们就必须告诉UIApplicationMain我们的应用代理的名字,否则就找不到它了。

把它添加到我的标签上

我们的评分应用使用一个有两个标签的标签化界面。通过Storyboard可以非常容易的创建标签。

切换回到MainStoryboard.storyboard,从Object Library中拖动一个Tab Bar Controller到设计器中。 你或许想把Xcode的窗口最大化,因为Tab Bar Controller还包含了两个视图控制器,所以你需要一些空间来摆放他们。

Adding a new tab bar controller into the Storyboard

这个新的Tab Bar Controller包含了两个预先配置好的子控制器,每个都对应了它的一个Tab。UITabBarController是一个容器控制器,因为他包含 了一个或多个的子控制器。 还有其他两个容器分别是Navigation Controller和Split View Controller(我们将在后面看到它们)。 iOS 5 另外一个很酷的新增特性就是提供了一个新的API让你可以写自己的容器控制器。 在本书的后续章节,我们会有一个教程来说明它。

在Storyboard编辑器中,通过箭头连接Tab Bar Controller和它包含的视图控制器来表示容器中的关系。

Relationship arrow in the Storyboard editor

注意:如果你想同时移动Tab Bar controller和它的子控制器,你可以按住Command键并点击选择多个scene,然后再移动它们(选中的scene会有一个浅蓝色的边框)。

拖动一个Label到第一个视图控制器中,设置他的文本为”First Tab”。 同样也拖动一个Label到第二个控制器中,并且设置他的文本为”Second Tab”。 这样我们就可以清楚的看到在切换Tab时发生了什么。

注意: 当编辑器被缩放时,你不能拖动任何东西到scene中, 所以,你首先需要返回到默认缩放级别中。

选中Tab Bar Controller并且找到Attributes Inspector。 选中内容为Initial View Controller复选框。

Is Initial View Controller attribute

在编辑面板中,原来指向普通控制器的那个箭头,现在指向了Tab Bar Controller:

Arrow indicating initial view controller in Storyboard editor

这样,当我们启动应用的时候,UIApplication就会将Tab Bar Controller作为我们应用的第一个界面。

Storyboard总是会有一个试图控制器用作初始控制器, 作为Stroyboard的入口。

启动这个应用试一试, 这个应用现在有一个Tab Bar并且你可以在这两个Tab之间来回切换:

App with tab bar

Xcode实际上有一个用来创建标签化应用的模板来供我们使用(不出意料,叫做Tabbed Application template),但是最好还是要知道它的原理, 这样在必要的时候,你也可以手动创建它。

你可以删除那个被模板默认添加进来的控制器,我们已经不再需要它了。 现在Storyboard仅包含了Tab Bar和它的两个子控制器。

顺便说一下,如果你连接多于5个scene到Tab Bar Controller中,它会自动放置一个 “更多” 标签,非常漂亮。

增加一个Table View Controller

现在附加到Tab Bar Controller的两个Scene都是普通的UIViewController。 我想把第一个标签的scene替换成一个UITableViewController。

点击第一个控制器, 选中它并且删除它。 从Object Library中拖出一个新的Table View Controller到面板中。

Adding a new table view controller to the Storyboard

当Table View Controller被选中后,在Xcode的菜单栏中选择 EditorEmbed InNavigation Controller。 这样会增加另外一个控制器到面板中:

Embedding in a navigation controller

你也可以从Object Library中拖出一个Navigation Controller, 但是Embed In这个方式更加简单。

因为Navigation Controller也是一个容器控制器(和Tab Bar Controller一样), 它有一个箭头指向Table View Controller。你也可以在文档大纲中看到他们之间的关系。

View controller relationships in outline of Storyboard editor

注意到,嵌入的Table View Controller包含有一个navigation bar。 Storyboard自动添加了这个,因为这个Scene将会被显示到Navigation Controller的框架中。 它不是一个真正的UINavigationBar, 而是用来模拟的。

如果你看一下Table View Controller的Attributes Inspector,可以在最上面看到模拟选项:

Simulated metrics in Storyboard editor

“Inferred” 是Storybard的默认设置, 它的意思是当一个Scene包含在navigation controller中的时候,将会显示一个navigation bar, 同样的,在tab bar controller中,会显示一个tab bar。 你也可以覆盖这个设置, 但要记住,他们只是用来帮你设计界面的。 这些模拟选项不会在运行时实际的出现,他们只是在设计界面时用来模拟运行时的最终效果。

让我们把这些新的Scene连接到Tab Bar Controller中。 按住Ctrl然后从Tab Bar Controller拖动到Navigation Controller:

Connecting scenes in the storyboard

当你这样做之后,一个小的弹出框显示出来:

Create relationship segue

选择 “Relationship – viewControllers” 这个选项。这样会在这两个Scene中创建一个新的关系:

Relationship arrow in the Storyboard editor

Tab Bar Controller有两个这样的关系,每个Tab对应一个。 Navigation Controller自己也有一个关系连接到Table View Controller。 还有另外一种箭头, 叫做Segue,我们在后面就会讨论它。

当我们建立了这个新的连接, 一个新的Tab就会被添加到Tab Bar Controller, 叫做 “Item”. 我想让这个新的Scene作为第一个Tab,所以,拖动这些Tab来改变它们的顺序:

Rearranging tabs in the Storyboard editor

运行应用看一下, 第一个Tab现在包含了一个里面有table view的navigation controller。

App with table view

在我们给应用增加一些实际功能之前,让我们稍微整理一下Storyboard。 我想要把第一个Tab的名字改成”Players”,把第二个改成”Gestures”。 你不能通过Tab Bar Controller来改变他们, 但是你可以通过连接到每个Tab的视图控制器来改变他们。

当你将一个视图控制器连接到Tab Bar Controller后, 它会得到一个Tab Bar Item对象,你可以用Tab Bar Item来设置tab的标题和图片。

在Navigation Controller中,选择Tab Bar Item, 在Attributes Inspector中设置它的Title为 “Players”:

Setting the title of a Tab Bar Item

把第二个Tab Bar Item的名称改为 “Gestures”。

我们应该也需要在这些tab上面放置一些图片。在教程资源中包 含了一个Images子目录。 把这个目录添加到项目中。 在名为”Players”的 Tab Bar Item 的 Attributes Inspector 中,选择Players.png 这张图片。 你可能已经猜到了, 也要为”Gestures” 这个Tab Item设置Gestures.png这张图片。

同样的,Navigation Controller中的子控制器,也有一个Navigation Item用来配置导航条。 选中Table View Controller中的Navigation Item, 将它的Title改为”Players”。

另外,你还可以双击导航条来改变它的Title。(注意:你应该双击Table View Controller中模拟的导航条,而不是Navigation Controller中的那个。)

Changing the title in a Navigation Item

运行应用,令人惊叹的事情发生了, 所有这些都没有写一行代码!

App with the final tabs

原型单元格

你可能注意到了,当我们添加Table View Controller之后,Xcode出现了这样的警告:

Xcode warning: Prototype cells must have reuse identifiers

警告的消息如是这样,“Unsupported Configuration: Prototype table cells must have reuse identifiers”。当你添加Table View Controller到storyboard后, 它默认会使用原型单元格,但是我们并没有正确的配置它,所以才会出现警告。

原型单元格是Storyboard和相比nib之下,提供的一项非常酷的优势特性。 以前, 如果你需要一个自定义的单元格,你只能用代码为单元格添加子视图,或者为单元格单独创建一个nib文件,然后再通过一些特殊的技巧加载它。 这样还是可行的,但是原型单元格让这一切变得更加简单。 现在,你可以直接在Storyboard编辑器中,设计你自定义的单元格。

Table View Controller默认有一个空的原型单元格。 选中它,然后在Attributes Inspector中设置Style为Subtitle。这样会立即将单元格的外观改变成包含两个Label单元格。 如果你之前使用过TableView并且创建过自己的单元格,你会发现这个和UITableViewCellStyleSubtitle是一样的。 借助原型单元格,你可以像我们刚才那样,使用任何一个内建的单元格样式,或者创建一个你自己的样式(这个是我们一会儿要做的)。

Creating a Prototype cell

设置Accessory属性为Disclosure Indicator, 并且把单元格的Reuse Identifier设置成“PlayerCell”。 这样就可以消除Xcode的警告了。 所有的原型单元格仍然还是一个UITableViewCell对象,所以他们还是应该有一个Reuse Identifier。 Xcode只是确保我们不会忘了它(至少我们中会注意警告的人)。

运行应用, …没有任何改变。 这并不奇怪,我没有为Table指定数据源,所以它还不知道该怎么显示每一行。

在项目中创建一个新文件。选择 UIViewController 模板。将这个类命名为PlayersViewController并且让它继承UITableViewController。 “With XIB for user interface option” 这个选项应该是未选中状态, 因为我们已经在storyboard中设计好了这个控制器。 今天没有nibs!

Creating a view controller with the table view controller template

返回Storyboard编辑器, 选中Table View Controller, 在Identity Inspector 中,设置Class为PlayersViewController。 这是将Storyboard中的Scene和你自己的控制器子类相关联的关键一步。 千万不要忘了这个,否则你自己的类不会被使用。

Setting the class name in the identity inspector

从现在开始,当你运行应用,Storyboard中的table view controller将会是PlayersViewController类的一个实例。

为PlayersViewController.h增加一个可变数组的属性:

#import <UIKit/UIKit.h>@interface PlayersViewController : UITableViewController@property (nonatomic, strong) NSMutableArray *players;@end

这个数组将会包含我们应用主要的数据模型。 它包含Player对象。 让我们现在就创建Player类。 用Objective-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是一个简单的容器对象,有三个属性: 玩家的名称,他正在玩的游戏名称,还有评分(1星到5星)。

我们将在App Delegate中创建一个数组和一些用来测试的Player对象,然后把他们赋值给 PlayersViewController的playes属性。

在 AppDelegate.m 中,导入Player和PlayersViewController类,并且增加一个新的实例变量 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对象并把它们添加到players数组中。 而我们是这样做的:

 UITabBarController *tabBarController = (UITabBarController *)  self.window.rootViewController;UINavigationController *navigationController =   [[tabBarController viewControllers] objectAtIndex:0];PlayersViewController *playersViewController =   [[navigationController viewControllers] objectAtIndex:0];playersViewController.players = players;

那是什么呢? 我们想要把 players 数组赋值给 PlayersViewController 的 players 属性, 然后它就可以用这个数组来作为数据源。 但是应用代理还不知道 PlayersViewController 这个控制器, 所以我们需要通挖掘 Storyboard 来找到它。

这就是 Storyboard 的其中一点局限性。 Interface Builder 总能够在 MainWindow.xib 中让应用代理中引用到, 并且你可以将顶级控制器连接到应用代理的 outlets 中。 目前这个在 Storyboard 中还是不行的。 你不能在应用代理中引用到顶级控制器。 这很不幸, 但是我们还是总能够通过代码来得到这些引用。

UITabBarController *tabBarController = (UITabBarController *)  self.window.rootViewController;

我们知道,Storybord 的根控制器是一个 Tab Bar Controller, 所以我们可以找到 window 的 rootViewController 并强制转换它。

PlayersViewController 在第一个Tab中的导航控制器内部, 所以我们可以先找到 UINavigationController 对象:

UINavigationController *navigationController = [[tabBarController   viewControllers] objectAtIndex:0];

然后将它当做我们要找的 PlayersViewController 的根控制器。

PlayersViewController *playersViewController =   [[navigationController viewControllers] objectAtIndex:0];

很不幸, UINavigationController 没有 rootViewController 这个属性, 所以我们需要使用 viewControllers 数组。(它倒有一个 topViewController 属性,但是它指向的是栈中的最顶端控制器, 但我们找的是最下端的。 当应用刚启动的时候,我们可以用 topViewController 来得到它, 但这种情况并不总是成立)。

现在,我们有一个充满 Player 对象的数组, 我们可以继续为 PlayersViewController 构建数据源。

打开 PlayersViewController.m, 并修改Table View 的数据源方法:

 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{return 1;}- (NSInteger)tableView:(UITableView *)tableView   numberOfRowsInSection:(NSInteger)section{return [self.players count];}

真正的功效发生在 cellForRowAtIndexPath 方法。 Xcode 模板生成的代码是这样的:

 - (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;}

这毫无疑问,是你一直以来创建你自己的 Table View 的代码。 好吧,以后不会再用到了! 将这个方法替换成:

- (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编辑器中为这个原型单元格指定的重用标识(Reuse Ifentifier), 在我们这个项目中就是 “PlayerCell”。 一定不要忘记设置这个标识,否则这个就不能正常工作了。

因为这个类还不知道 Player 对象, 所以需要在文件顶部,增加一行 #import 语句。

#import "Player.h"

还有,我们别忘了给这个属性加上 synthesize 语句:

@synthesize players;

现在,你可以运行应用了, 看!, Table View 中有了这些玩家信息了:

Table view with data

在这个应用中,我们只用了一个原型单元格, 但是如果你的 Table 需要显示不同种类的单元格, 那么你可以直接添加新的原型单元格到 Storyboard 中。 你可以复制现有的单元格,或者增加Table View 的 Prototype Cells 属性的值。 不过要确保你给每一个原型单元格都指定了重用标识。

仅仅需要一行代码,就可以使用原型单元格了。 我想,这太棒了!

设计你自己的原型单元格

对大多数应用来说,使用标准的单元格样式已经足够了, 但是, 我想在单元格的右边添加一张图片用来表示玩家的评分(用星星表示)。 标准的单元格样式没有提供这样的外观, 所以我们需要创建一个自定义个设计。

切换回到 MainStoryboard.storyboard, 选中Table View 的原型单元格, 设置 Style 属性为 Custom。 默认的Label 都消失了。

首先,我们让单元格的高度大一些, 拖动单元格下边框,或者在 Size Inspector 中改变 Row Height 属性都可以。 我用第二种方法设置单元格的高度为 55 点。

从 Objects Library 中拖出两个 Label 到单元格中, 并且让把两个 Label 放在和之前差不多的位置。 调整一些字体和颜色属性。 把他们的 Highlighted color 属性都设置成 white。 这样在用户点击单元格后单元格的背景变成蓝色时,看起来会好看一点。

拖动一个 Image View 到单元格中,并放置到右边, 紧挨着右边的指示箭头。 设置宽度为81 点, 高度不是很重要。
为了让任何放到这里的图片都不会被拉伸, 设置它的 Mode 属性为 Center (在 Attributes Inspector 中的 View 选项卡中)。

我将Label都设置成210点宽,所以他们不会和 Image View 重叠上。 原型单元格最终的设计看起来是这样的:

Prototype cells with a custom design

因为这是一个自定义单元格, 我们不能再用 UITableViewCell 的 textLabel 和 detailTextLabel 属性来放置文本。 这些属性指向的 Label 已经不在我们的单元格上面了。 我们需要使用 tags 来找到我们的 Label。

将 Name Label 的 tag 设为 100, Game Label 的tag 设为 101, Image View 的 tag 设为 102, 你可以在 Attributes Inspector 中设置它们。

然后打开 PlayersViewController.m , 修改 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;}

这里用到了一个新方法 imageForRating, 把这个方法增加到 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;}

已经差不多了. 现在重新运行应用吧。

Wrong cell height for custom cells made in Storyboard editor

恩,看起来没什么问题。 我们修改了原型单元格的高度, 但是 Table View 没有自动调整这些。 有两个方法可以修正它: 我们可以修改 Table View 的 Row Height 属性, 或者实现 heightForRowAtIndexPath 方法。 第一个方法更加简单, 让我们这样弄吧。

注意: 如果你提前不知道单元格的高度, 或者每个单元格有不同的高度, 那么你要用 heightForRowAtIndexPath 方法。

回到 MainStoryboard.storyboard, 在 Table View 的 Size Inspector 中, 设置行高为 55:

Setting the table view row height

顺便说一下, 如果你是通过拖动边缘修改的单元格高度, 而不是输入高度值。 那么Table View 的 Row Height 也会自动跟着改变。

如果运行应用, 它看起来会好很多了。

使用原型单元格的子类

我们的 Table View 已经运转的很不错了, 但是我不太喜欢使用tag来访问原型单元格的Label 和其他视图。 如果我们能把Lable 连接到 outlet 上, 然后使用相应的属性, 那就非常方便了。 结果证明, 我们可以:

在项目中增加一个新文件, 使用 Objective-C 模板。 文件名为 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, 选中原型单元格, 在 Identity Inspector 中修改 Class 属性为 “PlayerCell” 。现在当你用 dequeueReusableCellWithIdentifier 来获得新的单元格时, 它将返回而一个 PlayerCell 的实例, 而不再返回 UITableViewCell。

注意,我给这个类指定的名称和重用标识是一样的 — 他们都叫做 PlayerCell — 但这只是我为了保持前后一致。 类名和重用标识并没有什么关系, 所以你也可以给他们指定不同的名字。

现在,你可以将 Label 和 Image View 都连接到这些 outlet 中了。 选择label 从 Connections Inspector 中拖动到 Table View Cell 上面, 或者用另一种方式, 按住 Ctrl从 Table View 中拖回 Label。

Connecting the player cell

重要提示: 你用该将这些控件绑定到Table View Cell中, 而不是 View Controller! 你知道, 当你的数据源向Table View 请求一个新的单元格时, Table View 给你的不是原型单元格本身, 而是一个*拷贝*(或者,如果可能,一个之前被回收的单元格)。 这说明,会在任何一个时间,同时有多于一个 PlayerCell 实例存在。 如果你将单元格中的一个Label 连接到控制器的 Outlet上面, 那么多个label的拷贝会尝试使用同样一个outlet。 这很快就会引起麻烦。 (另外,将原型单元格的事件连接到控制器上是没问题的。 如果你的单元格上有自定义按钮或其他控件, 你应该这样做。)

现在,我们已经绑定好了属性, 我们可以简化我们的数据源代码:

- (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, 然后我们可以直接使用这里的 Label 和 Image View. 我确实很喜欢原型单元格的使用方式, 这让Table View 少了很多垃圾代码。

你还需要导入 PlayerCell 类:

#import "PlayerCell.h"

运行应用。 当你启动应用后, 它看起来和之前一样, 但在这一切的后面, 我们正在使用我们自定义的单元格。

这里有一些设计上的技巧。 当你在设计自己的单元格时, 这里有一些事情需要注意一下。 首先, 你应该设置 Label 的高亮颜色, 这样当用户点击单元格式,他们看起来会比较好。

Selecting the proper highlight color

第二, 你要保证你加入的内容是灵活的, 当单元格的尺寸改变时,里面内容的尺寸会随着它调整。 当你为单元格提供删除和移动能力的时候, 单元格的尺寸就将会改变。

Segues 介绍

现在是时候为我们的 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:

Creating a segue in the storyboard editor

松开鼠标按键, 一个小弹出框就出现了:

Popup to choose Segue type - push, modal, or custom

选择 Modal。 这样就在 Players 界面和这个 Navigation Controller 之间创建了一个新类型的箭头:

A new segue in the storyboard editor

这种类型的连接被称之为 segue(发音为:seg-way)),它表示了从一个界面切换到另一个界面。 目前为止我们建立的连接都是表示控制器之间的关系的。 而Segue, 和我们之前建立的连接不同, 它改变了屏幕上显示的界面。 他们可以通过点击按钮,单元格,手势等方式来触发。

segue最酷的一件事就是,你不再需要为展现新界面写任何代码了,也不需要为你的按钮绑定IBAction了。 我们刚才做的,从 Bar Button Item 拖动到下一个界面, 这就足以创建这个界面切换了。 (如果你的控件已经绑定了 IBAction 事件, 那么 segue 会覆盖它)。

运行应用, 然后点击这个 + 按钮,一个新的Table View 将会从屏幕上滑出来。

App with modal segue

这就是 “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。

Setting the title of the navigation bar to

然后修改 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 然后从按钮拖动到控制器, 然后从弹出框中找到相应的动作名称。

Connecting the action of a bar button item to the view controller in the storyboard editor

在 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”:

Setting the segue identifier

再次运行这个应用, 点击 Cancel 或者 Done 按钮, 就会按照预期关闭当前界面并且返回到玩家列表界面。

注意: 完全可以在弹出的模态界面中调用 dismissViewControllerAnimated:completion: 方法。 并不是必须要在代理中使用这个方法。 不过,我个人比较喜欢把它放在代理中, 但是,如果你向让模态界面来关闭它自己也是没问题的。 有一点你需要注意的: 如果你使用以前的这个方法 [self.parentViewController dismissModalViewControllerAnimated:YES] 来关闭界面的话,它不会正常工作了。 现在可以用 self.presentingViewController 而不是 self.parentViewController 来调用这个方法, 这是iOS 5 新增加的一个属性。

顺便说一下,segue 的 Attributes Inspector 中, 有一个 Transition 属性, 你可以选择不同的切换动画:

Setting the transition style for a segue

可以都试试他们,找到哪个是你最喜欢的。 但是不要改变 Style 设置。 因为这个界面是模态的,修改其他选项会导致应用崩溃。

我们将会在教程中多次用到代理模式。 这里是在两个 scene 之间建立连接的步骤。

  1. 在源 scene 中的按钮或其他控件上创建 segue 到目标 scene。(如果你想用模态的方式展现新界面, 那么一般目标应该是 Navigation Controller)
  2. 为 segue 设置一个唯一标识。(它仅仅需要在源 scene 中唯一, 不同的 scene 可以用同样的标识名)
  3. 为目标 scene 创建代理协议
  4. 调用 Cancel 和 Done 按钮上的代理方法, 另一方面, 你的目标 Scene 需要和源 Scene 进行通讯。
  5. 让源 Scene 实现代理协议。 它应该在 Cancel 或 Done 按钮按下的时候关闭目标控制器。
  6. 在源 Scene 中实现 prepareForSegue 方法, 在这里设置 destination.delegate = self; 。

因为没有反向的 Segue, 所以代理是必须要有的。 当 Segue 被触发时,会创建一个新的目标控制器。 你当然可以从目标控制器用 Segue 返回源控制器, 但是这样不会得到你期望的效果。

例如,如果你在 Cancel 按钮上,创建了一个返回 Players 界面的 Segue, 它不会关闭新建玩家的界面,也不会返回到 Players 界面, 它会创建一个新的 Players 界面。 这样你就陷入了一个死循环中, 直到应用的内存用尽。

记住:Segue 只能是单向的, 他们只能用于打开一个新界面。 如果要返回或者关闭界面(或者从导航控件的栈中弹出),通常都是用代理来做的。 Segue 仅仅听从于源控制器, 而目标控制器甚至都不知道自己是被 Segue 打开的。

静态单元格

当我们完成这些后, 增加玩家的界面应该是这样:

The finished add Player screen

当然,这是一个分组的Table View,但不同的是,这次我们不用为这个Table View 创建数据源了。 我们可以直接在 Storyboard 编辑器中设计它, 不需要 cellForRowAtIndexPath 方法了。 静态单元格提供的特性,让这成为了可能。

在 Add Player 界面中选择Table View, 并且在 Attributes Inspector 中修改 Content 的值为 Static Cells。 设置 Style 属性为 Grouped, Sections 属性为 2。

Configuring a table view to use static cells in the storyboard editor

当你修改了 Sections 属性, 编辑器会复制现有的 section。 (你也可以在左边的文档大纲中,选择一个特定的 Section, 然后复制它)。

我们的界面中,每个 Section 只需要一行数据, 删除掉那些多余的单元格吧。

选中最上面的 section, 在 Attributes Inspector 中设置 Header 属性为 “Player Name”。

Setting the section header for a table view

拖动一个新的 Text Field 到这个 Section 的单元格中。 删除它的边框,这样你就不能看到文本是从何处开始和结束的了。 设置字体为 System 17, 并且取消 Adjust to Fit。

我们将在 PlayerDetailsViewController 中用 Xcode 的 Assistant Editor 功能为这个文本框创建一个outlet。 用工具栏上的按钮打开 Assistant Editor*(那个看起来想一个燕尾服的按钮)。 它应该会自动打开 PlayerDetailsViewController.h 。

选中文本框, 然后按住 Ctrl 拖动到 .h 文件中:

Ctrl-dragging to connect to an outlet in the assistant editor

松开鼠标按键, 会出现一个弹出框:

The popup that shows when you connect a UITextField to an outlet

给这个新的 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 界面的最终设计如下:

The final design of the Add Player screen

当你使用静态单元格时, 你的 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 的设计部分了, 现在让它来实际运转起来吧。

让 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 编辑器中使用鼠标或触摸板(双指滑动)上面的滚动手势。 这个可能不是很明显, 但是它确实可以用。

Game Picker 界面

点击 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”。 这就是所有我们需要对这个界面进行的设计:

The design for the Game Picker screen

增加一个新的 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 界面。

App with Choose Game screen

这非常的酷吧? 我们不需要写一行代码,就可以打开一个新界面。 我们仅仅从这个静态单元格拖动到了新的 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;elsecell.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];}

棒极了,我们现在有了一个功能齐全的游戏选择界面!

Choose Game screen with checkmark


原创粉丝点击