iOS 技术文档收录iOS文档翻译

Start Developing iOS Apps (Swift

2017-06-12  本文已影响105人  raingu24

在本课中,你要关注两个功能——允许用户编辑和删除FoodTracker应用中的菜品。

学习目标

在本课结束的时候,你将能够:

启用现有菜品的编辑

当前,FoodTracker应用给用户添加新菜品到菜品列表的功能。在本课中,你将启用已有菜品的编辑。

当用户点击一个在表格场景中的菜品时,你将在详情场景显示这个菜品。用户能够改变这个菜品。如果他们点击Save按钮,你将同时更新菜品的数据以及它在菜品列表中的呈现。注意,应用没有保存模型数据。每当应用重启,它总是初始化样本数据。不过,当应用运行的时候,用户仍能修改数据。

通过设置菜品列表和菜品详情场景之间的segue开始。

配置table view cell

  1. 如果打开了助理编辑器,回到标准编辑器。


    image: ../Art/standard_toggle_2x.png
  2. 打开storyboard。
  3. 在画布上,选择在菜品列表场景中的table view cell。
  4. 按住Control键拖拽这个cell到菜品详情场景。


    image: ../Art/IEDB_drag_tabletomealscene_2x.png
    image: ../Art/IEDB_seguemenu_2x.png
  5. 在弹出的 选择segue菜单 选择Show。这个会使导航控制器将菜品详情场景压栈到导航控制器。
  6. 把菜品列表场景和菜品详情场景中键的导航控制器向下拉,就能看到这个新segue。


    image: ../Art/IEDB_drag_navcontroller_2x.png

    如果愿意,你能够使用在画布底部的缩放命令来缩放。

  7. 在画布上,选择新添加的segue。


    image: ../Art/IEDB_selectsegue_2x.png
  8. 在Attributes inspector,在Identifier键入ShowDetail。按下回车键。


    image: ../Art/IEDB_firstcheckpoint_2x.png

    创建新菜品和编辑现有菜品是非常相似的操作。因为,完成这两个任务你使用的是同一个界面。当然,你需要为场景的呈现和它的行为做一些修改。当用户添加新菜品和编辑一个现有菜品时,你需要一种方法来识别。

    回想一下,prepare(for:sender:)方法是在任何segue执行之前被调用的。你可以使用这个方法来识别哪个segue正在使用,并且在菜品详情场景中显示合适的信息。你是基于你给segue分配的标识符来区分它们的:当添加新菜品时是AddItem,而当编辑已有菜品的时是ShowDetail。

    辨别哪个segue正在使用

    1. 打开MealTableViewController.swift
    2. 在文件的顶部,紧跟着导入UIKit下面,导入统一日志系统:
        import os.log
    
    1. 在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.
            }
    

    因为MealTableViewController是UITableViewController的子类,所以模版实现带有prepare(for:sender:)骨架。

    1. 删除两行注释,并且用调用超类的实现来替代它们。
            super.prepare(for: segue, sender: sender)
    
    1. 在调用super.prepare(for:sender:)后,添加下面的switch语句:
            switch(segue.identifier ?? "") {
                
            }
    

    Switch语句考虑一个值,并将它和几种可能的匹配模式进行比较。然后基于第一个匹配成功的模式,执行合适的代码块。当要在多个选项中进行选择的时候使用switch语句代替if语句。
    上面的代码检查segue的标识符。如果标识符为nil, nil-coalescing 运算符(??)空字符串(“”)代替它。在本例中你不需要处理多个选项,这件化了switch语句的逻辑。

    1. 添加AddItem分支到switch。
            case "AddItem":
            os_log("Adding a new meal.", log: OSLog.default, type: .debug)
    

    如果用户添加一个项目到菜品列表,你不需要改变菜品详情场景的外观。只是在控制台记录一条简单的调试信息。如果你调试代码,这将帮助你跟踪应用的工作流。

    1. 添加ShowDetail分支到switch。
            case "ShowDetail":
            guard let mealDetailViewController = segue.destination as? MealViewController else {
                fatalError("Unexpected destination: \(segue.destination)")
            }
             
            guard let selectedMealCell = sender as? MealTableViewCell else {
                fatalError("Unexpected sender: \(sender)")
            }
             
            guard let indexPath = tableView.indexPath(for: selectedMealCell) else {
                fatalError("The selected cell is not being displayed by the table")
            }
             
            let selectedMeal = meals[indexPath.row]
            mealDetailViewController.meal = selectedMeal
    

    如果你正在编辑已有册菜品,你需要在菜品详情场景中显示这个菜品的数据。这个代码以获取目标视图控制器开始,然后选择菜品cell,然后获取选中cell的index path。guard 语句检查所有执行的降级工作、以及所有包含非空值的可选类型。这里,guard语句只是进行简单的合理检查。如果你的storyboard被正确的设置,guard语句将不会失败。
    一旦你又了index path,你可以查找这个路径上的菜品对象,并传递给目标视图控制器。

    1. 添加默认分支。
            default:
            fatalError("Unexpected Segue Identifier; \(segue.identifier)")
    

    如果你的storyboard被正确设置,这个默认分支永远不会被执行。但是,如果你稍后从你的菜品场景添加另外的segue而忘了更新 prepare(for:sender:)方法,新segue的标识符和两个分支标识符(AddItem、ShowDetail)将都不匹配。这种情况,switch语句会在控制台打印错误信息,并终止应用。

    你的 prepare(for:sender:)方法现在看起来是这样的:

            override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
                
                super.prepare(for: segue, sender: sender)
                
                switch(segue.identifier ?? "") {
                    
                case "AddItem":
                    os_log("Adding a new meal.", log: OSLog.default, type: .debug)
                    
                case "ShowDetail":
                    guard let mealDetailViewController = segue.destination as? MealViewController else {
                        fatalError("Unexpected destination: \(segue.destination)")
                    }
                    
                    guard let selectedMealCell = sender as? MealTableViewCell else {
                        fatalError("Unexpected sender: \(sender)")
                    }
                    
                    guard let indexPath = tableView.indexPath(for: selectedMealCell) else {
                        fatalError("The selected cell is not being displayed by the table")
                    }
                    
                    let selectedMeal = meals[indexPath.row]
                    mealDetailViewController.meal = selectedMeal
                    
                default:
                    fatalError("Unexpected Segue Identifier; \(segue.identifier)")
                }
            }
    

    你现在已经有了逻辑实现,那么打开 MealViewController.swift,来确保UI正确的更新。具体来说,当一个MealViewController(菜品详情场景)的实例被被创建的时候,它的视图应该被菜品属性的数据所占据,如果这些数据存在的话。

    你要在viewDidLoad()方法中做这一步的工作。

    更新viewDidLoad()的实现

    1. 打开MealViewController.swift。
    2. 在MealViewController.swift.中,找到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()
            }
    
    1. 在nameTextField.delegate行下面,添加下面的代码。如果meal属性非空,这个代码会设置MealViewController中的每个视图,以便显示来自meal属性的数据。meal属性只有在非空的时候,现有的菜品才能被编辑。
            // 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
            }
    

    你的viewDidLoad()方法看上去是这样的:

            override func viewDidLoad() {
                super.viewDidLoad()
                
                // Handle the text field’s user input through 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.
                updateSaveButtonState()
            }
    

    检查点:运行应用。从菜品列表点击菜品导航到菜品详情场景。这个详情场景应该使用相关菜品的数据进行了预填充。不幸的是,Save按钮还不能工作。如果点击Save,应用不会更新这个菜品,它只会添加一个新的菜品。接下来,你要修复这个问题。

    image: ../Art/IEDB_sim_editmeal_2x.png

    为了更新已存在的菜品,你要对unwindToMealList(sender:)方法进行修改,让它能够处理两种不同的情况:添加一个新菜品和修改一个已有的菜品。回顾一下,这个方法只是在用户点击Save按钮的时候调用,所以你不需要在这个方法中考虑Cancel按钮。

    更新unwindToMealList(sender:)的实现方法以便既能添加又能编辑菜品

    1. 打开 MealTableViewController.swift。
    2. 在 MealTableViewController.swift中,找到unwindToMealList(sender:)方法。
            @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)
                }
            }
    
    1. 在if语句开始的地方,添加下面的if语句:
            if let selectedIndexPath = tableView.indexPathForSelectedRow {
            }
    

    这个代码检查在这个table view中是否有一个行杯选中。如果有,这就意味着用户点击了一个cell用以编辑菜品。换句话说,这个if语句会在你编辑已有菜品的时候被执行。

    1. 在这个if语句中,添加如下代码:
            // Update an existing meal.
            meals[selectedIndexPath.row] = meal
            tableView.reloadRows(at: [selectedIndexPath], with: .none)
    

    第一行更新meals数组。它使用新的菜品对象替换了旧的。第二行冲加载了table view中的相应的行。这使用一个包含更新了菜品的数据的新cell代替了当前的cell,结果就是,当table view再次出现是,这个用户选择的行现在显示的是编辑过的菜品。

    1. 在if语句后面,添加一个else分支,并用花括号包裹这个方法中的最后四行。确保这些行都在else分支语句正确的缩紧,可以在选中这些行的情况下按下Control-I实现。
            else {
                // Add a new meal.
                let newIndexPath = IndexPath(row: meals.count, section: 0)
                
                meals.append(meal)
                tableView.insertRows(at: [newIndexPath], with: .automatic)
            }
    

    这个else分支语句会在没有选中table view 的行时执行,这也就是说用户点击的是菜品详情场景中的加号按钮。换句话说,这个else分支语句是当用户添加新菜品的时候调用。

    现在你的unwindToMealList(sender:)方法看上去是这样的。

            @IBAction func unwindToMealList(sender: UIStoryboardSegue) {
                if let sourceViewController = sender.source as? MealViewController, let meal = sourceViewController.meal {
                    
                    if let selectedIndexPath = tableView.indexPathForSelectedRow {
                        // Update an existing meal.
                        meals[selectedIndexPath.row] = meal
                        tableView.reloadRows(at: [selectedIndexPath], with: .none)
                    }
                    else {
                        // Add a new meal.
                        let newIndexPath = IndexPath(row: meals.count, section: 0)
                        
                        meals.append(meal)
                        tableView.insertRows(at: [newIndexPath], with: .automatic)
                    }
                }
            }
    

    检查点:运行应用。点击table view cell导航到菜品详情场景,并且看到它被菜品数据预填充。如果你点击Save,你所做的改变就会显示在菜品列表。

    image: ../Art/IEDB_sim_overwritemeal_2x.png

    取消编辑已有菜品

    用户可能决定不保存已编辑的菜品,并且不做任何改变的返回到菜品列表。为此,你将更新Cancel按钮的行为来恰当的移除场景。

    移除样式是基于场景被呈现的样式的。当用户点击Cancel按钮的时候,你将实现一个检查来确定当前场景是如何被呈现的。如果是模态方式呈现(用户点击加号按钮),它将使用dismissViewControllerAnimated(_:completion:)方法移除。如果它是使用push导航呈现的(用户点击table view cell),它将通过呈现它的导航控制器来移除它。

    进一步探索
    不同的呈现方式有不同的用处。当表达一个任务在继续之前用户必须完成或取消它时(例如添加一个新菜品),使用模态方式呈现场景。当用户要浏览分级数据时(例如,从菜品列表中选择一个菜品),使用导航控制器呈现场景。
    更多信息,查看iOS Human Interface Guidelines.中的Interaction > Modality and Interaction > Navigation。

    改变cancel方法的实现

    1. 打开MealViewController.swift。
    2. 在MealViewController.swift中,找到cancel(_:)方法。
            @IBAction func cancel(_ sender: UIBarButtonItem) {
                dismiss(animated: true, completion: nil)
            }
    

    这个实现方法现在只使用 dismiss(animated:completion:)来移除菜品详情场景,因为迄今为止你只使用加号按钮。

    1. 在 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表示的,菜品详情场景是通过用户点击加号按钮而被呈现的。这是因为在这中情况下菜品详情场景嵌套在它自己的导航控制器内,也就是导航控制器呈现了它。

    1. 在你刚添加的代码下面,添加下面的if语句,并把dismissViewControllerAnimated方法移到它里面:
            if isPresentingInAddMealMode {
                dismiss(animated: true, completion: nil)
            }
    

    之前,当用户点击Cancel按钮的时候你总是调用dismiss(animated:completion:)方法;但是只有当用户添加新菜品的时候dismiss(animated:completion:)才会工作。因此,现在代码在调用dismiss(animated:completion:)之前先检查用户是否要添加一个新菜品。注意,当用户是在编辑菜品的时候,场景仍然不会移除。接下来你会添加该代码。

    1. 紧跟着if语句,添加else分支语句:
            else if let owningNavigationController = navigationController{
                owningNavigationController.popViewController(animated: true)
            }
    

    如果用户正在编辑一个已存在的菜品,这个else块会被调用。这也意味着当用户选择菜品列表上的菜品的时候,菜品详情场景会被压栈道导航栈上。这个else语句使用一个if let语句来安全的解包视图控制器的navigationController属性。如果视图控制器已经被压栈到导航栈,这个属性会包含一个指向栈的导航控制器的引用。
    在else分支语句中的代码执行一个名为popViewController(animated:)的方法,这个方法会把当前视图控制器(菜品详情场景)从导航栈中弹出,并进行动画转换。这会移除菜品详情场景,返回到菜品列表。

    1. 紧接着第一个else语句立刻添加第二个else分支语句:
            else {
                fatalError("The MealViewController is not inside a navigation controller.")
            }
    

    这个else分支语句只有在菜品详情场景既不在模态当行控制器(例如添加一个新菜品)中也不在导航栈(例如编辑菜品)的时候才会被执行。如果你的应用导航流设置正确,这个else分支不会被执行。如果它执行,代表你的应用有错误。这个else分支会在控制台打印一个错误信息,并终止应用。

    你的 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 {
                    dismiss(animated: true, completion: nil)
                }
                else if let owningNavigationController = navigationController{
                    owningNavigationController.popViewController(animated: true)
                }
                else {
                    fatalError("The MealViewController is not inside a navigation controller.")
                }
            }
    

    检查点:运行应用。当你选择菜品的时候,你能点击Cancel来返回到菜品列表而不需要保存任何改变。另外,当你点击加号按钮然后点击Cancel按钮的时候,会带你回到菜品列表而不添加新菜品。

    支持删除菜品

    接下来,你将给用户一个从菜品列表删除菜品的功能。你需要一个方式让用户把table view设置到编辑模式,从中它们可以删除cell。你可以通过添加一个Edit按钮到table view的导航栏来实现它。

    添加Edit按钮到table view

    1. 打开MealTableViewController.swift。
    2. 在MealTableViewController.swift中,找到 viewDidLoad()方法。
            override func viewDidLoad() {
                super.viewDidLoad()
                
                // Load the sample data.
                loadSampleMeals()
            }
    
    1. 在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()
            }
    

    检查点:运行应用。注意有一个Edit按钮在table view的导航栏左侧。如果你点击这个按钮,table view就进入到了编辑模式——但是你还不能删除cell,因为你还没有实现它。

    image: ../Art/IEDB_sim_editbutton_2x.pngimage: ../Art/IEDB_sim_editbutton_2x.png

    要想在table view上执行任何类型的编辑,你需要实现一个它的委托方法,tableView(_:commit:forRowAt:)。这个委托方法管理行在它处于编辑模式时的改变。

    去除tableView(_:commit:forRowAt:)实现方法的注释。

    删除菜品

    1. 在MealTableViewController.swift中,找到tableView(_:commit:forRowAt:)方法并删除它的注释。
      你的模版实现方法看上去是这样的:
            // Override to support editing the table view.
            override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
                if editingStyle == .delete {
                    // Delete the row from the data source
                    tableView.deleteRows(at: [indexPath], with: .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
                }
            }
    
    1. 在// Delete the row from the data sourc注释下面,添加:
            meals.remove(at: indexPath.row)
    

    这段代码从meals删除这个Meal对象。接下来的那行代码用来从table view删除行。

    1. 在 MealTableViewController.swift中,找到tableView(_:canEditRowAt:)并删除注释。
      删除之后,这个模版实现看上去是这样的:
            // Override to support conditional editing of the table view.
            override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> 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, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
                if editingStyle == .delete {
                    // Delete the row from the data source
                    meals.remove(at: indexPath.row)
                    tableView.deleteRows(at: [indexPath], with: .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
                }
            }
    

    检查点:运行应用。如果你点击Edit按钮,table view进入编辑模式。你通过左侧的指示器来选择要删除cell,并通过点击cell中的Delete按钮来确定删除。或者,向左滑动cell快速显示出Delete按钮;这是个table view内建的行为。当你点击Delete按钮的时候,cell从列表中被移除。

    image: ../Art/IEDB_sim_deletebehavior_2x.pngimage: ../Art/IEDB_sim_deletebehavior_2x.png

    进一步探索
    当在编辑模式的时候,评分控件扩展到了删除按钮中。这时因为cell的布局并没有使用Auto Layout的关系。这个控件适合正常分配的控件,但当控件减小时控件不能适应。
    为了修好它,你需要使用嵌套的栈视图并使用Auto Layout的约束来布局;这是个留个读者的练习。
    更多信息,参见Auto Layout Guide.

    小结

    在本课中,你为菜品列表添加编辑和删除的菜品的支持。因为编辑一个菜品和创建一个新菜品非常相似,所以应用使用了同一个菜品详情场景。结果是,在你呈现视图控制器的时候你需要区分呈现的方式,模态的还是压栈的。你要基于它的呈现方式来修改菜品详情场景的外观和行为。

    你能添加、编辑、和删除菜品。但是这些数据没有保存。每次重启应用的时候,你总是从初始样本数据开始。在下一课中,你将添加代码来保存和加载菜品列表。

    注意
    想看本课的完整代码,下载这个文件并在Xcode中打开。
    下载文件

上一篇下一篇

猜你喜欢

热点阅读