重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。
翻译自苹果官网:
本课中,集中精力添加功能让用户在 app 中能编辑和删除美食。
学习目标
在本课的最后,你将能够:
- 区别 push 和 modal 导航
- 基于视图控制器的打开方式关闭它们
- 理解何时使用不同的向下转换的方式
- 利用可选绑定来检查复杂的条件
- 使用 segue 标识确定哪个 segue 正在发生。
允许编辑已存在的美食
现在,FoodTracker app 让用户能够向食物列表添加一份新的食物。下一步,让用户能够编辑已存在的食物。
用户点击单元格打开一个用食物信息填充的场景。用户修改然后点击 Save 按钮,更新信息并覆写食物列表中对应对象。
配置 table view 单元格
-
如果辅助编辑器打开了,点击 Standard 按钮返回标准编辑器。
[图片上传失败...(image-f118a-1608214610979)]
打开 Main.storyboard。
在画板中,选择 table view cell。
-
按住 Control 从 table view cell 拖动到美食场景。
[图片上传失败...(image-c0d85f-1608214610979)]
在拖动结束的位置弹出标题为 Selection Segue 的快捷菜单。
[图片上传失败...(image-a9a69f-1608214610979)]
选择菜单中的 show。
-
在食物列表和食物场景间拖动,松开后会看到新的 segue。
[图片上传失败...(image-e07768-1608214610979)]
使用 Command+减号(-)缩小画板。
-
选择画板中新加的 segue。
[图片上传失败...(image-184310-1608214610979)]
-
在属性检查器中的 Identifier 区域输入 ShowDetail。回车确认。
[图片上传失败...(image-2772be-1608214610979)]
当 segue 触发了,push 食物视图控制器到食物列表场景所在的导航栈中。
检验:运行 app。在食物列表场景,你应该能够点击一个 table view cell 来导航到食物场景,但是场景的内容是空的。当你点击 table view 中已经存在的单元格时候,你想要的是编辑它,而不是创建一个新的。
现在有两个 segue 能跳到相同的场景了,所以你需要一种方法来区别添加一份新的食物或者编辑已存在的这两种情况。
回忆之前任何 segue 执行前都会被调用的 prepareForSegue(_:sender:) 方法。你可以使用这个方法来区别哪个 segue 正在执行,然后在食物场景中显示合适的信息。使用之前设给它们的 identifiers 来区分 segues。AddItem(model segue)和 ShowDeatil(show segue)。
区分哪个 segue 在执行
打开 MealTableViewController.swift。
-
在 MealTableViewController.swift 中,找到并取消 prepareForSegue(_:sender:) 方法的注释。
(为了取消方法的注释,移除周围的 /* 和 */ 字符。)
当你做完这些后,模板默认实现如下:
// MARK: - Navigation // In a storyboard-based application, you will often want to do a little preparation before navigation override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { // Get the new view controller using segue.destinationViewController. // Pass the selected object to the new view controller. }
-
删除两行的注释,替换为 if 语句和 else 从句。
if segue.identifier == "ShowDetail" { } else if segue.identifier == "AddItem" { }
代码比较 segue identifier 和之前分配给它们的标识字符串。
-
在 if 语句(如果食物正在被编辑就执行)中,添加如下代码:
let mealDetailViewController = segue.destinationViewController as! MealViewController
代码尝试使用强制类型转换操作符(as!) 下转 segue 的目标视图控制器到 MealViewController。注意这个操作符的最后是感叹号(!)而不是问号(?)。这意味着执行了强制类型转换。如果转换成功,将 segue.destinationViewController 转换的 MealViewController 类型的值赋给局部变量 mealDetailViewController。如果转换不成功,app 运行时应该崩溃了。
只有确认转换会成功才使用强制转换 - 如果失败,app 产生错误并崩溃,否则,请使用 as? 转换。
-
在前一行的下面,添加另一个 if 语句(嵌套在第一个里面):
// Get the cell that generated this segue. if let selectedMealCell = sender as? MealTableViewCell { }
代码尝试使用可选类型转换符(as?)转换 sender 给 MealCell。如果转换成功,将 sender 转换的 MealTableViewCell 类型的值赋给本地常量 selectedMealCell 然后 if 语句继续执行。如果转换不成功,表达式值变为 nil 然后 if 语句不执行。
-
在 if 语句里面,添加这些代码:
let indexPath = tableView.indexPathForCell(selectedMealCell)! let selectedMeal = meals[indexPath.row] mealDetailViewController.meal = selectedMeal
代码为 table view 选中的单元格拿到对应的 Meal 对象。然后赋给目标视图控制器的 meal 属性。
-
在之前添加的 else 从句里面,添加 print 语句:
print("Adding new meal.")
尽管添加新的食物时候此方法不需要做什么事情,但是记录将要发生很有用。
最后的 prepareForSegue(_:sender:) 方法应该像这样:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowDetail" {
let mealDetailViewController = segue.destinationViewController as! MealViewController
// Get the cell that generated this segue.
if let selectedMealCell = sender as? MealTableViewCell {
let indexPath = tableView.indexPathForCell(selectedMealCell)!
let selectedMeal = meals[indexPath.row]
mealDetailViewController.meal = selectedMeal
}
}
else if segue.identifier == "AddItem" {
print("Adding new meal.")
}
}
现在需要逻辑实现,你需要在 MealViewController.swift 中做一些工作来确保界面正确的更新了。特别当 MealViewController 对象创建后,它的视图应该被它的 meal 属性的数据填充,当这些数据存在的情况下。回忆下最合适做这类工作的地方就是在 viewDidLoad() 方法里面了。
更新 viewDidLoad 的实现
打开 MealViewController.swift。
-
在 MealViewController.swift 中,找到 viewDidLoad() 方法。
override func viewDidLoad() { super.viewDidLoad() // Handle the text field’s user input via delegate callbacks. nameTextField.delegate = self // Enable the Save button only if the text field has a valid Meal name. checkValidMealName() }
-
在 nameTextField.delegate 行的下面,添加这些代码:
// Set up views if editing an existing Meal. if let meal = meal { navigationItem.title = meal.name nameTextField.text = meal.name photoImageView.image = meal.photo ratingControl.rating = meal.rating }
如果 meal 属性非空设置 MealViewController 的每个视图来显示对应的数据,这只在编辑已存在的食物时候发生。
你的 viewDidLoad() 方法应该像这样:
override func viewDidLoad() {
super.viewDidLoad()
// Handle the text field’s user input via delegate callbacks.
nameTextField.delegate = self
// Set up views if editing an existing Meal.
if let meal = meal {
navigationItem.title = meal.name
nameTextField.text = meal.name
photoImageView.image = meal.photo
ratingControl.rating = meal.rating
}
// Enable the Save button only if the text field has a valid Meal name.
checkValidMealName()
}
检验:运行 app。你应该能够点击一个单元格导航到食物场景中并看到填充了食物数据。但是如果点击 Save,app 添加了新的食物而不是覆写已经存在的。下一步实现正确的行为。
[图片上传失败...(image-c0cb01-1608214610979)]
为了覆写食物列表中已经存在的食物,你需要更新 unwindToMealList(_:) 方法来处理两种不同的情况。一种情况,你需要添加一份新的食物,另一种情况,你需要替换已经存在的那份。回忆下这个方法只在用户点击保存按钮的时候调用,所以你不需要在这个方法中处理 Cancel 按钮。
更新 unwindToMealList(_:) 实现来添加或者替换食物
打开 MealTableViewController.swift。
-
在 MealTableViewController.swift,找到 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) } }
-
在 if 语句的开头,添加如下 if 语句:
if let selectedIndexPath = tableView.indexPathForSelectedRow { }
代码检查 table view 中的行是否被选中。如果选中,意味着用户点击其中的一个单元格来编辑食物。
-
在 if 语句中添加如下代码:
// Update an existing meal. meals[selectedIndexPath.row] = meal tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
第一行更新 meals 中对应的食物信息。第二行刷新 table view 中合适的行来显示修改后的数据。
-
在 if 语句后面,添加如下的 else 从句:
else { // Add a new meal. let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0) meals.append(meal) tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom) }
选择 else 从句中这些行按 Control-I 确保它们正确缩进。
当在 table view 中没有选中行时执行 else 从句,这意味着用户点击增加按钮来打开食物场景。换言之,就是如果正在添加一份新的食物 else 从句就会执行。
最后的 unwindToMealList(_:) 方法应该像这样:
@IBAction func unwindToMealList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal {
if let selectedIndexPath = tableView.indexPathForSelectedRow {
// Update an existing meal.
meals[selectedIndexPath.row] = meal
tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None)
}
else {
// Add a new meal.
let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0)
meals.append(meal)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom)
}
}
}
检验:运行 app。你应该能够点击一个 table view cell 来导航到食物场景并看到它已经被食物数据填充了。如果你点击保存,你做的修改会覆写列表中现有的食物。
[图片上传失败...(image-906f1a-1608214610979)]
取消编辑现有的食物
用户或许决定不再编辑食物了,想在没有保存任何修改的情况下返回食物列表。为了做到这点,更新 Cancel 按钮的行为来适当的关闭场景。
关闭的类型取决于打开时的类型。当用户点击 Cancel 按钮时做一个检查来决定当前场景如何打开的。如果是模态打开的(使用 Add 按钮),使用 dismissViewControllerAnimated(_:completion:) 关闭。如果使用 push 导航打开的(点击单元格),会被那个打开它的导航控制器关闭的。
修改取消方法
打开 MealViewController.swift。
-
在 MealViewController.swift 中,找到 cancel(_:) 方法。
@IBAction func cancel(sender: UIBarButtonItem) { dismissViewControllerAnimated(true, completion: nil) }
当前只使用了
dismissViewControllerAnimated
来关闭食物场景因为你目前仅仅为 Add 按钮做处理了。 -
在 cancel(_:) 方法的最前面行,添加如下代码:
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways. let isPresentingInAddMealMode = presentingViewController is UINavigationController
代码创建一个布尔值表明打开这个场景视图控制器是否为 UINavigationController 类型。正如常量名字 isPresentingInAddMealMode 说明,这意味着食物场景使用 Add 按钮打开的。因为当食物场景以这种方式打开时候它被嵌入到它自己的导航控制器中,这意味着是导航控制器打开了它。
-
在你刚添加的行下面,添加如下 if 语句,并且移动 dismissViewControllerAnimated 那行到它里面:
if isPresentingInAddMealMode { dismissViewControllerAnimated(true, completion: nil) }
之前,dismissViewControllerAnimated 每次都会在 cancel(_:) 方法触发时调用,现在只在 isPresentingInAddMealMode 是 true 的情况下执行。
-
在 if 语句的右边,添加如下 else 从句:
else { navigationController!.popViewControllerAnimated(true) }
现在有 if 语句同时有个 else 从句,if 语句中的代码只会在 isPresentingInAddMealMode 是 true 的情况下执行,否则执行 else 从句的代码。当食物场景被 push 到导航栈时 else 从句执行。else 从句中代码调用
popViewControllerAnimated
从导航栈中弹出当前视图控制器(食物场景)并同时执行一个过渡动画。
最后的 cancel(_:) 方法应该像这样:
@IBAction func cancel(sender: UIBarButtonItem) {
// Depending on style of presentation (modal or push presentation), this view controller needs to be dismissed in two different ways.
let isPresentingInAddMealMode = presentingViewController is UINavigationController
if isPresentingInAddMealMode {
dismissViewControllerAnimated(true, completion: nil)
}
else {
navigationController!.popViewControllerAnimated(true)
}
}
检验:运行 app。现在当你点击 Add 按钮(+)然后点击 Cancel 而不是 Save,你应该导航返回到食物列表了。
支持删除食物
下一步,让用户能够从食物列表中删除一份食物。需要用户能够将 table view 变成编辑模式来删除单元格。通过在 table view 导航栏添加一个编辑按钮来完成这个功能。
为 table view 添加编辑按钮
打开 MealTableViewController.swift。
-
在 MealTableViewController.swift 中,找到 viewDidLoad() 方法。
override func viewDidLoad() { super.viewDidLoad() // Load the sample data. loadSampleMeals() }
-
在 super.viewDidLoad() 行的下面,添加下面这行代码:
// Use the edit button item provided by the table view controller. navigationItem.leftBarButtonItem = editButtonItem()
创建了一个特别的 bar button item 内置具有编辑功能。之后添加这个按钮到食物场景导航栏的左边。
viewDidLoad() 方法应该像这样:
override func viewDidLoad() {
super.viewDidLoad()
// Use the edit button item provided by the table view controller.
navigationItem.leftBarButtonItem = editButtonItem()
// Load the sample data.
loadSampleMeals()
}
检验:运行 app。注意在 table view 导航栏的左边有个编辑按钮。如果点击编辑按钮,table view 变成编辑模式-但是你还是没法删除单元格,因为你没有实现编辑操作。
为了执行在 table view 的编辑操作,你需要实现其中一个代理方法,tableView(_:commitEditingStyle:forRowAtIndexPath:)。这个代理方法负责在编辑模式时管理表格行。
你也需要取消 tableView(_:canEditRowAtIndexPath:) 的注释来支持编辑。
删除一份食物
-
在 MealTableViewController.swift 中,找到并取消
tableView(_:commitEditingStyle:forRowAtIndexPath:)
的注释。(为了取消这个方法的注释,移除它周围的 /* 和 */ 字符。)当你做了这些,模板默认实现如下:
// Override to support editing the table view. override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == .Delete { // Delete the row from the data source tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) } else if editingStyle == .Insert { // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view } }
-
在
// Delete the row from the data source
这行注释的下面,添加下面这行代码:meals.removeAtIndex(indexPath.row)
-
在
MealTableViewController.swift
中,找到并取消tableView(_:canEditRowAtIndexPath:)
方法的注释。做完后,模板默认实现如下:
// Override to support conditional editing of the table view. override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool { // Return false if you do not want the specified item to be editable. return true }
tableView(_:commitEditingStyle:forRowAtIndexPath:)
方法应该像这样:
// Override to support editing the table view.
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
if editingStyle == .Delete {
// Delete the row from the data source
meals.removeAtIndex(indexPath.row)
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
} else if editingStyle == .Insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
检验:运行 app。如果点击编辑按钮,table view 变成编辑模式。可以选择一个单元格通过点击左边的指示器来删除,然后通过按单元格的 Delete 按钮确认删除它。或者,向左清扫一个单元格来快速打开 Delete 按钮;这是 table view 内置的功能。当你点击单元格删除按钮,它就从列表中删除了。
[图片上传失败...(image-670155-1608214610979)]
注意
为了看到本课的完整的示例项目,下载文件并在 Xcode 中查看它。