在本章中,我们将使用导航控制器并继续创建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,并且不会添加任何新的菜谱

相关文章: