在菜品列表中存储新菜品
创建FoodTracker应用实用功能的下一个步骤是实现用户添加新菜品的能力。具体来说就是,当用户在菜品详情场景输入菜品名、评分、以及照片,并点击Save按钮时候,你想让MealViewController利用这些信息配置Meal对象,并把它传递给MealTableViewController显示在菜品列表上。
通过添加Meal属性到MealViewController开始
添加菜品属性到MealViewController
- 打开MealViewController.swift文件
- 在ratingControl outlet下面,添加下面的属性:
/*
This value is either passed by `MealTableViewController` in `prepare(for:sender:)`
or constructed as part of adding a new meal.
*/
var meal: Meal?
这个在MealViewController上声明的属性是一个可选类型的Meal,这意味着在任何时候,它有可能是nil。
只有当Save按钮被点击的时候,你才需要关心配置和传递Meal的事情。为了能够确定它何时发生,在MealViewController.swift中添加Save按钮的outlet。
连接Save按钮到MealViewController代码
- 打开storyboard。
-
打开助理编辑器。
-
尽可能扩展操作空间。
- 在storyboard中,选择Save按钮。
-
按住Control键,从Save按钮拖拽一条线到右侧的编辑器代码中,到ratingControl属性下面的时候放手。
-
在出现的对话框中,Name字段键入saveButton。
其他的属性保持不变。对话框应该是这样的:
- 点击Connect。
现在你有了辨认Save按钮的方式。
创建一个unwind segue
现在的任务是当用户点击Save按钮的时候传递Meal对象到MealTableViewController,而当用户点击Cancel按钮的时候丢弃这个对象,无论点击哪个都要从显示菜品详情场景切换到显示菜品列表场景。
为了达到这个目的,你将使用unwind segue。unwind segue向后移动一个或多个segue,返回到一个由已存在的视图控制器管理的场景。虽然通常segue会创建目标视图控制器的一个新实例,但unwind segue会让你返回到一个由已存在的视图控制器。使用unwind segue来实现导航到已存在的视图控制器。
无论segue何时被触发,它都提供一个地方让你添加用来执行的代码。这个方法称为prepare(for:sender:),它提供一个存储数据以及在源视图控制器(source view controller,segue出发的那个视图控制器)进行必要的清理的机会。你将在MealViewController中实现这个方法来做到这一点。
在MealViewController中实现 prepare(for:sender:)方法
-
回到标准编辑器。
- 打开MealViewController.swift文件。
- 在文件顶部,蹈入import UIKit,添加如下代码:
import os.log
这会导入统一的日志系统。像print()函数,统一日志系统让你发送消息到控制台。然而,统一日志系统在何时出现消息以及如何保存消息方面给了你更多的控制。
进一步探索
更多关于统一日志系统的消息,参见 Logging Reference。
- 在MealViewController.swift中,在 //MARK: Actions部分的上面,添加如下注释:
//MARK: Navigation
- 在这个注释下面,添加下面这个方法框架:
// This method lets you configure a view controller before it's presented.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
}
- 在prepare(for:sender:)方法中,添加对超类实现的调用:
super.prepare(for: segue, sender: sender)
这个UIViewController类的实现方法没有做任何事,但是当你重写prepare(for:sender:)的时候总是调用super.prepare(for:sender:)是一个好习惯。这样的话当你子类化一个不同类的时候就不会忘了。
- 在super.prepare(for:sender:)下面,添加如下guard语句:
// Configure the destination view controller only when the save button is pressed.
guard let button = sender as? UIBarButtonItem, button === saveButton else {
os_log("The save button was not pressed, cancelling", log: OSLog.default, type: .debug)
return
}
这段代码验证sender是一个按钮,然后使用身份运算符(identity operator ===)来检查sender引用的对象和saveButton的outlet引用的是否相同。
如果它们不同,则else中的语句执行。应用使用系统标准的日志机制来记录调试信息。调试信息包含在调试期间或者分析特定问题的时候有用的信息。它们只用于调试环境中,不会出现在传送应用的时候。在输出了调试信息后,该方法返回。
- 在else语句的下面,添加如下代码:
let name = nameTextField.text ?? ""
let photo = photoImageView.image
let rating = ratingControl.rating
这段代码以当前的text file的文本、选择的图片、以及场景中的评分来创建常量。
注意name行中的空合并运算符(nil coalescing operator)。Nil coalescing 运算符,在可选类型对象有值时用于返回一个这个值,否则返回一个默认值。此处,如果它有一个有效值,这个运算符解包可选String类型将值返回给nameTextField.text(它是可选是因为text field可能有也可能没有文本)。如果它是nil,那么操作符就会返回空字符串(“”)。
- 紧接着添加下面的代码:
// Set the meal to be passed to MealTableViewController after the unwind segue.
meal = Meal(name: name, photo: photo, rating: rating)
这段代码在执行segue之前用合适的值配置meal属性。
记住
Meal类的init?(name:, photo:, rating:)初始化器方法是一个可失败的初始化器。如果name属性是一个空字符串,或者ratting属性小于0或大于5,这个代码会分配nil给meal变量。否则,它会一个新的Meal对象给meal变量。
现在 prepare(for:sender:)方法看上去是这样的:
// This method lets you configure a view controller before it's presented.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
// Configure the destination view controller only when the save button is pressed.
guard let button = sender as? UIBarButtonItem, button === saveButton else {
os_log("The save button was not pressed, cancelling", log: OSLog.default, type: .debug)
return
}
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的下一步是添加一个action方法到目标视图控制器(destination view controller,就是segue要去的视图控制器)。这个方法必须用IBAction属性标记,并且使用segue(UIStoryboardSegue)作为参数。因为你想返回到菜品列表场景,所以你使用这个格式添加一个action方法到MealTableViewController.swift文件。
在这个方法中,你将编写用来添加新的菜品(从源视图控制器MealViewController传递过来的)到菜品列表数据,以及给菜品列表场景的表视图添加一个新行(row)。
添加一个action方法到MealViewController
- 打开MealViewController.swift。
- 在//MARK: Private Methods部分之前,添加下面的注释:
//MARK: Actions
- 在//MARK: Actions注释后面,添加如下代码:
@IBAction func unwindToMealList(sender: UIStoryboardSegue) {
}
- 在 unwindToMealList(_:)方法中,添加下面的if语句:
if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal {
}
在这个if语句的条件里发生了很多事。
这段代码使用可选类型转换运算符(optional type cast operator, as?)来尝试将segue源视图控制器降级到MealViewController实例。你需要降级是因为sender.sourceViewController是一个UIViewController类型,而你需要的是MealViewController类型。
这个运算符返回一个可选值,如果不能降级,它的值为nil。如果降级成功,这个代码会分配一个MealViewController的实例给本地常量sourceViewController,并且检查sourceViewController的meal属性是否为nil。如果这个属性非空,代码分配这个属性的值给本地的常量meal,并执行if语句。
如果降级失败或者meal参数为nil,这个条件判定为false,并且if语句不会执行。
- 在if语句中,添加如下代码:
// Add a new meal.
let newIndexPath = IndexPath(row: meals.count, section: 0)
这段代码计算新cell显示在table view中的位置,并且把它存储在本地常量newIndexPath中。
- 在if语句中,在之前代码的下面,添加如下代码:
meals.append(meal)
这是把新meal添加到已存在的菜品列表数据模型中。
- 在if语句中,在之前代码的下面,添加如下代码:
tableView.insertRows(at: [newIndexPath], with: .automatic)
这样就让添加包含新菜品信息的cell到表视图的新行有了动画。这个 .automatic动画选项使用了基于表的当前状态和插入点位置的最好动画。
你稍后将完成这个方法更多的高级实现,但是现在,unwindToMealList(_:)方法看上去是这样的:
@IBAction func unwindToMealList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? MealViewController, let meal = sourceViewController.meal {
// Add a new meal.
let newIndexPath = IndexPath(row: meals.count, section: 0)
meals.append(meal)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
现在,你需要创建一个真实的unwind segue来触发这个action方法。
连接Save按钮到unwindToMealList方法
- 打开storyboard。
-
在画布上,按住Control键,拖拽Save按钮到菜品场景顶部的Exit项目上。
在拖拽结束的地方出现一个菜单。它显示了所有可用的unwind 动作方法。
- 选择unwindToMealListWithSender:方法。
现在,当用户点击Save按钮的时候,它们会导航到菜品列表场景,在这个过程中unwindToMealList(sender:)方法会被调用。
进一步探索
Unwind segue为传递信息回较早的视图控制器提供了一个简单方法。但有时候,在视图控制器之间你需要更加复杂的通信。那种情况下,要考虑使用委托(delegate)模式。
更多信息,参见 The Swift Programming Language (Swift 4)中的Delegation。
检查点:运行应用。现在当你点击加号按钮的时候,创建一个新菜品,点击Save按钮,你将看到新菜品出现在了你的菜品列表里。
重要
如果你在快捷菜单中没有看到 unwindToMealListWithSender:方法,确保方法有正确的名字。@IBAction func unwindToMealList(sender: UIStoryboardSegue)。
当用户没有输入项目名字的时候不能保存
如果用户视图保存一个没有名字的菜品会发生什么?因为MealViewController中的meal属性是一个可选类型,并且你将它设置为了如果没有名字就初始化失败,Meal对象不能创建并添加到菜品列表——这是你所期望的。但是你可以实现进一步的功能,当用户输入菜品名的时候让Save按钮不可用,以保证用户不会意外的在没有名字的情况下添加菜品,并且在取消键盘之前检查它们是否已经指定了一个有效的名字。
当没有item名字的时候让Save按钮不可用
- 在MealViewController.swift文件中,找到 //MARK: UITextFieldDelegate部分。
- 在这部分中,添加另一个UITextFieldDelegate方法:
func textFieldDidBeginEditing(_ textField: UITextField) {
// Disable the Save button while editing.
saveButton.isEnabled = false
}
这个方法在编辑会话开始或者键盘出现的时候调用。这段代码让用户在编辑text field的时候Save按钮不可用。
- 滚动到类的底部,在结束花括号前面添加下面的注释:
//MARK: Private Methods
- 在//MARK: Private Methods注释后面,添加下面代码:
private func updateSaveButtonState() {
// Disable the Save button if the text field is empty.
let text = nameTextField.text ?? ""
saveButton.isEnabled = !text.isEmpty
}
这个辅助方法让text filed为空的时候Save按钮不可用。
- 返回到//MARK: UITextFieldDelegate部分,并找到textFieldDidEndEditing(_:)方法:
func textFieldDidEndEditing(_ textField: UITextField) {
}
这个方法现在是空的。
- 添加这些代码:
updateSaveButtonState()
navigationItem.title = textField.text
首先调用updateSaveButtonState()方法来检查text filed是否有文本,从而确定Save按钮是否可用。第二行使用这个文本设置场景的标题。
- 找到 viewDidLoad()方法。
override func viewDidLoad() {
super.viewDidLoad()
// Handle the text field’s user input through delegate callbacks.
nameTextField.delegate = self
}
- 在实现文件中添加一个调用updateSaveButtonState(),来确保在输入有效的名字之前Save按钮不可用。
// Enable the Save button only if the text field has a valid Meal name.
updateSaveButtonState()
你现在的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.
updateSaveButtonState()
}
你的 textFieldDidEndEditing(_:)方法看上去是这样的:
func textFieldDidEndEditing(_ textField: UITextField) {
updateSaveButtonState()
navigationItem.title = textField.text
}
检查点:运行应用。当你点击加号按钮的时候,直到你输入有效(非空)的菜品名字并且收回键盘前Save按钮是不可用的
取消添加新菜品
用户或许决定取消添加新菜品,并且不保存任何数据的返回到菜品列表界面。为此,你需要实现Cancel按钮的功能。
创建并实现cancel动作方法
- 打开storyboard。
-
打开助理编辑器。
- 在storyboard中,选择Cancel按钮。
-
按住Control键,从Cancel按钮处拖拽到右边编辑器的代码中,停在 //MARK: Navigation注释的下面,松手。
- 在出现的对话框中,Connection字段,选择Action。
- Name字段,键入cancel。
-
Type字段,选择UIBarButtonItem。
其他选项不变。你的对话框看上去是这样的:
- 点击Connect。
Xcode添加必要的代码到MealViewController.swift中来设置action方法。
@IBAction func cancel(_ sender: UIBarButtonItem) {
}
- 在cancel(_:)方法中,添加下面的代码:
dismiss(animated: true, completion: nil)
dismiss(animated:completion:)方法会移除模态场景,并且动画的返回到前一个场景中(在本例,是菜品列表场景)。当菜品详情场景移除的时候应用不保存任何数据,prepare(for:sender:)方法和unwind 动作方法都不会被调用。
你的 cancel(_:)方法看上去是这样的:
@IBAction func cancel(_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
检查点:运行应用。现在当你点击加号按钮然后再点击Cancel按钮,你将导航回到菜品列表场景且没有添加新菜品。
小结
在本课中,你学习了如何推入场景到导航栈,以及如何模态的呈现视图。你学习了如何使用unwind segue导航回到之前的场景、如何通过segue传递数据、以及如何移除模态视图。
现在,应用显示一个样本菜品的初始列表,并且让你能够添加新菜品到列表。在下一课中,你将添加编辑和删除菜品的功能。
注意
想看本课的完整代码,下载这个文件并在Xcode中打开。
下载文件