实现导航

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

posted @ 2015-07-07 11:27  jy02432443  阅读(838)  评论(0编辑  收藏  举报