制作table view cell的幾種方法
在AllListsViewController中創(chuàng)建table view cell的方法比在ChecklistViewController中略復雜一些。在后者中你僅僅是通過簡單的一個語句就獲得了一個新的table view cell:
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem",for: indexPath)
但是在AllListsViewController中為了實現(xiàn)同樣的目的我們寫了一大堆代碼:
let cellIdentifier = "Cell"
if let cell =
tableView.dequeueReusableCell(withIdentifier: cellIdentifier) {
return cell
} else {
return UITableViewCell(style: .default,reuseIdentifier: cellIdentifier)
}
這里我們還是調(diào)用了dequeueReusableCell(withIdentifier),只是以前我們在故事模版中放置了cell并且給了它一個身份標示,而這次沒有。
如果這個table view找不到任何可重用的cell,這個方法會返回nil,這時你不得不手動創(chuàng)建cell,這就是else后面跟的代碼的作用。
這實際上是兩種不同類型的dequeueReusableCell(...),其中一個有IndexPath參數(shù)而另一個沒有。在AllListsViewController中我們使用的是沒有IndexPath參數(shù)的這一個。兩者的區(qū)別在于有IndexPath參數(shù)的這一個僅用于標準cell。如果在AllListsViewController中使用有IndexPath參數(shù)的這個方法,app就會崩潰掉。
制作cell有四種方法:
1、使用標準cell。這是最簡單也是最快的一種方法。我們在ChecklistViewController中做的就是。
2、使用靜態(tài)cell。你在Add/Edit界面中使用的就是靜態(tài)cell。靜態(tài)cell最大的優(yōu)勢就是不用給它提供數(shù)據(jù)源方法,適用于你提前知道cell內(nèi)容的情況。
3、使用nib文件。一個nib(也叫做XIB)就像一個迷你的僅僅包含一個自定義的UITableViewCell對象的故事模版。這和使用標準cell非常相似,只是你是在故事模版之外使用它。
4、手動創(chuàng)建,就是我們在AllListsViewController中使用的方法。在早期的iOS版本中,只有這一種方法。這種方法要復雜一些,但是更加靈活。
當你手動創(chuàng)建一個cell時,你需要指定一個確定的cell style,就會得到一個已經(jīng)包含標簽和圖片的預置布局的cell。
在All Lists View Controller中,你使用了“Default”style,在稍后你會將它切換為“Subtitle”,這會在主標簽的下方,給你一個小一點的次級標簽。
使用標準cell style意味著你不需要設計你自己的cell布局。對于大多數(shù)app而言標準cell已經(jīng)足夠用了。
標準cell和靜態(tài)cell都可以使用標準cell style。標準cell和靜態(tài)cell的默認style都是“Custom”,這種style要求你使用自己的標簽,但是你可以通過界面建造器將它改變?yōu)閮?nèi)建的style。
最后,你需要注意的是:有時我看到其他人是這樣寫代碼的,使用代碼為每一行創(chuàng)建一個新的cell而不是試著重用cell。你千萬不要這樣做!一定要首先向table view請求看看是否有可以重用的cell,使用dequeueReusableCell(...)這個方法。
為每一行都創(chuàng)建一個新的cell,會使app變慢,創(chuàng)建一個對象總是比重用一個對象要慢。所以為每一行都創(chuàng)建一個新的cell會占據(jù)大量內(nèi)存,為了用戶著想,你也應該重用cell。
查看待辦事項分類
目前,由AllListsViewController中的lists數(shù)組組成的數(shù)據(jù)模型包含了少量的Checklist對象。數(shù)據(jù)模型中同時還有來自ChecklistViewController的items數(shù)組,其中包含ChecklistItem對象。
你也許已經(jīng)注意到了,當你點擊任何一行時,無論是哪一行,都會展示一模一樣的待辦事項。
而實際上,每個待辦事項分類,都應該對應不同的待辦事項內(nèi)容。我們之后會完成這一工作。
首先,我們來設置好映射被選擇的待辦事項分類的名稱,作為界面的標題。
打開ChecklistViewController.swift,添加一個實例變量:
var checklist: Checklist!
過會我再講為什么這必須是個可選型。
還是在ChecklistViewController.swift中,將viewDidLoad()方法修改為:
override func viewDidLoad() {
super.viewDidLoad()
title = checklist.name
}
這一步的作用是改變界面的標題,就是導航欄的標題,將導航欄的標題修改為Checklist對象的名稱。
當執(zhí)行轉場時,你會將這個checklist對象給到ChecklistViewController。
打開AllListViewController.swift,將tableView(didSelectRowAt)修改為:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let checklist = lists[indexPath.row]
performSegue(withIdentifier: "ShowChecklist", sender: checklist)
}
和以前一樣,你使用performSegue()來執(zhí)行轉場。這個方法之前有一個參數(shù)sender,之前是nil。現(xiàn)在你用來傳遞用戶點擊的那一行的Checklist對象。
你可以在sender參數(shù)中放置任何東西。如果你通過故事模版執(zhí)行轉場(而不是像現(xiàn)在這樣手動轉場),那么sender就會引用被觸發(fā)的空控件,例如用于Add按鈕的UIBarButton對象或者用于列表中某一行的UITableViewCell。
但是因為你是通過手動開始轉場的,所以你可以在sender中放入最方便的對象。
將Checklist對象放入sender參數(shù)時,還不會將這個對象給到ChecklistViewController。這一步發(fā)生在“prepare-for-segue”中,你還沒有在代碼里寫這個方法。
在AllListsViewController.swift中添加以下方法:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowChecklist" {
let controller = segue.destination as! ChecklistViewController
controller.checklist = sender as! Checklist
}
}
你之前應該見到過這個方法。prepare(for:sender:)在轉場執(zhí)行后立即被調(diào)用。你可以在這里,在新的視圖還沒有在屏幕上可視化之前設置新視圖的屬性。
??:轉場的目標是ChecklistVieController,不是UINavigationController,這和之前有點不同。
到Add/edit界面的轉場是一種modally presented(這個真心不知道怎么翻譯)方式,針對與嵌入導航控制器中的視圖控制器。
而這次是“Push”型的轉場,直接轉到Checklist View Controller。
看看故事模版就知道在All Lists界面和Checklist界面之間沒有導航控制器。這個轉場直接從一個視圖轉到另一個。
在prepare(for:sender:)中,你需要將被點擊行的Checklist對象給到ChecklistViewController。這就是為什么之前你將Checklist對象放入sender參數(shù)中的原因。(你也可以將Checklist對象臨時存儲到一個實例變量里,但是把這個對象放入sender參數(shù)中更加簡單)
所有這一切發(fā)生在ChecklistViewController被加載前,ChecklistViewController被實例化時的一瞬間。這就是說它的viewDidLoad()方法在prepare(for:sender:)之后被調(diào)用。
在這一時刻,這個視圖控制器的checklist屬性被來自sender的Checklist對象填充,并且viewDidLoad()可以據(jù)此修改界面的標題。

這一系列過程解釋了為什么checklist屬性被聲明為可選型。因為直到調(diào)用viewload()前,它都是nil。
nil通常不是Swift中允許的變量取值,但是可選型例外。
之前我們聲明可選型時用的是問號,這里是一個感嘆號,感嘆號的作用和問號非常類似,區(qū)別在于用感嘆號時,你不需要用if let去對它進行解包。
使用這種隱式解包可選型時,需要非常小心,因為它們沒有任何保護措施。
運行app,點擊一個待辦事型分類,轉入的屏幕界面的標題會顯示為這個待辦事項分類的名稱。

注意一點,把Checklist對象給到ChecklistViewController并不會形成一個拷貝。
你僅僅是傳遞這個對象的一個引用到視圖控制器,用戶對Checklist對象做出的任何變更,都會體現(xiàn)在AllListsViewController上。
這兩個視圖控制器讀取的都是同一個Checklist對象。過會在Checklist中添加新的ChecklistItem時,這一點會成為你的便利條件。
類型扮演(type cast)
在prepare(for:sender:)中,你寫了這樣的代碼:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
...
controller.checklist = sender as! Checklist
...
}
這里的as!是什么呢?
如果你足夠細心的話,你會注意到“as something”已經(jīng)出現(xiàn)過好幾次了。這就是類型扮演(type cast)
類型扮演通知Swift解釋具有不同數(shù)據(jù)類型的值。這和電影中的某一個演員正好相反,在電影中一個演員只扮演一個角色,而在swift中,類型扮演的實際作用就是改變了對象的角色。
上面的方法中,sender的參數(shù)是Any?,這意味著這個參數(shù)可以是任何類型的對象:一個UIBarButtonItem,一個UITableViewCell,或者一個Checklist對象。感謝這里的問號,使得它甚至可以為nil。
但是controller.checklist總是期待一個合適的Checklist對象,它無法處理其他對象,比如UITableViewController,因此,swift需要你只能把Checklist對象放入checklist屬性中。
通過“sender as! Checklist”,你告訴了Swift它可以安全的將sender作為Checklist對象處理。
另一個類型扮演的例子是:
let controller = segue.destination as! ChecklistViewController
轉場的destination(目的地)屬性引用轉場結束時接受到的視圖控制器,顯然 ,蘋果的工程師無法提前預言這個視圖控制器就是我們命名的ChecklistViewController。
所以你不得不在讀取任何這個對象的屬性前,先將它由通用類型UIViewController扮演為這個app中存在的ChecklistViewController。
在舉一個例子,在loadChecklistItems()中:
items = unarchiver.decodeObjectForKey("ChecklistItems")
as! [ChecklistIt]
NSKeyedUnarchiver將"ChecklistItems"鍵值下凍結的對象解碼到一個數(shù)組中,但是你必須告訴swift這確實是一個包含ChecklistItem對象的數(shù)組。
沒有類型扮演的的話,swfit會認為這是任何類型,這樣就會造成和items數(shù)組的數(shù)據(jù)類型不相容的事情發(fā)生。
還有一種使用as?的類型扮演,這是用于可選型的類型扮演,或者說這個類型扮演可能會為nil。我們會在后面接觸到這種例子。
如果你不太理解這些內(nèi)容也不要擔心,我們會通過大量的例子讓你消化這個內(nèi)容。
你使用類型扮演的最終原因是,iOS架構的通信原理是由Object-C寫成的,swift在類型上的要求比OC要寬松一些,在OC中你需要更加精確的指明類型。
添加和編輯待辦事項分類
讓我們快速完成添加和編輯待辦事項分類功能。這是另一個擁有靜態(tài)cell的UITableViewController。
如果之前的代碼你已經(jīng)了然于心了,那么現(xiàn)在工作對你就是小菜一碟!
在工程導航器中新增一個Cocoa Touch Class模版或者直接新增一個swift文件,取名為ListDetailViewController。
將模版中原有的內(nèi)容都刪掉,替換為下面的語句:
import UIKit
protocol ListDetailViewControllerDelegate: class {
func listDetailViewControllerDidCancel(_ controller: ListDetailViewController)
func listDetailViewController(_ controller: ListDetailViewController,didFinishAdding checklist: Checklist)
func listDetailViewController(_ controller: ListDetailViewController,didFinishEditing checklist: Checklist)
}
class ListDetailViewController: UITableViewController,UITextFieldDelegate {
@IBOutlet weak var textField: UITextField!
@IBOutlet weak var doneBarButton: UIBarButtonItem!
weak var delegate: ListDetailViewControllerDelegate?
var checklistToEdit: Checklist?
}
我僅僅是把ItemDetailViewController.swift中的內(nèi)容拷貝過來改了改名字。同時注意一下,你現(xiàn)在要處理的是Checklist對象,而不是ChecklistItem。
添加一個viewDidLoad()方法:
override func viewDidLoad() {
super.viewDidLoad()
if let checklist = checklistToEdit {
title = "Edit Checklist"
textField.text = checklist.name
doneBarButton.isEnabled = true
}
}
這樣當用戶編輯已經(jīng)存在的待辦事項分類時,可以將界面的標題修改為Edit Checklist,并且將被修改的待辦事項分類的名稱放入text field。
同時也添加一個viewWillAppear()方法,用于自動彈出小鍵盤:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
textField.becomeFirstResponder()
}
然后給Cancel按鈕以及Done按鈕添加動作方法:
@IBAction func cancel() {
delegate?.listDetailViewControllerDidCancel(self)
}
@IBAction func done() {
if let checklist = checklistToEdit {
checklist.name = textField.text!
delegate?.listDetailViewController(self, didFinishEditing: checklist)
} else {
let checklist = Checklist(name: textField.text!)
delegate?.listDetailViewController(self, didFinishAdding: checklist)
}
}
這些代碼對你應該非常熟悉了。這和之前的編輯及添加待辦事項界面幾乎一模一樣。
為了在done()方法中創(chuàng)建新的Checklist對象,你使用了Checklist的init(name)方法,并且將textField.text作為參數(shù)傳入到name中。
你不能像下面這樣去實現(xiàn)這個目的,這樣做是達不到預期效果的:
let checklist = Checklist()
checklist.name = textField.text!
因為Checklist不具備一個沒有任何參數(shù)的init()方法,所以Checklist()會返回一個報錯。它只有一個init(name)方法,所以你每次創(chuàng)建一個新的Checklist對象時,都必須用這個方法進行初始化。
同時確保用戶無法選擇text field所在行的cell:
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
return nil
}
最后添加text field的委托方法,根據(jù)用戶的輸入是否為空來啟用或者禁用Done按鈕。
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let oldText = textField.text! as NSString
let newText = oldText.replacingCharacters(in: range, with: string) as NSString
doneBarButton.isEnabled = (newText.length > 0)
return true
}
這也是你在ItemDetailViewController中做過一次的事。
讓我們在界面建造器中為這個新的視圖控制器制作用戶界面。
打開故事模版,拖拽一個Navigation Controller到畫布中并且將它放置在其他視圖控制器的下面。

界面建造器已經(jīng)假定你要嵌入一個table view controller到導航控制器中,這樣就為你省了不少事。
選定新的table view controller(名字叫做“root view controller”的那個)并且打開身份檢查器。將class中填寫為ListDetailViewController。
將導航欄的標題由“Root View Controller”修改為Add Checklist。(如果雙擊不好使的話,你可以在綱要面板中選定Root View Controller然后在屬性檢查器中進行改名)
添加Cancel和Done按鈕并且將按鈕和動作方法鏈接起來。同時將Done按鈕和doneBarButton鏈接起來,并且取消選定Enable選項。
小貼士:如果你無法將 bar button拖拽到導航欄上,也可以直接往略縮面板里拖。
選中table view,然后在屬性檢查器中設置Static Cells,和style設置為Grouped。然后刪除掉多余的兩個cell。
拖拽一個Text Field到cell中,然后對其進行如下配置:
Border Style: none
Font size: 17
Placeholder text: Name of the List
Adjust to Fit: disabled
Capitalization: Sentences
Return Key: Done
Auto-enable Return key: check
然后將這個Text Field和textField outlet鏈接起來。
然后按住ctrl將Text Field拖拽到視圖控制器上,在彈出窗口中選擇delegate。這樣這個視圖控制器就是text field的委托了。
打開text field的鏈接檢查器,將Did End on Exit拖拽到代表視圖控制器的黃色圓圈圖標上,在彈出窗口中選擇done。
(以上步驟如果不熟悉,可以回頭去看看之前的課程,這些步驟我們都詳細做過一遍)

回到All Lists View Controller(就是叫做Checklists的那個),并且拖拽一個bar button上去,并且將這個button設置為Add。
按住ctrl拖拽這個新的Add按鈕到下面的導航控制器上,并且在彈出窗口選擇Present Modally segue。
選擇這個新的轉場,并且將其命名為AddChecklist。
你的故事模版現(xiàn)在看起來應該是這個樣子:

堅持一下,就快完了。你還需要將AllListsViewController做成ListDetailViewController的委托。我們之前也做過一次類似的事情。
通過在All Lists view controller的class聲明行中添加ListDetailViewControllerDelegate來使得它遵循這一協(xié)議。
打開AllListsViewController.swift:
AllListsViewController: UITableViewController,ListDetailViewControllerDelegate {
還是在AllListsViewController.swift中,擴展一下prepare(for:sender:),
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowChecklist" {
let controller = segue.destination as! ChecklistViewController
controller.checklist = sender as! Checklist
} else if segue.identifier == "AddChecklist" {
let navigationController = segue.destination as! UINavigationController
let controller = navigationController.topViewController as! ListDetailViewController
controller.delegate = self
controller.checklistToEdit = nil
}
}
第一個if中的內(nèi)容不要改動,從else if開始添加新內(nèi)容。
這段代碼的作用和以前一樣,旬齋導航控制器中的視圖控制器,并且設置它的delegate為self。
在AllListsViewController.swift的底部,添加協(xié)議方法:
func listDetailViewControllerDidCancel(_ controller: ListDetailViewController) {
dismiss(animated: true, completion: nil)
}
func listDetailViewController(_ controller: ListDetailViewController, didFinishAdding checklist: Checklist) {
let newRowIndex = lists.count
lists.append(checklist)
let indexPath = IndexPath(row: newRowIndex, section: 0)
let indexPaths = [indexPath]
tableView.insertRows(at: indexPaths, with: .automatic)
dismiss(animated: true, completion: nil)
}
func listDetailViewController(_ controller: ListDetailViewController, didFinishEditing checklist: Checklist) {
if let index = lists.index(of: checklist) {
let indexPath = IndexPath(row: index, section: 0)
if let cell = tableView.cellForRow(at: indexPath) {
cell.textLabel!.text = checklist.name
}
}
dismiss(animated: true, completion: nil)
}
這些方法會在用戶點擊Cancel或者Done按鈕時被調(diào)用。
這些代碼你都應該很熟悉才對,我們之前都有完整的做過一次。
同時添加table view的數(shù)據(jù)源方法來允許用戶刪除某一條記錄:
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
lists.remove(at: indexPath.row)
let indexPaths = [indexPath]
tableView.deleteRows(at: indexPaths, with: .automatic)
}
運行app,現(xiàn)在你可以新增或者刪除待辦事項分類了:

??:如果app崩潰了,那么就檢查一下是不是所有的鏈接都做好了。任何一點細節(jié)的丟失,都會導致app崩潰。
你還無法對已經(jīng)存在的條目進行修改,然我們來完成這最后一點代碼。
之前我們也是通過轉場的方式進入到編輯界面,但是這一次我們不這樣做,我們要通過手動的方式來從故事模版中讀取這個新的視圖控制器,多掌握一些方法總是好的。
打開AllListsViewController.swift,添加一個tableView(accessoryButtonTappedForRowWith)方法。這個方法是table view的委托方法之一,其作用就和名字一樣一目了然。
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
let navigationController = storyboard!.instantiateViewController(withIdentifier: "ListDetailNavgationController") as! UINavigationController
let controller = navigationController.topViewController as! ListDetailViewController
controller.delegate = self
let checklist = lists[indexPath.row]
controller.checklistToEdit = checklist
present(navigationController, animated: true, completion: nil)
}
在這個方法內(nèi),你為Add/Edit Checklist界面創(chuàng)建了新的視圖控制器對象,并且將其展現(xiàn)在屏幕上。這和轉場的作用大致相似。這個視圖控制器被嵌入到故事模版中,并且你請求故事模版對象讀取它。
你是在哪里獲取這個故事模版對象的呢?每個視圖控制器都有一個storyboard屬性來引用這個視圖控制器是從哪個故事模版中被讀取的。你可以使用這個屬性來故事模版的所有功能,比如實例化其他視圖控制器。
這個storyboard屬性是可選型,因為視圖控制器并不全部從故事模版中讀取,但是我們眼下的這個是,所以我們使用感嘆號對其解包。因為我們可以確定在我們這個app中storyboard不會為nil,所以直接用感嘆號強制解包就可以,而不需要用if let的方式。
調(diào)用instantiateViewController(withIdentifier)時用到了一個字符串“ListDetailNavigationController”,這就是請求故事模版創(chuàng)建新視圖控制器的方式,在我們這個例子中,這個新的視圖控制器就是包含ListDetailViewController的導航控制器。
你可以直接實例化ListDetailViewController,但是ListDetailViewController是嵌入在導航控制器內(nèi)部的,如果直接實例化它而不管導航控制器的話,你就無法看到界面標題,以及Done和Cancel按鈕。
打開故事模版,選擇指向List Detail View Controller的導航控制器,然后打開身份檢查器,將Storyboard ID填寫為ListDetailNavigationController:

運行app,點擊某一行上的詳細信息按鈕試試,如果app崩潰了,重新保存一下故事模版再運行一次。
練習:設置List Detail View Controller的identifier為ListDetailNavigationController,而不是導航控制器,然后運行app看看會發(fā)生什么,試著解釋一下為什么會這樣,如果你可以解釋的話,那么證明你已經(jīng)掌握了這些內(nèi)容。
??:你還能跟上我的步伐嗎?
如果你對這一切非常茫然并且想要放棄的話,千萬要打消這個念頭。
學習新的東西本來就是一個枯燥的過程,編程尤其如此。你可以關掉電腦,去睡一覺,過幾天以后再重新打開看看。
說不定就靈關一閃的明白了起來。