本文由CocoaChina翻译小组@TurtleFromMars翻译自raywenderlich,原文:Storyboards Tutorial in Swift: Part 2swift
更新记录:该Storyboard教程由Caroline Begbie更新iOS 8和Swift相关内容。原文做者为教程编纂组的成员Matthijs Hollemans。数组
2014/12/5更新:更新至 Xcode 6.2 Beta。app
若是你想学习Storyboard,你来对地方了!编辑器
在本系列Storyboard教程的第一部分,咱们已经学习了如何使用Interface Builder建立并链接不一样的视图控制器,还有如何直接在Storyboard编辑器中建立自定义表项。ide
本教程的第二部分,也是最终部分,内容包括segue(转场),static table view cell(静态表项),添加玩家页面和游戏选择页面!函数
咱们从上部分结束的地方开始,请先打开以前的项目,或者下载上半部分教程的示例代码。工具
好,如今让咱们一块儿探索Storyboard的其余酷炫特性吧!布局
转场(Segue)性能
让咱们向Storyboard中继续添加视图控制器,建立一个让用户添加新玩家的页面。学习
打开Main.storyboard,在包含表视图的那个Players场景的导航栏右侧拖入一个Bar Button Item(栏按钮项),在属性检查器中将Identifier设为Add,使其成为标准添加(加号)按钮。
当用户点按这个按钮时,你但愿App会弹出一个模态页面让用户输入新玩家的详细信息。
在Players场景的右边拖入一个新的Navigation Controller(导航控制器)。记得双击面板能够缩放画面腾出空间。新加入的导航控制器附带一个表视图控制器,很方便。
这里有个小技巧:选择刚才在Players页面里加入的加号按钮,按住control键把它拖向新建的导航控制器,松手,在弹出的小选单中选择modal(模态)。
还记得吗:当Storyboard面板处于缩小状态时,没法添加或修改内容。若是在建立转场时遇到问题,请尝试双击放大!
如今Players页面和导航控制器之间多了一个新箭头。
这 种链接的类型叫作segue(转场,读做seg-way,源自电影术语,原指两个场景间的过渡衔接),表示一个页面到另外一个页面的过渡。此前咱们所见的 Storyboard链接描述的都是视图控制器的包含关系,而转场是用来切换页面的。转场能够由点击按钮、表项、手势等条件触发。
使用转场的好处是,不再用为呈现新页面写代码了,也不用把按钮链接到IBAction方法上,你只须要在Storyboard中从一个栏按钮项拖到下一个页面就能够建立过渡了。(注:若是你的控件已经绑定了IBAction链接,该链接会被转场屏蔽。)
运行App,点击加号按钮,一个新的表视图会从屏幕下方滑入。
这就是所谓的模态转场。新页面彻底覆盖原页面,在关闭模态页面以前,用户只能在新页面进行交互。后面咱们还会看到push(入栈)转场,这种转场会把新页面压入导航控制器的导航栈(navigation stack)。
如今新页面还没什么用,连关闭页面返回都作不到,有去无回,由于转场是单向操做。
为返回页面,Storyboard提供了unwind(回退)转场。接下来咱们要实现返回功能,主要分三个步骤:
1. 建立让用户点选的控件,一般是个按钮。
2. 在你想返回的控制器建立回退方法。
3. 在Storyboard中将控件与回退方法链接。
首 先打开Main.storyboard,选择新的表视图控制器场景(叫“Root View Controller”的那个)。双击导航栏,把标题改为“Add Player”。而后在导航栏添加两个栏按钮项,在属性检查器中设置左侧按钮的Identifier为Cancel,右侧按钮为Done,并将右侧按钮的 Style改为Done。
接 下来在项目中用Cocoa Touch Class模板添加一个新文件,命名为PlayerDetailsViewController并令其继承 `UITableViewController`。要把这个类关联到Storyboard,先切回Main.storyboard,选择添加玩家的场景, 而后在身份检查器(Identity inspector)中设Class为PlayerDetailsViewController。这个步骤我常常忘掉,在此特意提醒,还请读者牢记。
如今终于能够建立回退转场了。在PlayersViewController.swift(不是detail那个)的类定义下面添加以下的回退方法:
@IBAction func cancelToPlayersViewController(segue:UIStoryboardSegue) {
dismissViewControllerAnimated(true, completion: nil)
}
@IBAction func savePlayerDetail(segue:UIStoryboardSegue) {
dismissViewControllerAnimated(true, completion: nil)
}
这两个方法在调用时都会解除这个控制器。后面你会改写`savePlayerDetail`,让它名副其实地履行本身的职责。
最后回到Interface Builder,把Cancel按钮和Done按钮链接到相应的action方法上。按住control从栏按钮拖到视图控制器上面的出口(exit)对象上,而后从弹出的选单中选择正确的action名称。
记住取消方法的方法名,建立回退转场时,App中的全部回退方法(形如`@IBAction func methodname(segue:UIStoryboardSegue)`)都会在列表中显示,因此命名方法时要多加注意,避免混淆。
运行App,点击加号按钮,而后测试Cancel和Done按钮。仅仅几行代码就能够实现如此功能。
静态表项(Static Cell)
完成这部分后,添加玩家页面会像这样:
当 然这是一个分组表视图(grouped table view),但没必要为该表建立数据源,也没必要为此编写`cellForRowAtIndexPath`方法,你能够直接在Interface Builder中完成设计。这个特性叫作静态表项(static cell)。
选中Add Player场景的表视图,在属性检查器中设Content为Static Cells,把Style由Plain改为Grouped,并为表视图设置两个分段(section)。
修改Sections属性值时,编辑器会复制已有的分段。(你也能够在左侧的文档大纲中选择特定分段并复制。)
最终页面每一个分段应该只有一行,请在面板或文档大纲中选中并删除多余的表项。
在文档大纲中选择最上面的表视图分段,在属性选择器中设Header字段值为Player Name。
向该分段内拖入一个新的Text Field(文本字段),横向拉长并移除边框,使文本字段控件融入周围环境。设字体为 _System 17.0_ ,勾掉Adjust to Fit选项。
接 下来咱们要用Xcode的Assistant Editor(辅助编辑器)功能为该文本字段在`PlayerDetailsViewController`中建立一个outlet。在 Storyboard中,点击工具栏上的按钮(图标是两个套在一块儿的圆圈)打开辅助编辑器,应该会自动打开 PlayerDetailsViewController.swift(若是没有,在右侧的跳转栏中选择相应文件)。
选择新建的文本字段, 按住control拖到swift文件的类定义下面。在弹出框中将新outlet命名为nameTextField并点击Connect。在点击 Connect后Xcode会在PlayersDetailViewController类中添加属性并在Storyboard中创建链接:
为表项上的视图建立outlet对于原型表项来讲可能会遇到问题,这在上一部分的教程中提到过,不过静态表项就没必要担忧了,由于每一个静态表项都只会有惟一的实例,把子视图与视图控制器的outlet链接彻底没问题。
把第二分段的静态表项的Style设为Right Detail,这会套用一个标准表项样式,双击左侧的label,把文本改成Game,而后为该表项设定Disclosure Indicator(展开方向标)附件。
仿 照刚才的Name文本字段,为右面的label("Detail"的那个)建立outlet并命名为detailLabel,该表项上的label都是常 规`UILabel`对象。在创建链接前选择Detail文本字段时可能须要屡次点击,请确保选择的是label而不是整个表项。完成后如图:
添加玩家页面的最终设计效果如图:
目前在Storyboard中设计的页面尺寸都符合iPhone 5的4英寸屏幕,高度为568点。固然你的App应当在不一样的屏幕尺寸下正常工做,你能够在Storyboard中预览全部的尺寸。
在工具栏上点开辅助编辑器,选择跳转栏中的Preview。点击辅助编辑器左下角的加号添加新的预览尺寸,若是想删除一个屏幕尺寸,选中并按delete键便可。
一个简单的评分App不须要什么花哨的东西,只是使用表视图控制器,页面自动缩放以填满屏幕空间。当你想为不一样的屏幕尺寸适配布局时,你须要使用Auto Layout和Size Classes。
构建并运行App,你会注意到添加玩家页面依然是空白!
表视图控制器在使用静态表项时不须要数据源,而以前你用Xcode模板建立的`PlayerDetailsViewController`类中依然有部分数据源相关代码,静态表项所以没法正常工做,因此静态内容没有显示出来。咱们这就来解决问题!
打开PlayerDetailsViewController.swift文件,删除这一条代码往下的全部内容(注意不要删掉类本身的括号):
// MARK: - Table view data source
如今,自从加入这个类之后Xcode显示的那几条警告(warning)也应该消失了。
运行App,检查使用静态表项的新页面。彻底没有写代码,其实刚才还删了一段代码!
还 要了解一点:静态表项只在`UITableViewController`中有效,虽然Interface Builder容许你在常规`UIViewController`中的表视图对象里添加静态表项,运行时不会发挥做用,缘由是 `UITableViewController`中额外实现了一些用来处理静态表项数据源的操做。在项目中误用的话Xcode甚至会拒绝编译,输出报错信 息:“Illegal Configuration: Static table views are only valid when embedded in UITableViewController instances”。
另外一方面,原型表项在常规视图内的表视图中能够正常工做,但在nib中就没戏了。目前来说,使用原型表项或静态表项就必须使用Storyboard。
你也有可能想在一个表视图中混合使用静态表项和常规的动态表项,很遗憾的是目前的SDK对此支持欠佳。若是你的App有这种需求,请参考苹果开发者官方论坛上的相关帖子寻求可行方案。
注:若是构建的页面上包含的静态表项多到没法在可视范围内所有展现,你能够在Interface Builder中直接利用滚动手势查看,这个功能可能不容易发现,但确实管用。
不过总的来讲该写代码的地方只能靠代码,甚至静态表项的表视图也是如此。前面在把文本字段拖进第一个表项的时候,你可能发现尺寸不大合适,文本字段周围有一点白边,并且用户看不到文本字段的实际范围,若是正好点在边框上,没有弹出键盘,用户会感到困惑。
为避免这种状况,你应该让那一行任意位置接受的点击均可以唤出键盘。要这样作很容易,打开PlayerDetailsViewController.swift并以下添加
tableView(_:didSelectRowAtIndexPath:)`方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
if indexPath.section == 0 {
nameTextField.becomeFirstResponder()
}
}
代码的意思是若是用户点按第一个表项,App应该激活相应文本字段。该分段只有一个表项,你只需使用分段的索引。设文本字段为第一响应者会自动唤出键盘。这只是一小处用户体验优化,但就是这样一个小细节能够给用户省去一点烦恼。
小诀窍:添加delegate委托方法或重写视图控制器方法时,直接输入方法名开头的几个字母(前面不加func),便可在自动补全列表中选择正确的方法。
另外,还应该在Storyboard的属性检查器中把相应表项的Selection Style设为None(本来是Default),不然用户点按文本字段周围的边框时该行会高亮。
好啦,添加玩家页面设计完成。如今咱们要实现功能。
为添加玩家页面实现功能
如今先无论Game这行,只输入玩家名称。
当用户点击Cancel按钮时,页面关闭,用户刚刚输入的数据随之做废。这部分功能直接用回退转场已经实现好了。
而当用户点击Done时,你应该建立一个新的Player对象,参照用户输入填充属性后更新玩家列表。
转场即将发生时,`prepareForSegue(:sender:)`会被调用。你能够重写这个方法,在退出视图以前将数据保存到一个新的Player对象中。
注:不要擅自调用`prepareForSegue`方法,这是UIKit通知你一个转场刚刚被触发的消息。
在PlayerDetailsViewController.swift中,先在类上添加一条属性:
var player:Player!
这条语句并不会将属性实例化,但其中的感叹号把该变量定义为隐式解包可选量(implicitly unwrapped optional),意思是该变量必须被实例化,并且你肯定它在被使用前必定有值。
接下来在PlayerDetailsViewController.swift中添加如下方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SavePlayerDetail" {
player = Player(name: self.nameTextField.text, game: "Chess", rating: 1)
}
}
prepareForSegue(_:sender:)`方法判断转场的标识符是否为`SavePlayerDetail`,当且仅 当断定结果为真时,建立一个新的Player实例,其中game和rating均取默认值。若是此时运行,App会崩溃,由于不存在标识符 `SavePlayerDetail`,player不会被实例化,结合前面的隐式解包可选量定义,引起运行时错误。
小提示:若是App出现诡异的崩溃问题,并且代码看起来彷佛并没有逻辑错误,那么多是在代码中删除过对象或修改过对象名,以至Storyboard引用对象出错。
在Main.storyboard中,在文档大纲里找到Add Player场景,选择链接到`savePlayerDetail`这个action的回退转场,将其标识符改成`SavePlayerDetail`:
而后选择链接到`cancelToPlayersViewController`的回退转场,将其标识符改成`CancelPlayerDetail`。以供`prepareForSegue(_:sender:)`方法判断标识符。
转到PlayersViewController类,以下修改回退转场方法`savePlayerDetail(segue:)`:
@IBAction func savePlayerDetail(segue:UIStoryboardSegue) {
let playerDetailsViewController = segue.sourceViewController as PlayerDetailsViewController
//add the new player to the players array
players.append(playerDetailsViewController.player)
//update the tableView
let indexPath = NSIndexPath(forRow: players.count-1, inSection: 0)
tableView.insertRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
//hide the detail view controller
dismissViewControllerAnimated(true, completion: nil)
}
这会经过传入方法的转场引用获取一个指向`PlayerDetailsViewController`的引用,并借此向数据源中使用的Player数组添加新的Player对象,而后通知表视图在末尾新增了一行,由于表视图和数据源应当保持同步。
你可能会直接调用`tableView.reloadData()`,但仍是为新行插入的操做加入动画效果比较好。`UITableViewRowAnimation.Automatic`会以插入新行的位置自动选用合适的动画,十分方便。
试试看,如今应该能够向列表中加入新玩家了!
性能
现 在Storyboard已经有好几个视图控制器了,你或许会担忧性能问题,不过一次载入整个Storyboard并非什么苦活,Storyboard不 会当即实例化全部的视图控制器,当即载入的只有初始视图控制器。而因为这里的初始视图控制器是一个分页栏控制器,包含的两个视图控制器也会被载入(第一个 分页标签的Players场景和第二个分页标签的场景)。
其余视图控制器只有在转场过去的时候才会被实例化。而当关闭视图控制器的时候,它们会当即被释放,因此内存中只有活跃使用的视图控制器,就好像分别使用nib同样。
实践是检验真理的惟一标准,在PlayerDetailsViewController类中添加构造器(initializer)和析构器(deinitializer):
required init(coder aDecoder: NSCoder) {
println("init PlayerDetailsViewController")
super.init(coder: aDecoder)
}
deinit {
println("deinit PlayerDetailsViewController")
}
你刚刚重写了`init(coder:)`和`deinit`方法,让它们向Xcode调试面板输出信息。如今运行App,打开添加玩家页面,你会发现视图控制器只有在被打开的时候才会分配。
关闭添加玩家页面的时候,不管是点击Cancel仍是Done都会看到deinit析构器的`println()`输出。若是再次打开这个页面,你还会看到`init(coder:)`的输出,这样你应该相信这个事实了:视图控制器是按需加载的,就像手动载入nib同样。
注: 若是你之前用过nib,那么你应该会很熟悉构造器`init(coder:)`,这部分机制延续到了Storyboard中:使用的方法依然是 `init(coder:)`,`awakeFromNib()`和`viewDidLoad()`。Storyboard能够当作附带了过渡信息和关联 信息的一系列nib的集合,而Storyboard内的视图和视图控制器使用与nib相同的方式编码并解析。
游戏选择页面
在添加玩家页面中点选Game行应该打开一个新页面并让用户从列表中选择一个游戏,这意味着下一步要加入另一个表视图控制器,不过此次的页面不是模态显示,而是压入导航栈。
向 Storyboard中拖入一个新的表视图控制器,在添加玩家页面中选择Game表项(确保选中的是整个表项,而不是其中的label),而后按住 control拖到新建的表视图控制器,在二者之间建立转场。在弹出的选单中选择转场类型为Push,而后在属性检查器中把转场的Identifier标 识符设为PickGame。
双击导航栏,将新场景命名为Choose Game。设原型表项的Style为Basic(基本),设重用标识符为GameCell,如图:
在 项目中使用Cocoa Touch Class模板新建一个Swift文件,命名为GamePickerViewController,继承UITableViewController。回 到Storyboard中将游戏选择页面的Custom Class设为`GamePickerViewController`。
如今为新页面添加数据。在GamePickerViewController.swift中,在开头添加games属性,而后重写viewDidLoad函数,像这样:
var games:[String]!
override func viewDidLoad() {
super.viewDidLoad()
games = ["Angry Birds",
"Chess",
"Russian Roulette",
"Spin the Bottle",
"Texas Hold'em Poker",
"Tic-Tac-Toe"]
}
你刚刚新增了一个叫作`games`的字符串数组,并在`viewDidLoad()`中用写定的内容填充数组。
而后以下替换数据源方法:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return games.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("GameCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = games[indexPath.row]
return cell
}
上述代码将`games`数组设为数据源并替换表项的textLabel中的字符串值。
只要数据源准备就绪就应该能正常工做。运行App,点选Game行,新的游戏选择页面会滑入屏幕。如今点击各项不会有什么效果,但因为该页面呈如今导航栈上,你能够直接点击返回按钮,返回原来的添加玩家页面。
不用写代码就能够唤出新页面,是否是很赞?只要按住control从静态表项拖到新场景,写的代码只有填充表视图的内容,并且通常来说比原地设计好的列表要灵活些(由于games数组更方便修改)。
固然新页面要返回数据才有用,为此你要添加一个新的回退转场。
在GamePickerViewController类的上面添加持有选中的游戏的名称和索引的属性:
var selectedGame:String? = nil
var selectedGameIndex:Int? = nil
而后修改`cellForRowAtIndexPath:`:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("GameCell", forIndexPath: indexPath) as UITableViewCell
cell.textLabel?.text = games[indexPath.row]
if indexPath.row == selectedGameIndex {
cell.accessoryType = .Checkmark
} else {
cell.accessoryType = .None
}
return cell
}
这会在当前所选游戏对应的表项附上选中标记(对号),这对用户体验来讲不可或缺。
接着添加`tableview(tableview:didSelectRowAtIndexPath:)`方法:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
tableView.deselectRowAtIndexPath(indexPath, animated: true)
//Other row is selected - need to deselect it
if let index = selectedGameIndex {
let cell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: index, inSection: 0))
cell?.accessoryType = .None
}
selectedGameIndex = indexPath.row
selectedGame = games[indexPath.row]
//update the checkmark for the current row
let cell = tableView.cellForRowAtIndexPath(indexPath)
cell?.accessoryType = .Checkmark
}
这段代码首先会取消选择刚刚点选的行,外观会从灰色高亮变回常规的白色,而后移除对号,并在刚刚点选的行上附加选中标记。
运行App,测试是否正常。点选一个游戏名,相应行会附上选中标记,点选另外一个游戏名,选中标记也随之移动。
按要求来讲点选某行以后应该关闭该页面,不过如今并无自动返回,由于还没有绑定回退转场。
在PlayerDetailsViewController.swift的类上面添加一个持有被选游戏的属性,以便以后在Player对象中保存。令其默认值为"Chess",这样一来新玩家总会有一个选定的游戏。
var game:String = "Chess"
一样在该文件中改写`viewDidLoad()`以在静态表项中游戏名称:
override func viewDidLoad() {
super.viewDidLoad()
detailLabel.text = game
}
添加回退转场方法:
@IBAction func selectedGame(segue:UIStoryboardSegue) {
let gamePickerViewController = segue.sourceViewController as GamePickerViewController
if let selectedGame = gamePickerViewController.selectedGame {
detailLabel.text = selectedGame
game = selectedGame
}
self.navigationController?.popViewControllerAnimated(true)
}
上述代码会在用户从选择游戏场景选中一个游戏后执行。该方法按照选中的游戏更新页面上的label和game属性,而后将GamePickerViewController弹出导航栈。
在Main.storyboard中按住control从表项拖到Exit出口对象,而后从弹出列表中选择`selectedGame:`。
设该回退转场标识符为SaveSelectedGame。
运行App试试看,建立新玩家,点选Game行并选择一个游戏。
不 幸的是,这个回退转场方法是在`tableView(_:didSelectRowAtIndexPath:)`方法前执行的,因此 `selectedGameIndex`并未及时更新。幸运的是你能够重写`prepareForSegue(_:sender:)`方法,在转场以前完 成更新操做。
在GamePickerViewController中添加`prepareForSegue(segue:)`方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SaveSelectedGame" {
let cell = sender as UITableViewCell
let indexPath = tableView.indexPathForCell(cell)
selectedGameIndex = indexPath?.row
if let index = selectedGameIndex {
selectedGame = games[index]
}
}
}
`prepareForSegue(_:sender:)`的sender参数是引起转场的对象,在这里对应选中的游戏表项,因此你能够利用表项的indexPath来在games数组中肯定选中的游戏并在转场发生以前更新`selectedGame`。
如今运行App,选择游戏后玩家的游戏信息会随之更新了。
接下来改写PlayerDetailsViewController的prepareForSegue方法来返回选中的游戏,而不是写定的"Chess"。这样一来,完成添加玩家的操做后,Players场景中会显示玩家实际选择的游戏。
在PlayerDetailsViewController.swift中以下改写`prepareForSegue(_:sender:)`方法:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "SavePlayerDetail" {
player = Player(name: nameTextField.text, game:game, rating: 1)
}
}
完成添加玩家页面并点击Done后,玩家列表会更新正确的游戏信息。
还有一点,当你选择一个游戏,返回添加玩家页面,而后尝试从新选择游戏的时候,以前选定的游戏应该显示选中标记。解决方法是在转场时把PlayerDetailsViewController中保存的选中的游戏传给GamePickerViewController。
仍是在PlayerDetailsViewController.swift中,于`prepareForSegue(segue:,sender:)`方法的末尾添加如下代码:
if segue.identifier == "PickGame" {
let gamePickerViewController = segue.destinationViewController as GamePickerViewController
gamePickerViewController.selectedGame = game
}
注意:如今有两条检查`segue.identifier`的 `if`语句。SavePlayerDetail是返回玩家列表的回退转场,PickGame是前往游戏选择页面的入栈转场。添加的代码会在 GamePickerViewController的视图加载以前更新其中的`selectedGame`。
打开GamePickerViewController.swift并在`viewDidLoad()`末尾添加如下代码:
if let game = selectedGame {
selectedGameIndex = find(games, game)!
}
这两行代码获取从 PlayerDetailsViewController传进的selectedGame并将其转换成正确的索引。`find()`函数会在games数 组中查找匹配selectedGame的String,而后返回匹配元素的索引,赋值给selectedGameIndex,这个索引用来在对应表项上设 置选中标记。
好。如今选择游戏页面功能实现完成!
何去何从?
这是整个教程的示例项目,包含上述全部源代码。
可喜可贺,如今你已经了解Storyboard编辑器的基本用法,可以建立包含多个视图控制器并能经过转场在场景之间切换的App!在一处集中管理多个视图控制器和互相的关联,让总体把握App的样子更加容易。
你也看到了自定义表视图和表项有多么容易。有了静态表项,不用实现全部的数据源方法也能够构建一些界面。
若是想深刻了解Storyboard,请参阅咱们的书籍iOS 8教程,其中涵盖了最新的通用Storyboard(Universal Storyboard)。