在上一章中,您創(chuàng)建了一個在 UITableView 中顯示 Item 實例列表的應(yīng)用程序。 下一步是允許用戶與表進(jìn)行交互——添加,刪除和移動行。 圖11.1顯示了本章結(jié)束后的 Homepwner
。
圖11.1 編輯模式下的Homepwner
編輯模式
UITableView 有一個 editing
屬性,當(dāng)該屬性設(shè)置為 true
時,UITableView 進(jìn)入編輯模式。 表視圖處于編輯模式后,表的行可由用戶操縱。 根據(jù)表視圖的配置方式,用戶可以更改行的順序,添加行或刪除行。(編輯模式不允許用戶編輯行的文本內(nèi)容。)
但首先,用戶需要一個將 UITableView 置于編輯模式的方法。 現(xiàn)在,您將在表的 頭部視圖(header view
) 中添加一個按鈕。 頭部視圖顯示在表格的頂部,對于添加 section 范圍內(nèi) 或 表范圍內(nèi) 的標(biāo)題和控件很有用。它可以是任何 UIView 實例。
請注意,表視圖有兩種 “header” : table header 和 section header。 同樣,也有 table footer 和 section footer(圖11.2)。
圖11.2 header 和 footer
您正在創(chuàng)建一個頭部視圖。 它將有兩個子視圖并且是 UIButton 的實例:一個用于切換編輯模式,另一個用于在表中添加一個新的 Item。 您可以以編程方式創(chuàng)建此視圖,但在這種情況下,您將在故事板文件中創(chuàng)建這個視圖及其子視圖。
首先,我們設(shè)置必要的代碼。 重新打開 Homepwner.xcodeproj
。 在 ItemsViewController.swift
中,實現(xiàn)兩個方法。
class ItemsViewController: UITableViewController {
??var itemStore: ItemStore!
??@IBAction func addNewItem(_ sender: UIButton) {
??}
??@IBAction func toggleEditingMode(_ sender: UIButton) {
??}
現(xiàn)在打開 Main.storyboard。 從對象庫中,將 View 拖動到 prototype cell 上方的表視圖的最頂部。 這將添加視圖作為表視圖的 頭部視圖。 調(diào)整這個視圖的高度大約為 60 點。 (如果你要確切地修改,可以使用尺寸檢查器)。
現(xiàn)在將兩個 Button 從對象庫拖動到頭部視圖。 更改文本并定位它們,如圖11.3所示。 您不需要和圖中一樣精確——您將很快添加約束來定位按鈕。
圖11.3添加按鈕到頭部視圖
選擇這兩個按鈕并打開 自動布局 Align 菜單。 在 Vertically in Container 選擇常量0.確保 Update Frames 設(shè)置為 None,然后單擊 Add 2 Constraints(圖11.4)。
圖11.4 對齊(Align)菜單約束
打開 Add New Constraints 菜單并進(jìn)行配置,如圖11.5所示。 確保在鍵入它們之后保存 前部(leading) 和 尾部(trailing) 約束的值; 有時值不會保存,所以有點棘手。 完成后,單擊 Add 4 Constraints。
圖11.5 添加新約束
最后,連接兩個按鈕的動作,如圖11.6所示。
圖11.6連接兩個動作
構(gòu)建并運(yùn)行應(yīng)用程序以查看界面。
現(xiàn)在我們來實現(xiàn) toggleEditingMode(_ :) 方法。 您可以直接修改 UITableView 的 editing
屬性。 但是, UIViewController 也有 editing
屬性。 UITableViewController 實例自動設(shè)置其表視圖的 editing
屬性以匹配自己的 editing
屬性。 通過在視圖控制器本身設(shè)置 editing
屬性,可以確保界面的其他方面也同時進(jìn)入或離開編輯模式。例子將在第14章中 UIViewController 的 editButtonItem
看到。
要設(shè)置視圖控制器的 isEditing
屬性,可以調(diào)用 setEditing(_:animated :) 方法。 在 ItemsViewController.swift
中,實現(xiàn) toggleEditingMode(_ :)。
@IBAction func toggleEditingMode(_ sender: UIButton) {
??// If you are currently in editing mode...
??if isEditing {
????// Change text of button to inform user of state
????sender.setTitle("Edit", for: .normal)
????// Turn off editing mode
????setEditing(false, animated: true)
??} else {
????// Change text of button to inform user of state
????sender.setTitle("Done", for: .normal)
????// Enter editing mode
????setEditing(true, animated: true)
??}
}
構(gòu)建并運(yùn)行您的應(yīng)用程序。 點擊 Edit
按鈕,UITableView 將進(jìn)入編輯模式(圖11.7)。
圖11.7 編輯模式下的UITableView
添加行
在運(yùn)行時,有兩個通用的界面用于在表視圖中添加行。
- 表格視圖cell上方的按鈕:通常用于添加有詳細(xì)視圖的記錄。 例如,在
聯(lián)系人(Contacts)
應(yīng)用中,當(dāng)您遇到新人并想要取消他或她的信息時,您可以點擊一個按鈕。 - 具有綠色加號的cell:通常用于向記錄添加新字段,例如當(dāng)您要在
聯(lián)系人(Contacts)
應(yīng)用程序中為個人記錄添加生日時。 在編輯模式下,點擊添加生日(add birthday)
旁邊的綠色加號。
在本練習(xí)中,您將使用第一個選項,并在標(biāo)題視圖中創(chuàng)建一個新按鈕。 當(dāng)點擊此按鈕時,新行將添加到 UITableView。
在 ItemsViewController.swift
中,實現(xiàn) addNewItem(_ :)。
@IBAction func addNewItem(_ sender: UIButton) {
??// Make a new index path for the 0th section, last row
??let lastRow = tableView.numberOfRows(inSection: 0)
??let indexPath = IndexPath(row: lastRow, section: 0)
??// Insert this new row into the table
??tableView.insertRows(at: [indexPath], with: .automatic)
}
構(gòu)建并運(yùn)行應(yīng)用程序。 點擊 Add
按鈕,...應(yīng)用程序崩潰。 控制臺告訴您,表視圖有內(nèi)部不一致(internal inconsistency) 異常。
記住,最終,UITableView 的 dataSource
決定了表視圖應(yīng)該顯示的行數(shù)。 插入新行后,表視圖有六行(原來的五行加上新行)。 當(dāng) UITableView 詢問其 dataSource
的行數(shù)時,ItemsViewController 會咨詢該 store 并返回應(yīng)該有五行。 UITableView 無法解決這種不一致并引發(fā)異常。
您必須確保 UITableView 及其 dataSource
在插入新行之前通過向 ItemStore 中添加 Item
來改變行數(shù)。
在 ItemsViewController.swift 中,更新 addNewItem(_ :)。
@IBAction func addNewItem(_ sender: UIButton) {
??Make a new index path for the 0th section, last row
??let lastRow = tableView.numberOfRows(inSection: 0)
??let indexPath = IndexPath(row: lastRow, section: 0)
??Insert this new row into the table
??tableView.insertRows(at: [indexPath], with: .automatic)
??Create a new item and add it to the store
??let newItem = itemStore.createItem()
??// Figure out where that item is in the array
??if let index = itemStore.allItems.index(of: newItem) {
????let indexPath = IndexPath(row: index, section: 0)
????// Insert this new row into the table
????tableView.insertRows(at: [indexPath], with: .automatic)
??}
}
運(yùn)行應(yīng)用程序。 點擊 Add
按鈕,新行將滑動到表的底部位置。 請記住,視圖對象的作用是將模型對象呈現(xiàn)給用戶; 更新視圖而不更新模型對象沒什么卵用。
現(xiàn)在您可以添加行和 item,您不再需要將五個隨機(jī)項目放入 store 的代碼。
打開 ItemStore.swift
并刪除初始化程序代碼。
init() {
??for _ in 0..<5 {
????createItem()
??}
}
構(gòu)建并運(yùn)行應(yīng)用程序。 首次啟動應(yīng)用程序時,不再有任何行,但您可以通過點擊 Add
按鈕添加一些行。
刪除行
在編輯模式中,帶有減號(如圖11.7所示)的紅色圓圈是刪除控件,然后點擊一個應(yīng)該會刪除該行。 但是,在這一點上,您實際上不能刪除該行。 (嘗試看看。)在表視圖刪除一行之前,它會在其數(shù)據(jù)源上調(diào)用關(guān)于建議刪除的方法,并等待確認(rèn)。
刪除 cell 時,您必須做兩件事情:從 UITableView 中刪除該行,并從 ItemStore 中刪除與之相關(guān)聯(lián)的 Item。 要將其關(guān)閉,ItemStore 必須知道如何從其中刪除對象。
在 ItemStore.swift
中,實現(xiàn)一個新的方法來刪除特定的 item。
func removeItem(_ item: Item) {
??if let index = allItems.index(of: item) {
????allItems.remove(at: index)
??}
}
你將會實現(xiàn) tableView(_:commit:forRow :),一個 UITableViewDataSource 協(xié)議的方法。(這個方法在 ItemsViewController 上調(diào)用,請記住,盡管 ItemStore 是保存數(shù)據(jù)的地方,而 ItemsViewController 才是表視圖的 dataSource。)
當(dāng)在數(shù)據(jù)源上調(diào)用 tableView(_:commit:forRowAt :) 時,會傳遞兩個額外的參數(shù)。 第一個是 UITableViewCellEditingStyle,在這種情況下,它是 .delete
。 另一個參數(shù)是表中行的 IndexPath。
在 ItemsViewController.swift
中,實現(xiàn)此方法使 ItemStore 刪除正確的對象,并通過在表視圖上調(diào)用 deleteRows(at:with :) 方法來確認(rèn)刪除行。
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
??// If the table view is asking to commit a delete command...
??if editingStyle == .delete {
????let item = itemStore.allItems[indexPath.row]
????// Remove the item from the store
????itemStore.removeItem(item)
????// Also remove that row from the table view with an animation
????tableView.deleteRows(at: [indexPath], with: .automatic)
??}
}
構(gòu)建并運(yùn)行應(yīng)用程序,創(chuàng)建一些行,然后刪除一行。 它就會消失。請注意,滑動刪除也可以。
移動行
要更改 UITableView 中的行順序,您將使用 UITableViewDataSource 協(xié)議中的另一個方法——tableView(_:moveRowAt:to :)。
要刪除一行,您必須在 UITableView 上調(diào)用 deleteRows(at :),以確認(rèn)刪除。 但是,移動一行不需要確認(rèn):表視圖會自動移動該行,并通過調(diào)用 tableView(_:moveRowAt:to :) 方法來報告數(shù)據(jù)源發(fā)生了移動。 實現(xiàn)此方法來更新數(shù)據(jù)源以匹配新順序。
但是,在實現(xiàn)此方法之前,您需要給 ItemStore 一個方法來更改其 allItems
數(shù)組中 item 的順序。
在 ItemStore.swift
中,實現(xiàn)這個新方法。
func moveItem(from fromIndex: Int, to toIndex: Int) {
??if fromIndex == toIndex {
????return
??}
??// Get reference to object being moved so you can reinsert it
??let movedItem = allItems[fromIndex]
??// Remove item from array
??allItems.remove(at: fromIndex)
??// Insert item in array at new location
??allItems.insert(movedItem, at: toIndex)
}
在 ItemsViewController.swift
中,實現(xiàn) tableView(_:moveRowAt:to :) 來更新 store。
override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
??// Update the model
??itemStore.moveItem(from: sourceIndexPath.row, to: destinationIndexPath.row)
}
運(yùn)行你的應(yīng)用程序。 添加幾個 item,然后點擊編輯,并查看每一行側(cè)面的新的重新排序控件(三條水平線)。 觸摸并按住重新排序控件并將該行移動到新位置(圖11.8)。
圖11.8移動一行
請注意,簡單地實現(xiàn) tableView(_:moveRowAt:to :) 會導(dǎo)致重新排序控件出現(xiàn)。 UITableView 可以在運(yùn)行時詢問其數(shù)據(jù)源是否實現(xiàn)了 tableView(_:moveRowAt:to :)。 如果是,則表視圖會在表視圖進(jìn)入編輯模式時添加重新排序控件。
顯示用戶警報
在本節(jié)中,您將了解用戶警報以及配置和顯示用戶警報的不同方法。 用戶警報可以為您的應(yīng)用程序提供更好的用戶體驗,因此您會經(jīng)常使用它們。
警報通常用于警告用戶一個重要的動作即將發(fā)生,同時給他們機(jī)會取消該動作。 當(dāng)您要顯示警報時,您將創(chuàng)建一個具有首選樣式的 UIAlertController 實例。 兩種可用的樣式是 UIAlertControllerStyle.actionSheet 和 UIAlertControllerStyle.alert (圖11.9)。
圖11.9 UIAlertController樣式
.actionSheet 樣式用于向用戶呈現(xiàn)要從中選擇的動作列表。 .alert 類型用于顯示關(guān)鍵信息,要求用戶決定如何繼續(xù)。 這個區(qū)別可能看起來很微妙,但是如果用戶決定取消選擇,或者動作不重要的話,那么一個 .actionSheet 可能是最好的選擇。
您將使用 UIAlertController 來確認(rèn) item 的刪除。 您將使用 .actionSheet 樣式,因為警報的目的是確認(rèn)或取消可能的破壞性操作。
打開 ItemsViewController.swift
并修改 tableView(_:commit:forRowAt :),要求用戶確認(rèn)或取消 刪除 item。
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
??// If the table view is asking to commit a delete command...
??if editingStyle == .delete {
????let item = itemStore.allItems[indexPath.row]
????let title = "Delete \(item.name)?"
????let message = "Are you sure you want to delete this item?"
????let ac = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
????// Remove the item from the store
????itemStore.removeItem(item)
????// Also remove that row from the table view with an animation
????tableView.deleteRows(at: [indexPath], with: .automatic)
??}
}
在確定用戶想要刪除項目之后,您將創(chuàng)建一個具有適當(dāng)標(biāo)題和消息的 UIAlertController 實例,描述將要執(zhí)行的操作。 此外,您為該警報指定 .actionSheet 樣式。
顯示警報時用戶可以選擇的動作是 UIAlertAction 的實例,您可以添加多個操作,而不管警報的樣式。 使用 addAction(_ :) 方法將動作添加到 UIAlertController 中。
在 tableView(_:commit:forRowAt :) 中的動作工作表中添加必要的操作。
...
let ac = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
??ac.addAction(cancelAction)
let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: { (action) -> Void in
??// Remove the item from the store
??self.
itemStore.removeItem(item)
??// Also remove that row from the table view with an animation
??self.
tableView.deleteRows(at: [indexPath], with: .automatic)
})
ac.addAction(deleteAction)
...
第一個動作的標(biāo)題為 “Cancel”,并使用 .cancel
樣式創(chuàng)建。.cancel
樣式會顯示為標(biāo)準(zhǔn)藍(lán)色字體。 此操作將允許用戶退出刪除項目。 處理程序參數(shù)允許在發(fā)生該動作時執(zhí)行閉包。 因為不需要其他動作,所以將 nil
作為參數(shù)傳遞。
第二個動作的標(biāo)題為 “Delete”,并使用 .dructructive
樣式創(chuàng)建。 因為破壞性的行為應(yīng)該被明確標(biāo)注和注意到,.dructructive
風(fēng)格會產(chǎn)生明亮的紅色文字。 如果用戶選擇此動作,則需要刪除 item 和表視圖 cell。 這一切都在傳遞給動作的構(gòu)造器的 'handle' 閉包中完成。
現(xiàn)在已經(jīng)添加了動作,可以向用戶顯示警報控制器。 因為 UIAlertController 是 UIViewController 的子類,所以您可以使用 模態(tài)(modally)
將其呈現(xiàn)給用戶。 模態(tài)視圖控制器(modal view controller)
接管整個屏幕,直到其完成工作。
要以視圖方式呈現(xiàn)視圖控制器,您可以在其視圖位于屏幕上的視圖控制器上調(diào)用 present(_:animated:completion :)。 要呈現(xiàn)的視圖控制器被傳遞給它,并且該視圖控制器的視圖接管屏幕。
...
let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: { (action) -> Void in
??// Remove the item from the store
??self.itemStore.removeItem(item)
??// Also remove that row from the table view with an animation
??self.tableView.deleteRows(at: [indexPath], with: .automatic)
})
ac.addAction(deleteAction)
// Present the alert controller
present(ac, animated: true, completion: nil)
...
構(gòu)建并運(yùn)行應(yīng)用程序并刪除一個 item。 將提供一個動作以供您確認(rèn)刪除(圖11.10)。
圖11.10 刪除 item
設(shè)計模式
設(shè)計模式(design pattern)
解決了常見的軟件工程問題。 設(shè)計模式不是代碼的實際代碼段,而是可以在應(yīng)用程序中使用的抽象概念或方法。 良好的設(shè)計模式是任何開發(fā)人員有價值和強(qiáng)大的工具。
在開發(fā)過程中始終如一地使用設(shè)計模式可以減少解決問題的精神開銷,從而可以更輕松,快速地創(chuàng)建復(fù)雜的應(yīng)用程序。 以下是您已經(jīng)使用的一些設(shè)計模式:
-
委托模式(Delegation)
:一個對象將某些功能委托給另一個對象。 當(dāng)文本字段的內(nèi)容更改時,您通過 UITextField 使用委托來通知。 -
數(shù)據(jù)源模式(Data source)
:數(shù)據(jù)源與委托類似,但不是對另一個對象做出反應(yīng),而是在請求時,數(shù)據(jù)源負(fù)責(zé)向另一個對象提供數(shù)據(jù)。 您之前已將數(shù)據(jù)源模式與表視圖一起使用過:每個表視圖都有一個數(shù)據(jù)源,它至少負(fù)責(zé)告訴表視圖要顯示多少行以及每個索引路徑應(yīng)顯示哪個單元格。 -
Model-View-Controller
:應(yīng)用程序中的每個對象都可以滿足三個角色之一。 模型對象是數(shù)據(jù)。 視圖顯示UI。 控制器提供將模型和視圖結(jié)合在一起的膠水。 -
目標(biāo) - 動作對(Target-action pairs)
:當(dāng)特定事件發(fā)生時,一個對象調(diào)用另一個對象的方法。 目標(biāo)是有一個方法被調(diào)用的對象,動作是被調(diào)用的方法。 例如,您使用具有按鈕的目標(biāo)動作對:當(dāng)觸發(fā)事件發(fā)生時,將會在另一個對象(通常是視圖控制器)上調(diào)用一個方法。
蘋果在使用這些設(shè)計模式時非常一致,因此了解和識別它們非常重要。 繼續(xù)閱讀這本書,留意這些模式! 認(rèn)識它們將幫助您更輕松地學(xué)習(xí)新的課程和框架。
青銅挑戰(zhàn):重命名刪除按鈕
刪除行時,會出現(xiàn)一個確認(rèn)按鈕,標(biāo)有 Delete。 嘗試將此按鈕的標(biāo)簽更改為 Remove。
白銀挑戰(zhàn):防止重新排序
表格視圖總是顯示一個最后一行,表示 “No more items!”(這部分挑戰(zhàn)與上一章的挑戰(zhàn)是一樣的,如果你已經(jīng)完成了,你可以復(fù)制你之前的代碼 )現(xiàn)在,令最后一行不能移動。
黃金挑戰(zhàn):真正防止重新排序
完成銀牌挑戰(zhàn)后,您可能會注意到,即使您不能移動 No more items!
行本身,您仍然可以拖動其下的其他行。 做到這一點——無論什么——No more items!
行永遠(yuǎn)不會被淘汰出最后的位置。 最后,使它不可被刪除。