Start Developing iOS Apps (Swift)->实现导航(二)

在菜品列表中存储新菜品

创建FoodTracker应用实用功能的下一个步骤是实现用户添加新菜品的能力。具体来说就是,当用户在菜品详情场景输入菜品名、评分、以及照片,并点击Save按钮时候,你想让MealViewController利用这些信息配置Meal对象,并把它传递给MealTableViewController显示在菜品列表上。

通过添加Meal属性到MealViewController开始

添加菜品属性到MealViewController

  1. 打开MealViewController.swift文件
  2. 在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代码

  1. 打开storyboard。
  2. 打开助理编辑器。


    image: ../Art/assistant_editor_toggle_2x.png
  3. 尽可能扩展操作空间。


    image: ../Art/navigator_utilities_toggle_on_2x.png
  4. 在storyboard中,选择Save按钮。
  5. 按住Control键,从Save按钮拖拽一条线到右侧的编辑器代码中,到ratingControl属性下面的时候放手。


    image: ../Art/IN_savebutton_dragoutlet_2x.png
  6. 在出现的对话框中,Name字段键入saveButton。
    其他的属性保持不变。对话框应该是这样的:


    image: ../Art/IN_savebutton_addoutlet_2x.png
  7. 点击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:)方法

  1. 回到标准编辑器。


    image: ../Art/standard_toggle_2x.png
  2. 打开MealViewController.swift文件。
  3. 在文件顶部,蹈入import UIKit,添加如下代码:
import os.log

这会导入统一的日志系统。像print()函数,统一日志系统让你发送消息到控制台。然而,统一日志系统在何时出现消息以及如何保存消息方面给了你更多的控制。

进一步探索
更多关于统一日志系统的消息,参见 Logging Reference。

  1. 在MealViewController.swift中,在 //MARK: Actions部分的上面,添加如下注释:
    //MARK: Navigation
  1. 在这个注释下面,添加下面这个方法框架:
        // This method lets you configure a view controller before it's presented.
        override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        }
  1. 在prepare(for:sender:)方法中,添加对超类实现的调用:
        super.prepare(for: segue, sender: sender)

这个UIViewController类的实现方法没有做任何事,但是当你重写prepare(for:sender:)的时候总是调用super.prepare(for:sender:)是一个好习惯。这样的话当你子类化一个不同类的时候就不会忘了。

  1. 在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中的语句执行。应用使用系统标准的日志机制来记录调试信息。调试信息包含在调试期间或者分析特定问题的时候有用的信息。它们只用于调试环境中,不会出现在传送应用的时候。在输出了调试信息后,该方法返回。

  1. 在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,那么操作符就会返回空字符串(“”)。

  1. 紧接着添加下面的代码:
        // 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

  1. 打开MealViewController.swift。
  2. 在//MARK: Private Methods部分之前,添加下面的注释:
        //MARK: Actions
  1. 在//MARK: Actions注释后面,添加如下代码:
        @IBAction func unwindToMealList(sender: UIStoryboardSegue) {
            
        }
  1. 在 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语句不会执行。

  1. 在if语句中,添加如下代码:
        // Add a new meal.
        let newIndexPath = IndexPath(row: meals.count, section: 0)

这段代码计算新cell显示在table view中的位置,并且把它存储在本地常量newIndexPath中。

  1. 在if语句中,在之前代码的下面,添加如下代码:
        meals.append(meal)

这是把新meal添加到已存在的菜品列表数据模型中。

  1. 在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方法

  1. 打开storyboard。
  2. 在画布上,按住Control键,拖拽Save按钮到菜品场景顶部的Exit项目上。


    image: ../Art/IN_savebutton_dragunwind_2x.png

    在拖拽结束的地方出现一个菜单。它显示了所有可用的unwind 动作方法。


    image: ../Art/IN_savebutton_unwindsegue_2x.png
  3. 选择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按钮不可用

  1. 在MealViewController.swift文件中,找到 //MARK: UITextFieldDelegate部分。
  2. 在这部分中,添加另一个UITextFieldDelegate方法:
        func textFieldDidBeginEditing(_ textField: UITextField) {
            // Disable the Save button while editing.
            saveButton.isEnabled = false
        }

这个方法在编辑会话开始或者键盘出现的时候调用。这段代码让用户在编辑text field的时候Save按钮不可用。

  1. 滚动到类的底部,在结束花括号前面添加下面的注释:
        //MARK: Private Methods
  1. 在//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按钮不可用。

  1. 返回到//MARK: UITextFieldDelegate部分,并找到textFieldDidEndEditing(_:)方法:
        func textFieldDidEndEditing(_ textField: UITextField) {
        }

这个方法现在是空的。

  1. 添加这些代码:
        updateSaveButtonState()
        navigationItem.title = textField.text

首先调用updateSaveButtonState()方法来检查text filed是否有文本,从而确定Save按钮是否可用。第二行使用这个文本设置场景的标题。

  1. 找到 viewDidLoad()方法。
        override func viewDidLoad() {
            super.viewDidLoad()
            
            // Handle the text field’s user input through delegate callbacks.
            nameTextField.delegate = self
        }
  1. 在实现文件中添加一个调用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按钮是不可用的

image: ../Art/IN_sim_savebuttondisabled_2x.png

取消添加新菜品

用户或许决定取消添加新菜品,并且不保存任何数据的返回到菜品列表界面。为此,你需要实现Cancel按钮的功能。

创建并实现cancel动作方法

  1. 打开storyboard。
  2. 打开助理编辑器。


    image: ../Art/assistant_editor_toggle_2x.png
  3. 在storyboard中,选择Cancel按钮。
  4. 按住Control键,从Cancel按钮处拖拽到右边编辑器的代码中,停在 //MARK: Navigation注释的下面,松手。


    image: ../Art/IN_cancelbutton_dragaction_2x.png
    image: ../Art/IN_cancelbutton_dragaction_2x.png
  5. 在出现的对话框中,Connection字段,选择Action。
  6. Name字段,键入cancel。
  7. Type字段,选择UIBarButtonItem。
    其他选项不变。你的对话框看上去是这样的:


    image: ../Art/IN_cancelbutton_addaction_2x.png
  8. 点击Connect。
    Xcode添加必要的代码到MealViewController.swift中来设置action方法。
        @IBAction func cancel(_ sender: UIBarButtonItem) {
        }
  1. 在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中打开。
下载文件

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容