重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。
翻译自苹果官网:
本课中,使用导航控制器和 segues 为 app 创建导航流。最后会完成一个完整的导航方案。样子如下:
[图片上传失败...(image-421b09-1608214786914)]
学习目标
本课的最后,你将能够:
- 在 storyboard 中嵌入视图控制器到导航控制器里面。
- 在视图控制器之间创建 segues。
- 使用属性检查器编辑 segue 的属性。
- 使用
prepareForSegue(_:sender:)
方法在视图控制器间传递数据 - 操作一个 unwind segue
- 使用堆栈视图来创建强大灵活的布局
添加一个 segue 导航
数据像预期一样显示了,是时候提供从食物列表导航到食物场景的方式了。场景间的过渡被称为 segues。
在创建 segue 前,你需要配置场景。首先,把表格视图控制器放到导航控制器里面。导航控制器用于在一系列的视图控制器间前后过渡。视图控制器集合由一个叫导航栈的导航控制器管理着。第一个添加到栈中的控制器成为 root view controller 并且永远也不会从导航栈中弹出。
添加导航控制器
打开 Main.storyboard。
-
通过点击场景 dock 选中表格视图控制器。
[图片上传失败...(image-472cc4-1608214786914)]
-
选中后,选择 Editor > Embed In > Navigation Controller。
Xcode 往 storyboard 中添加新的导航控制器,设置它为
storyboard entry point
,并在导航控制器和表格视图控制器间创建关联。[图片上传失败...(image-e3745f-1608214786915)]
在画板中,连接控制器间的图标是 root view controller relationship。表格视图控制器是导航控制器的根视图控制器。storyboard entry point 设置为导航控制器因为它现在是表格视图控制器的容器了。
你或许注意到 table view 的顶部有个条形控件。这就是导航栏。在导航栈中每个控制器都得到了一个导航栏,它可以包含控件用于向前和向后导航。下一步,往导航栏添加按钮用于食物场景间过渡。
检验:运行 app。在表格视图的上面你应该看到额外的控件。这就是导航控制器提供的导航栏。它扩展状态栏为它自己的背景,所以状态栏不再覆盖你的内容了。
[图片上传失败...(image-b3d88-1608214786915)]
为场景配置导航栏
现在,给导航栏添加一个标题(食物列表)和一个按钮(用于添加食物)。导航栏从当前显示的视图控制器中得到标题 - 它们自己并没有标题。使用表格视图控制器的 navigation item 设置标题而不是直接设置到导航栏上。
在食物列表中配置导航栏
-
双击食物列表场景中的导航栏。
[图片上传失败...(image-646c8b-1608214786915)]
光标出现了,让你输入文字。
-
输入 Your Meals 然后按回车来保存。
[图片上传失败...(image-832eb8-1608214786915)]
打开对象库。(选择 View > Utilities > Show Object Library。)
在对象库中找到 Bar Button 对象。
-
从列表中拖动这个 Bar Button 对象到食物列表场景的导航栏上。
当拖动 bar button item 时候出现一个按钮叫做 Item 。[图片上传失败...(image-93ceee-1608214786915)]
选择 bar button item 然后打开属性检查器。
-
从 System Item 选项旁边的弹出菜单中选择 Add。按钮修改为一个 + 按钮。
[图片上传失败...(image-960b61-1608214786915)]
检验:运行 app。导航栏应该有个标题并显示一个加号(+)按钮。按钮不会做任何事。稍后会修复这个问题。
[图片上传失败...(image-809c46-1608214786915)]
你希望 + 按钮打开场景界面,所以让按钮触发一个 segue 到这个场景并打开它。
配置食物场景中的加号按钮
在画板中,选择加号按钮(+)。
-
按住 Control 从按钮拖动到食物场景中。
[图片上传失败...(image-f9431e-1608214786915)]
在拖动结束的地方出现一个标题 Action Segue 的快捷菜单。
[图片上传失败...(image-56ef72-1608214786915)]
当用户点击加号按钮时候,Action Segue 菜单让你选择使用哪种 segue 从食物列表导航到新的食物视图控制器中。
选择 Action Segue 菜单中的 show。
Xcode 设置 show segue 并配置食物场景在导航控制器中显示 - 在 Interface Builder 中看到的导航栏。
[图片上传失败...(image-c4f2ef-1608214786915)]
检验:运行 app。点击加号按钮并从食物列表场景导航到食物场景中。因为你正在使用一个 show segue 的导航控制器。这意味着你可以在食物场景中点击返回按钮来返回食物列表界面。
[图片上传失败...(image-19bee4-1608214786915)]
使用推荐的 show segue 最终产生 push 样式的导航 - 但这不是在添加一份食物的时候应该做的。Push 导航被设计用来给用户提供更多信息。而添加一份食物,在另一方面,是个模态操作-用户执行的动作是完整且独立的,之后从场景中返回主导航。为这类场景合适的呈现方式就是 modal segue。
取代删除存在的 segue 并创建一个新的,在属性检查器中简单修改 segue 的样式。因为对于大多数 storyboard 中选中的控件,可以使用属性检查器去编辑一个 segue 的属性。
修改 segue 样式
-
从食物列表场景选择 segue 到食物场景。
[图片上传失败...(image-abbec9-1608214786915)]
在属性检查器中 Segue 选项旁边的弹出菜单中选择 Present Modally。
在属性检查器的 Identifier 区域旁边输入 AddItem。回车确认。
之后,需要这个标识来识别 segue。
一个模态视图控制器不需要添加到导航栈中,所以它不会从食物列表导航控制器中得到一个导航栏。然而,你想要保持导航栏来给用户提供视觉一致性。为了在模式弹出时给食物场景一个导航栏,把它嵌入到它自己的导航控制器。
给食物场景添加导航控制器
-
点击它的场景 dock 选中食物场景。
[图片上传失败...(image-4555e4-1608214786915)]
选中后,选择 Editor > Embed In > Navigation Controller。
像之前一样,Xcode 添加导航控制器并在食物场景顶部显示导航栏。下一步,配置导航栏给场景添加一个标题和连个按钮,Cancel 和 Save。稍后,将为这些按钮连接动作。
[图片上传失败...(image-657c89-1608214786915)]
在食物场景中配置导航栏
-
双击食物场景中的导航栏。
[图片上传失败...(image-3ac466-1608214786915)]
出现一个光标,让你输入文本。
输入 New Meal 并回车确认保存。
从对象库中拖动一个 Bar Button Item 对象到食物场景导航栏的左边。
在属性检查器的 System Item 处,选择 Cancel。
按钮文本修改为 Cancel。
[图片上传失败...(image-e112f2-1608214786915)]
从对象库中拖动另一个 Bar Button Item 对象到食物场景导航栏的最右边。
-
在属性检查器的 System Item,选择 Save。
按钮文本修改为 Save。[图片上传失败...(image-2e3eab-1608214786915)]
检验:运行 app。点击 Add 按钮。你仍然能够看到食物场景,但是不再有导航返回食物列表的按钮了 - 相应的,你会看到你添加的两个按钮, Cancel 和 Save。这些按钮还没有关联任何动作,点击它们,但是并没有做什么。稍后配置按钮来保存或取消添加一个新的食物并让用户返回食物列表。
[图片上传失败...(image-668f7f-1608214786915)]
使用自动布局 (Auto Layout)完成界面
从你构建原始界面有一段时间了,从那时起有好多事情已经改变了。此刻,你不需要对布局做任何改变,所以是时候使用自动布局确保一切看起来很棒。
为了做这些,对你的堆栈视图做一些调整。
更新堆栈视图的布局
-
在食物场景中,点击淡蓝色区域选中堆栈视图。
[图片上传失败...(image-c64a3a-1608214786915)]
也可以在大纲视图中选择堆栈视图。
-
在画板的右下角,打开
Resolve Auto Layout Issues
菜单。[图片上传失败...(image-8b3dff-1608214786915)]
-
在 Selected Views 下面,选择 Update Constraints。
[图片上传失败...(image-35cdb7-1608214786915)]
控件还在原位置,但是堆栈视图现在被导航栏约束而不是视图顶部边界。
食物场景约束和界面应该像这样:
[图片上传失败...(image-1761dc-1608214786915)]
检验:运行你的 app。文本框,image view 和评分控件应该接近导航栏了。
在食物列表中保存一份新的食物
下一步为 FoodTracker app 添加的功能就是让用户有能力添加一份新的食物。当用户在食物场景中输入食物的名字,评分和照片并点击保存按钮,MealViewController 配置个带有合适信息的 Meal 对象并传递给 MealTableViewController 用来显示在食物列表中。
通过添加一个 Meal 属性到 MealViewController 开始。
给 MealViewController 添加一个 Meal 属性
打开 MealViewController.swift。
-
在 MealViewController.swift 的 ratingControl outlet 下面,添加如下属性:
/* This value is either passed by `MealTableViewController` in `prepareForSegue(_:sender:)` or constructed as part of adding a new meal. */ var meal: Meal?
在 MealViewController 中定义了一个可选的 Meal 属性,这意味着在有些时候,它或许是 nil。
连接 Save 按钮到 MealViewController 代码
打开你的 storyboard。
-
点击 Xcode 工具栏的 Assistant 按钮来打开辅助编辑器。
[图片上传失败...(image-e4c083-1608214786915)]
-
如果想要更多空间来工作,点击 Xcode 工具栏的 Navigator 和 Utilities 按钮收缩项目导航和实用工具区。
[图片上传失败...(image-8d6ded-1608214786915)]
在你的 storyboard 中,选择 Save 按钮。
-
按住 Control 从画板的 Save 按钮拖动到右边编辑器中的代码,在 ratingControl 属性下面一行停止拖动。
[图片上传失败...(image-d15130-1608214786915)]
-
在出现的对话框中,输入名字:saveButton。
忽略剩下的选项。你的对话框应该像这样:[图片上传失败...(image-ecac67-1608214786915)]
点击 Connect。
你或许有种识别 Save 按钮的办法。
创建一个 Unwinde Segue
当用户点击 Save 和 Cancel 按钮时,传递 Meal 对象给 MealTableViewController。就是从显示食物场景切换为食物列表。
为了完成这些,使用 unwind segue。它让用户返回到上一个或者更多的视图控制器的实例。使用 unwind segues 来实现反向导航。
当 segue 触发了,它给你提供一个添加你自己代码的地方。这个方法叫做 prepareForSegue(_:sender:)
,它给你一个机会储存数据和在源视图控制器(segue 来源于这个视图控制器)做一些必要的清理工作。即将在 MealViewController 中实现这个方法。
在 MealViewController 中实现 prepareForSegue(_:sender:) 方法
-
点击 Standard 按钮返回标准编辑器。
[图片上传失败...(image-fcfe56-1608214786915)]
打开 MealViewController.swift。
-
在 MealViewController.swift 的 // MARK: Actions 区域,添加如下注释:
// MARK: Navigation
这个注释让你(和其他任何读你代码的人)知道方法是关联 app 的导航流。
-
在注释的下面,添加方法主体:
// This method lets you configure a view controller before it's presented. override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { }
-
在 prepareForSegue(_:sender:) 方法中,添加如下 if 语句:
if saveButton === sender { }
代码通过使用恒等运算符(===)来检查 saveButton 关联的 outlet 和 sender 对象是否相同的对象。如果是,if 语句就会执行。
-
在 if 语句里面,添加如下代码:
let name = nameTextField.text ?? "" let photo = photoImageView.image let rating = ratingControl.rating
注意 name 行的 ?? 用于在可选无值时返回一个默认的值。这里展开 nameTextField.text 返回的可选 String(它是可选的因为文本框或许没有文字),如果是不合法的字符串会返回 nil。然后使用空字符串("")代替返回。
-
在 if 语句中,添加如下代码:
// Set the meal to be passed to MealTableViewController 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 MealTableViewController after the unwind segue.
meal = Meal(name: name, photo: photo, rating: rating)
}
}
下一步创建 unwind segue 的操作就是添加一个动作方法到目标视图控制器( segue 要跳过去的视图控制器)。这个方法应该被 IBAction 属性标记并拥有一个 segue (UIStoryboardSegue) 的参数。因为想要返回食物列表场景,使用这种方式添加动作方法。
在这个方法中,编写逻辑来添加新的食物(从源视图控制器 MealViewController 中传递)到食物列表数据中并在 table view 中添加新的一行。
为 MealTableViewController 添加一个动作方法
打开 MealTableViewController.swift。
-
在 MealTableViewController.swift 的大括号 } 前面,添加如下方法:
@IBAction func unwindToMealList(sender: UIStoryboardSegue) { }
-
在
unwindToMealList(_:)
动作方法中,添加如下 if 语句:if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal { }
这个 if 条件语句里面有许多门道。
代码使用可选类型转换操作符(as?)来尝试向下转换 segue 的视图控制器到 MealViewController 类型。你需要转换因为 sender.sourceViewController 是 UIViewController 的类型。但是你需要使用 MealViewController。
操作符返回可选值,当下转不可能的时候变成 nil。如果下转成功,代码将视图控制器赋给局部常量 sourceViewController,检查 sourceViewController 中的 meal 属性是否为 nil。如果 meal 属性不为 nil,代码分配属性的值给本地常量 meal 并执行 if 语句。
-
在 if 语句中,添加如下代码:
// Add a new meal. let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
-
在 if 语句前一行的代码下面,添加如下代码:
meals.append(meal)
添加新的一份食物到列表中。
-
在 if 语句的前一行代码的下面,添加如下代码:
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
往 tableView 中插入新的一行单元格。.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 动作方法
打开 storyboard。
-
在画板中,按住 Control 从 Save 按钮拖动到食物场景顶部的 Exit 选项中。
[图片上传失败...(image-7fce92-1608214786915)]
在拖拽结束的位置弹出一个菜单。
[图片上传失败...(image-5b553-1608214786915)]
从快捷菜单中选择 unwindToMealList:。
现在,当用户点击 Save 按钮,在 unwindToMealList(_:) 动作方法调用过程中,他们导航返回到食物列表场景。
检验:运行 app。现在当你点击 + 按钮,创建一份新的食物,然后点击 Save,你应该在你的食物列表中看到这份新的食物了。
重要提示:
如果你在快捷菜单中没有看到 unwindToMealList 方法,确保方法有正确的标识。@IBAction func unwindToMealList(sender: UIStoryboardSegue)
当用户未输入食物名不能保存
如果用户尝试保存一份没有名字的食物会导致什么后果?因为 MealViewController 中的 meal 属性是可选的所以当没有名字的时候构造失败,Meal 对象不会创建和添加到食物列表中,这正是我们预期发生的。但是可以更进一步防止这种意外,当用户正在输入食物的名字禁用保存按钮,在关闭键盘前检查他们是否指定了正确的名字。
当没输入名字时禁用保存按钮
在 MealViewController.swift 中,找到 // MARK: UITextFieldDelegate 区域。
你可以使用 functions menu 快速跳到那里,它会在你点击编辑区顶部的名字时候出现。-
在这个区域,添加另一个 UITextFieldDelegate 方法:
func textFieldDidBeginEditing(textField: UITextField) { // Disable the Save button while editing. saveButton.enabled = false }
当编辑开始时
textFieldDidBeginEditing
方法会被调用。这时禁用保存按钮。 -
在 textFieldDidBeginEditing(_:) 方法的下面,添加另一个方法:
func checkValidMealName() { // Disable the Save button if the text field is empty. let text = nameTextField.text ?? "" saveButton.enabled = !text.isEmpty }
这是个帮助方法用来当文本框是空时禁用保存按钮。
-
找到
textFieldDidEndEditing(_:)
方法:func textFieldDidEndEditing(textField: UITextField) { }
此刻实现应该是空的。
-
添加这些行代码:
checkValidMealName() navigationItem.title = textField.text
第一行调用 checkValidMealName() 来检查文本框中是否有文本,如果有启用保存按钮。第二行设置场景的标题为它的文本。
-
找到 viewDidLoad() 方法:
override func viewDidLoad() { super.viewDidLoad() // Handle the text field’s user input through delegate callbacks. nameTextField.delegate = self }
-
在实现部分添加 checkValidMealName() 的调用确保禁用了保存按钮直到用户输入正确的名字:
// Enable the Save button only if the text field has a valid Meal name. checkValidMealName()
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。现在当你点击 + 按钮,保存按钮被禁用直到你输入一个正确的(非空)的食物名字并关闭键盘了。
[图片上传失败...(image-39005a-1608214786915)]
Cancel 添加新的食物
用户或许决定取消添加新的食物,然后不保存就返回食物列表。为了做到这些,实现 Cancel 按钮的功能。
创建和实现一个取消动方法
打开 storyboard。
-
点击 Xcode 工具栏的 Assistant 按钮打开辅助编辑器。
[图片上传失败...(image-ab9791-1608214786915)]
在 storyboard 中,选择 Cancel 按钮。
-
按住 Control 从画板的 Cancel 按钮拖动到右边编辑器的代码中,在 MealViewController.swift 的 // MARK: Navigation 注释下面一行停止拖动。
[图片上传失败...(image-7cd3e6-1608214786915)]
在弹出的对话框的 Connection 区域选择 Action。
输入名字:cancel。
-
类型选择 UIBarButtonItem。
忽略剩下的选项。你的对话框应该像这样:[图片上传失败...(image-d72a07-1608214786915)]
-
点击 Connect。
Xcode 添加必要的代码到 MealViewController.swift 中来设置这个动作。@IBAction func cancel(sender: UIBarButtonItem) { }
-
在 cancel(_:) 方法中,添加如下行代码:
dismissViewControllerAnimated(true, completion: nil)
代码在没有保存任何信息的情况下关闭食物场景。关闭后,显示食物列表。
cancel(_:) 动作方法应该像这样:
@IBAction func cancel(sender: UIBarButtonItem) {
dismissViewControllerAnimated(true, completion: nil)
}
检验:运行 app。现在点击 + 之后点击 Cancel 而不是 Save,你应该导航返回到食物列表但你并没有添加一份新的食物。
注意:
为了看到本课的完整示例项目,下载文件并在 Xcode 中查看它。