在菜品列表中存儲新菜品
創建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。
-
打開助理編輯器。
image: ../Art/assistant_editor_toggle_2x.png -
盡可能擴展操作空間。
image: ../Art/navigator_utilities_toggle_on_2x.png - 在storyboard中,選擇Save按鈕。
-
按住Control鍵,從Save按鈕拖拽一條線到右側的編輯器代碼中,到ratingControl屬性下面的時候放手。
image: ../Art/IN_savebutton_dragoutlet_2x.png -
在出現的對話框中,Name字段鍵入saveButton。
其他的屬性保持不變。對話框應該是這樣的:
image: ../Art/IN_savebutton_addoutlet_2x.png - 點擊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:)方法
-
回到標準編輯器。
image: ../Art/standard_toggle_2x.png - 打開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項目上。
image: ../Art/IN_savebutton_dragunwind_2x.png
在拖拽結束的地方出現一個菜單。它顯示了所有可用的unwind 動作方法。
image: ../Art/IN_savebutton_unwindsegue_2x.png - 選擇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。
-
打開助理編輯器。
image: ../Art/assistant_editor_toggle_2x.png - 在storyboard中,選擇Cancel按鈕。
-
按住Control鍵,從Cancel按鈕處拖拽到右邊編輯器的代碼中,停在 //MARK: Navigation注釋的下面,松手。
image: ../Art/IN_cancelbutton_dragaction_2x.png - 在出現的對話框中,Connection字段,選擇Action。
- Name字段,鍵入cancel。
-
Type字段,選擇UIBarButtonItem。
其他選項不變。你的對話框看上去是這樣的:
image: ../Art/IN_cancelbutton_addaction_2x.png - 點擊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中打開。
下載文件