在本章中,我们将使用导航控制器并继续创建FoodTracker app的导航流程。在课程结束后,你将有一个导航策略和交互流程。当你完成时,你的app看起来如下所示:
学习目标
在课程结束时,你将学会:
1.在storyboard中的导航控制器内嵌入一个已经存在的视图控制器
2.在两个视图控制器之间创建桥梁
3.在storyboard的Attributes inspector内编辑一个segue的属性
4.通过使用prepareForSegue(_:sender:)来在视图控制器之间传递数据
5.执行一个unwind segue(用于实现向后导航的一个segue类型)
6.使用stack view来创建健壮,灵活的布局(Xcode 7.0)
添加一个segue到向前导航
数据显示如预期一样,是时候提供一个方法来从meal list场景到meal场景的导航了。场景之间的转换通过调用segues(类似android的intent)
在创建一个segue之间,你需要配置你的场景。首先你把table view controller放入一个导航控制器的内部。导航控制器通过向前和向后来管理一系列view controller的转换。通过一个特定的导航控制器来管理一个view controllers集,这被称为导航堆栈,第一个添加到栈中的会成为root view controller,它永远不会从导航堆栈弹出。
添加导航控制器到你的meal list场景
1.打开你的storyboard,Main.storyboard
2.选择table view controller(你也可以通过 scene dock来选择)
3.在table view controller被选中的情况下,选择Editor > Embed In > Navigation Controller
Xcode会添加一个新的导航控制器到你的storyboard中,设置storyboard的入口点,并在新的导航控制器和已存在的table view控制器之间创建一个关系
在画布中,会有一个连接到控制器icon,它是root view controller的关系。table view controller是导航控制器的root view controller。storyboard的入口点设置为导航控制器,是因为导航控制器是一个现有的table view controller的容器。你可能注意到table view顶部有一个栏了。这就是导航栏。每一个在导航栈中获得一个导航栏的控制器,能包含向前,向后导航。接下来,你需要添加一个按钮到这个导航栏来过渡到meal场景。
检查站:运行你的app。在你table view的上方,应该可以看到额外的空间。这是导航控制器提供的导航栏。导航栏会扩展它的背景到状态栏的顶部,所以状态栏不会和你的内容重叠了
为场景配置导航栏
现在,你将添加一个标题和一个按钮到导航栏。导航栏从当前显示的导航controller中,获得他们的标题。导航控制器本身没有标题,它包裹的内容才有标题。你使用meal list的导航item设置标题,而不是在导航栏直接设置它。
在meal list配置导航栏
1.双击meal list场景中的导航栏(点击中间)
会出现一个光标,让你输入文本
2.输入Your Meals然后按下Return来保存
3.打开Object library
4.找到 Bar Button Item对象
5.拖动Bar Button Item对象到导航栏的最右边
一个Item的按钮会出现在,你松开的地方
6.选择 bar button item,打开 Attributes inspector
7.在 Attributes inspector,在标签Identifer旁,选择Add
按钮会变成一个(+)
检查点,执行你的APP,导航栏会显示一个标题和一个(+)按钮。现在这个按钮不会做任何事,接下来我们会修复它
你想要通过点击(+)按钮跳转到meal场景,所以我们会通过点击按钮触发一个segue来跳转到那个场景
配置(+)按钮
1.在画布上,选择(+)按钮
2.按住Control键拖动按钮到meal场景中
一个Action Segue的快捷菜单出现,松开的地方
Action Segue菜单允许你选择segue的类型
4.这里我们选择Show
Xcode设置Action Segue并配置meal场景用于显示,现在Interface Builder中的界面如下:
检查站:执行你的APP,你现在可以点击(+)按钮并可以从meal list场景导航到meal场景了。因为你使用导航控制器来显示一个segue,那向后导航已经自动帮你处理好了,会自动出现一个back按钮在你的meal场景中。这意味着你能点击back按钮回到meal list场景
推送风格导航用于显示segue。但是在增加item时,这可能并不是你想要的。推送导航设计于钻取界面,无论用户选择什么,你应该提供更多信息。增加一个item,另一方面是一个模式的操作,用户执行一个动作,这是完整的,自成体系的,然后从场景返回到主导航。对于这个类型的场景展示,有一个合适的方法叫modal segue。(需要用户在展示的控制器中执行一个操作,才能返回到主流程)
如果要删除已存在的segue并创建一个新的,在Attributes inspector中简单的改变segue的风格即可。如大多数在storyboard可选的元素一样,你能使用Attributes inspector来编辑一个segue的属性
改变segue的风格
1.在meal list场景和meal场景之间选中segue(那个小箭头)
2.在Attributes inspector中,找到Seque标签,下拉选择Present Modally
3.在Attributes inspector中,找到Identifier标签,输入AddItem,然后Return
后面我们会需要这个标示符来识别segue
一个modal的视图控制器不被添加到导航栈,因此它不会有一个导航栏。然而,你想要保持导航栏来提供给用户视觉连续性。当展示modal时,为了给meal场景一个导航栏,它会嵌入在自己的导航控制器中
添加一个导航控制器到meal场景
1.选中 meal scene
2.选中meal场景的情况下,选择Editor > Embed In > Navigation Controller
和以前一样,Xcode添加一个导航控制器并显示一个导航栏在meal场景的顶部,接下来,配置这个导航栏,我们添加两个按钮Cancel,Save。和一个标题。你会用来这两个按钮来执行一些动作。
在meal场景中配置导航栏
1.双击meal场景中的导航栏(点中间),出现一个光标,让你输入文本
2.输入New Meal然后按Return
3.在Object library中拖动Bar Button Item对象到导航栏最左边
4.在Attributes inspector中,找到Identifier标签,选择Cancel。
按钮的文本变成了Cancel
5.在Object library中拖动Bar Button Item对象到导航栏右边
6.在Attributes inspector中,找到Identifier标签,选择Save
按钮的文本变成了Save
检查站:执行的app,点击(+)按钮。然后会出现meal场景,但meal场景中不会有back导航。你会在上方看见两个按钮(Cancel和Save)。但这两个按钮没有绑定动作,你点击它们没有任何反应。接下来我们会配置这两个按钮的动作
使用自动布局完成UI(Xcode7下可用)
这是一段时间以来,对于你原来建立的用户界面,有很多事情发生了改变。在这一点上,你不用对你的布局做出任何改变,所以自动布局看起来很好用。
要做到这一点,你需要对stack view做一些简单的调整。
更新stack view的布局
1.在meal场景中,选中stack view
2.在画布的底部右边,打开Resolve Auto Layout Issues菜单
3.选择Update Constraints
元素的位置还是没变,但 stack view现在被固定于导航栏上,而不是View的顶部边缘。现在UI看起来如下:
检查站:执行的app。一切看起来都和以前一样
在Meal List中保存新的Meals
接下来我们要实现一个添加新菜谱的功能。当用户输入菜谱名称,评级和照片时,点击Save按钮,你想要MealViewController配置一个Meal对象,然后返回适当的信息到
MealTableViewController的菜谱列表场景中来显示。首先我们添加一个Meal属性到MealViewController中
添加一个Meal属性到MealViewController中
1.打开MealViewController.swift
2.找到MealViewController.swift,在ratingControl的outlet下,添加以下属性
/* This value is either passed by `MealListTableViewController` in `prepareForSegue(_:sender:)` or constructed as part of adding a new meal. */ var meal = Meal?()
这个属性是可选的,因为它有可能为nil的情况
你只需要在点击Save按钮时,关心配置和传递Meal。所以我们需要添加一个Save按钮的outlet到MealViewController.swift中
连接Save按钮到MealViewController代码中
1.打开你的storyboard
2.打开assistant editor
3.在storyboard中,选中Save按钮
4.按住Control键拖动Save按钮到MealViewController.swift中的
ratingControl属性下
5.在弹出的对话框中,Name标签旁,输入saveButton,然后点击Connect
创建一个Unwind Segue
现在的任务是当用户点击Save按钮时,传递Meal对象到MealTableViewController。当用户点击Cancel按钮时,则取消。
要做到这点,你将使用一个unwind segue。一个unwind segue,可以通过一个或多个segues向后返回到一个已存在的view controller实例中。你使用unwind segues来实现反向导航。
每当一个segue被触发,它提供一个让你添加代码并执行的地方。这个方法叫prepareForSegue(_:sender:),它可以让你存储数据并做一些必要的清理工作。你可以在MealViewController
中实现这个方法来做到这点
在MealViewController中实现prepareForSegue(_:sender:)方法
1.返回到standard editor
2.打开MealViewController.swift
3.在MealViewController.swift上方,添加注释
// MARK: Navigation
4.在注释下方,添加如下代码
// This method lets you configure a view controller before it's presented. override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { }
5.在prepareForSegue(_:sender:)方法中,添加if语句
if saveButton === sender { }
(===)操作符用来检查对象的引用是否相同,即
saveButton和sender是否是同一个对象。如果是,if语句会执行
6.在if语句中,添加如下代码
let name = nameTextField.text ?? "" let photo = photoImageView.image let rating = ratingControl.rating
这段代码从当前文本框,选中的image,和评级数据三个方面创建了常量
注意,在name
这行使用了空值合并运算符(??)。这个运算符对于可选变量有值时,返回一个值,如果可选变量为nil时,则返回默认值。这里我们通过nameTextField.text来返回一个值,他可能为空,如果用户没有在文本框中输入内容,那么就为nil,则返回空串("")
7.接着在if语句中,添加如下代码
// Set the meal to be passed to MealListTableViewController after the unwind segue. meal = Meal(name: name, photo: photo, rating: rating)
这段代码用来在segue执行前使用适当的值来配置meal属性
现在完整的prepareForSegue(_:sender:)方法看起来如下:
// This method lets you configure a view controller before it's presented. override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if saveButton === sender { let name = nameTextField.text ?? "" let photo = photoImageView.image let rating = ratingControl.rating // Set the meal to be passed to MealListTableViewController after the unwind segue. meal = Meal(name: name, photo: photo, rating: rating) } }
接下来我们创建的unwind segue会添加一个动作方法到目标视图控制器(就是segue将要去的视图控制器)。这个方法必须标记为IBAction属性来获取一个segue(UIStoryboardSegue
)作为参数。因为你想要unwind segue返回到meal list场景,你需要添加一个这种格式的动作方法到 MealTableViewController.swift中。
在这个方法中,你将写逻辑来来添加新的菜谱到meal list数据中并会在meal list场景下的table view内添加新的一行
添加一个动作方法到MealTableViewController
1.打开MealTableViewController.swift
2.在MealTableViewController.swift中,(})之前,添加如下代码:
@IBAction func unwindToMealList(sender: UIStoryboardSegue) {
}
3.在unwindToMealList(_:)动作方法内,添加以下if语句
if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal{ }
if语句中会发生很多事情。
代码使用可选类型强制转换操作符(as?),试图子类强转到源view controller的segue到MealViewController类型。你需要子类强转,因为sender.sourceViewController是UIViewController类型,但你需要使用MealViewController工作。
这个操作符返回一个可选值,如果子类强转不可行,那么它将会是nil。如果子类强转成功,代码会分配view controller到局部常量sourceViewController,并检查是否
sourceViewController中的
meal属性为nil。如果meal属性非nil,代码分配属性值到局部常量meal并执行if语句。
4.在if语句中,添加以下代码
// Add a new meal. let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
代码会计算table view中新插入的cell的显示位置,并存储它在局部常量newIndexPath中
5.在if语句中,添加以下代码
meals.append(meal)
添加新的菜谱到已存在的meals列表中(数据模型)
6.在if语句中,添加以下代码:
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
会有个动画添加新的行(cell)到table view中,它会包含新的菜谱信息。.Bottom动画选项会显示从底部滑动插入
你稍后将完成一个更高级的方法实现,但现在unwindToMealList(_:)动作方法看起来如下:
@IBAction func unwindToMealList(sender: UIStoryboardSegue) { if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal { // Add a new meal. let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0) meals.append(meal) tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom) } }
现在你需要创建一个实际的unwind segue来触发这个动作方法
连接Save按钮到unwindToMealList动作方法
1.打开你的storyboard
2.在画布中,按住Control键拖动Save按钮到meal场景的Exit items上
当你松开时,会出现一个提示
3.从快捷菜单中选择unwindToMealList:
现在当用户点击Save按钮时,导航返回到meal list场景,在此期间,unwindToMealList(_:)动作方法会被调用
检查站:执行你的app。现在当你点击(+)按钮时,创建一个新的菜谱,然后点击保存,你将会看见新的菜谱出现在你的meal list中
如果你没有看到在快捷菜单中unwindToMealList方法,确保该方法具有正确的签名:@IBAction func unwindToMealList(sender: UIStoryboardSegue)
当用户没有输入一个Item Name时,禁用保存
如果没有name时,用户点击save按钮会发生什么?因为在MealDetailTableViewController中的meal属性是可选的,所以如果没有name,那么你的初始化程序会失败,Meal对象不会创建,也不会添加到meal list场景中。但你可以在软键盘消失前,检测用户是否指定了一个有效的name,如果用户意外的没有添加meal的name,那么我们禁用Save按钮
当没有name时,禁用Save按钮
1.在MealViewController.swift找到 // MARK: UITextFieldDelegate
2.然后添加另一个UITextFieldDelegate协议内的方法
func textFieldDidBeginEditing(textField: UITextField) { // Disable the Save button while editing. saveButton.enabled = false }
当编辑开始时,或当软键盘显示时,textFieldDidBeginEditing会被调用。然后我们通过代码来禁用Save按钮
3.在textFieldDidBeginEditing(_:)方法下方添加另一个方法
func checkValidMealName() { // Disable the Save button if the text field is empty. let text = nameTextField.text ?? "" saveButton.enabled = !text.isEmpty }
这个帮助方法用来检查当文本框为空时,禁用Save按钮
4.找到textFieldDidEndEditing(_:)方法,添加如下代码:
checkValidMealName()
navigationItem.title = textField.text
第一行是检查文本框是否为空,来启用或禁用Save按钮。第二行是设置场景的标题为文本框中的文本
6.找到viewDidLoad()方法,然后添加如下代码:
// Enable the Save button only if the text field has a valid Meal name. checkValidMealName()
首先,载入界面后,确保Save按钮是被禁用的
完整的 viewDidLoad()方法如下
override func viewDidLoad() { super.viewDidLoad() // Handle the text field’s user input through delegate callbacks. nameTextField.delegate = self // Enable the Save button only if the text field has a valid Meal name. checkValidMealName() }
完整的textFieldDidEndEditing()方法如下
func textFieldDidEndEditing(textField: UITextField) { checkValidMealName() navigationItem.title = textField.text }
检查站:执行的APP。现在当你点击(+)按钮时,Save按钮首先会被禁用,直到你输入一个有效的meal name并关闭软键盘后Save按钮可用
取消新菜谱的添加
用户可能决定取消添加一个新的菜谱,并返回到meal list场景中。对于这点,我们需要实现Cancel按钮的行为
创建和实现取消动作方法
1.打开你的storyboard
2.打开assistant editor
3.在storyboard中,选中Cancel按钮
4.按住Control键拖动Cancel按钮到 MealViewController.swift
代码中// MARK: Navigation注释的下方
5.在弹出的对话框中,Connection旁选择Action
6.Name标签旁,输入cancel
7.Type标签旁,选择UIBarButtonItem
8.点击Connect,出现以下代码:
@IBAction func cancel(sender: UIBarButtonItem) {
}
9.在cancel(_:)动作方法中,添加以下代码:
dismissViewControllerAnimated(true, completion: nil)
这行代码是让meal场景消息,没有存储任何信息
你完整的cancel(_:)动作方法如下:
@IBAction func cancel(sender: UIBarButtonItem) { dismissViewControllerAnimated(true, completion: nil) }
检查站:执行你的APP,现在当你点击(+)按钮后,点击Cancel按钮,你将导航回到meal list,并且不会添加任何新的菜谱