Storyboard和普通的nib相比有很多优点。
- 借助Storyboard,你可以对你应用中所有的界面和它们之间的联系有一个更好的概念上的总览。 因为所有的设计都在单个文件中,而不是分布成许多nib文件, 可以更加容易的找到任何东西。
- Storyboard表明了各个界面之间的切换规则。 这些切换规则叫做“segues”, 按住ctrl键,从一个控制器拖动到另一个就可以创建它们。多亏了segues,能让你用更少的代码来处理UI。
- Storyboard让UITableView使用起来更加简单, 它提供了原型单元格(prototype cells)和静态单元格(static cells). 你几乎可以完全在Storyboard编辑器中来设计你的UITableView,大大减少了你的代码量。
Dock显示了scene中最顶级的组件。 每个scene至少有一个First Responder和一个View Controller对象, 但是他也可以拥有其他的顶级组件。 后面会更多的讲解。 Dock可以方便的建立连接。 如果你想将一些其他的东西连接到控制器, 你可以把他的图标拖动到Dock上面。
要使用Storyboard,你的应用代理对象就必须继承UIResponder(以前都是直接继承自NSObject),并且还有一个UIWindow属性(和以前相比,这个属性不再是一个IBOutlet)。
如果你再看一下 AppDelegate.m , 你会发现它没有做任何事情, 所有的方法都是空的。 即使是 application:didFinishLaunchingWithOptions: 方法, 也只不过简单的返回了一个YES。 在以前,这里会把主视图控制器的视图添加到window上面,或者将window设置到rootViewController属性上面,但是现在,这些都不需要了。
这个秘密就在于Info.plist文件中, 点击Ratings-Info.plist文件(在Supporting Files分组中), 你将会看到下面的内容:
在基于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中看到这些:
这里面有一个新的iPhone/iPod Deployment Info选项让你来选择是使用Storyboard还是nib文件来启动应用。
设置初始化controller
选中Tab Bar Controller并且找到Attributes Inspector。 选中内容为Initial View Controller复选框。
为controller添加一个navgationController
当Table View Controller被选中后,在Xcode的菜单栏中选择 Editor ->Embed-> InNavigation Controller。 这样会增加另外一个控制器到面板中:
把这新的Scene连接到Tab Bar Controller中
按住Ctrl然后从Tab Bar Controller拖动到Navigation Controller:
当你这样做之后,一个小的弹出框显示出来:
选择 “Relationship – viewControllers” 这个选项。这样会在这两个Scene中创建一个新的关系:
Tab Bar Controller有两个这样的关系,每个Tab对应一个。 Navigation Controller自己也有一个关系连接到Table View Controller。 还有另外一种箭头, 叫做Segue,我们在后面就会讨论它。
当我们建立了这个新的连接, 一个新的Tab就会被添加到Tab Bar Controller, 叫做 “Item”. 我想让这个新的Scene作为第一个Tab,所以,拖动这些Tab来改变它们的顺序:
正确设置table view controller
你可能注意到了,当我们添加Table View Controller之后,Xcode出现了这样的警告:
警告的消息如是这样,“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是一样的。 借助原型单元格,你可以像我们刚才那样,使用任何一个内建的单元格样式,或者创建一个你自己的样式(这个是我们一会儿要做的)。
设置Accessory属性为Disclosure Indicator, 并且把单元格的Reuse Identifier设置成“PlayerCell”。 这样就可以消除Xcode的警告了。 所有的原型单元格仍然还是一个UITableViewCell对象,所以他们还是应该有一个Reuse Identifier。 Xcode只是确保我们不会忘了它(至少我们中会注意警告的人)。
创建controller文件,做为storyboard对应controller得父类:
在项目中创建一个新文件。选择 UIViewController 模板。将这个类命名为PlayersViewController并且让它继承UITableViewController。 “With XIB for user interface option” 这个选项应该是未选中状态, 因为我们已经在storyboard中设计好了这个控制器。 今天没有nibs!
返回Storyboard编辑器, 选中Table View Controller, 在Identity Inspector 中,设置Class为PlayersViewController。 这是将Storyboard中的Scene和你自己的控制器子类相关联的关键一步。 千万不要忘了这个,否则你自己的类不会被使用。
从现在开始,当你运行应用,Storyboard中的table view controller将会是PlayersViewController类的一个实例。
找到storyboard对应得controller:
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];
cell自动创建并复用
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"PlayerCell"];
如果当前没有可以重复使用的单元格, 这里将会自动创建一个新的原型单元格,并且返回给你。 你要做的仅仅是提供一下你在Storyboard编辑器中为这个原型单元格指定的重用标识(Reuse Ifentifier), 在我们这个项目中就是 “PlayerCell”。 一定不要忘记设置这个标识,否则这个就不能正常工作了。
果你的 Table 需要显示不同种类的单元格, 那么你可以直接添加新的原型单元格到 Storyboard 中。 你可以复制现有的单元格,或者增加Table View 的 Prototype Cells 属性的值。 不过要确保你给每一个原型单元格都指定了重用标识。
动态改变cell得高度:
我们修改了原型单元格的高度, 但是 Table View 没有自动调整这些。 有两个方法可以修正它: 我们可以修改 Table View 的 Row Height 属性, 或者实现 heightForRowAtIndexPath 方法。 第一个方法更加简单, 让我们这样弄吧。
注意: 如果你提前不知道单元格的高度, 或者每个单元格有不同的高度, 那么你要用 heightForRowAtIndexPath 方法。
回到 MainStoryboard.storyboard, 在 Table View 的 Size Inspector 中, 设置行高为 55:
顺便说一下, 如果你是通过拖动边缘修改的单元格高度, 而不是输入高度值。 那么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
回到 MainStoryboard.storyboard, 选中原型单元格, 在 Identity Inspector 中修改 Class 属性为 “PlayerCell” 。现在当你用 dequeueReusableCellWithIdentifier 来获得新的单元格时, 它将返回而一个 PlayerCell 的实例, 而不再返回 UITableViewCell。
注意,我给这个类指定的名称和重用标识是一样的 — 他们都叫做 PlayerCell — 但这只是我为了保持前后一致。 类名和重用标识并没有什么关系, 所以你也可以给他们指定不同的名字。
现在,你可以将 Label 和 Image View 都连接到这些 outlet 中了。 选择label 从 Connections Inspector 中拖动到 Table View Cell 上面, 或者用另一种方式, 按住 Ctrl从 Table View 中拖回 Label。
重要提示: 你用该将这些控件绑定到Table View Cell中, 而不是 View Controller! 你知道, 当你的数据源向Table View 请求一个新的单元格时, Table View 给你的不是原型单元格本身, 而是一个*拷贝*(或者,如果可能,一个之前被回收的单元格)。 这说明,会在任何一个时间,同时有多于一个 PlayerCell 实例存在。 如果你将单元格中的一个Label 连接到控制器的 Outlet上面, 那么多个label的拷贝会尝试使用同样一个outlet。 这很快就会引起麻烦。 (另外,将原型单元格的事件连接到控制器上是没问题的。 如果你的单元格上有自定义按钮或其他控件, 你应该这样做。)
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:
松开鼠标按键, 一个小弹出框就出现了:
选择 Modal。 这样就在 Players 界面和这个 Navigation Controller 之间创建了一个新类型的箭头:
这种类型的连接被称之为 segue(发音为:seg-way)),它表示了从一个界面切换到另一个界面。 目前为止我们建立的连接都是表示控制器之间的关系的。 而Segue, 和我们之前建立的连接不同, 它改变了屏幕上显示的界面。 他们可以通过点击按钮,单元格,手势等方式来触发。
segue最酷的一件事就是,你不再需要为展现新界面写任何代码了,也不需要为你的按钮绑定IBAction了。 我们刚才做的,从 Bar Button Item 拖动到下一个界面, 这就足以创建这个界面切换了。 (如果你的控件已经绑定了 IBAction 事件, 那么 segue 会覆盖它)。
通过prepareForSegue,在segue切换时,设置delegate和传递数据:
- (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)
这是因为我们没有给Segue设置过标识(identifier)。 prepareForSegue 中的代码检测的是标识为 AddPlayer 的 segue。 建议每次都进行这样的检测, 因为你可能在一个控制器中有多个segue, 并且你需要区分他们(后面我们会做到这个)。
为了解决这个问题, 我们进入Storyboard编辑器,并且点击在 Players 界面和 Navigation Controller 之间的 segue。 注意到 Bar Button Item 被高亮显示了, 所以你可以清楚的看到是哪个控件触发的这个segue。
在 Attributes Inspector 中,设置 Identifier 属性为 “AddPlayer”:
再次运行这个应用, 点击 Cancel 或者 Done 按钮, 就会按照预期关闭当前界面并且返回到玩家列表界面。
这里是在两个 scene 之间建立连接的步骤。
- 在源 scene 中的按钮或其他控件上创建 segue 到目标 scene。(如果你想用模态的方式展现新界面, 那么一般目标应该是 Navigation Controller)
- 为 segue 设置一个唯一标识。(它仅仅需要在源 scene 中唯一, 不同的 scene 可以用同样的标识名)
- 为目标 scene 创建代理协议
- 调用 Cancel 和 Done 按钮上的代理方法, 另一方面, 你的目标 Scene 需要和源 Scene 进行通讯。
- 让源 Scene 实现代理协议。 它应该在 Cancel 或 Done 按钮按下的时候关闭目标控制器。
- 在源 Scene 中实现 prepareForSegue 方法, 在这里设置 destination.delegate = self; 。
静态单元格
当我们完成这些后, 增加玩家的界面应该是这样:
当然,这是一个分组的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;
我告诉过你,这种方式在原型单元格中是不能用的, 但是在静态单元格中是可以的。 因为每个静态单元格只有一个实例(不像原型单元格, 他们从来不会被复制), 所以,将它们的子视图连接到控制器上也是完全可以的。
设置第二个 Section 中的静态单元格的 Style 属性为 Right Detail。 这给了我们一个标准的样式去操作。 修改左边的 Label 的文字为 “Game”, 并且为这个单元格设置一个向右指示的箭头。 为右边的 Label(文本为 “Detail” 的那个)创建一个 outlet, 并且命名为 detailLabel。 这个单元格上面的 Label 只是普通的 UILabel 对象。
Add Player 界面的最终设计如下:
当你使用静态单元格时, 你的 Table View 控制器不需要数据源。 因为我们是用 Xcode 模板创建的 PlayerDetailsViewController 类, 它仍然会有数据源相关的默认代码, 所以让我们把这些代码删除掉吧, 现在我们不需要它了。 删除下面两个代码断之间的所有代码:
segues实例controller,初始化成员的选择:
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中的视图和视图控制器,还是以同样的方式编码和解码。