iOS Apprentice中文版-從0開始學iOS開發(fā)-第二十一課

制作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)在看起來應該是這個樣子:

全家福,三個導航控制和四個table view controller

堅持一下,就快完了。你還需要將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)容。

??:你還能跟上我的步伐嗎?
如果你對這一切非常茫然并且想要放棄的話,千萬要打消這個念頭。
學習新的東西本來就是一個枯燥的過程,編程尤其如此。你可以關掉電腦,去睡一覺,過幾天以后再重新打開看看。
說不定就靈關一閃的明白了起來。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容