作者:AppCoda,原文鏈接,原文日期:2015-11-16
譯者:pmst;校對(duì):numbbbbb;定稿:numbbbbb
幾乎所有的應(yīng)用程序都有一個(gè)共同的特點(diǎn):允許用戶在多個(gè)視圖控制器之間導(dǎo)航和協(xié)同工作。這些視圖控制器應(yīng)用非常廣泛,例如簡(jiǎn)單顯示某些形式的信息到屏幕上,或從用戶處收集復(fù)雜的輸入數(shù)據(jù)。為了實(shí)現(xiàn)一款應(yīng)用的不同功能,經(jīng)常需要?jiǎng)?chuàng)建新的視圖控制器,且多數(shù)任務(wù)比較艱巨。不過(guò),倘若你利用expandable tableviews(之后統(tǒng)一譯為可展開(kāi)的 tableview ) ,我們就能避免創(chuàng)建新的視圖控制器(以及相關(guān)的界面和 storyboard)。
顧名思義,可展開(kāi)的 tableview “允許”其單元格展開(kāi)和折疊,顯示和隱藏那些始終可見(jiàn)的單元格下的其他單元格。當(dāng)需要收集簡(jiǎn)單數(shù)據(jù)或向用戶顯示請(qǐng)求信息時(shí),創(chuàng)建可展開(kāi)的 tableview 是一個(gè)不錯(cuò)的選擇。通過(guò)這種方式,我們無(wú)需再創(chuàng)建新的視圖控制器,只需給定幾種選項(xiàng)供用戶抉擇(只能選其一)。例如,利用可展開(kāi)的 tableview ,你可以顯示和隱藏用于收集數(shù)據(jù)的表格選項(xiàng),而不再需要其他額外的視圖控制器。
是否應(yīng)該使用可展開(kāi)的 tableview 取決于你所開(kāi)發(fā)的應(yīng)用程序的性質(zhì)。應(yīng)用程序的外觀和體驗(yàn)通常來(lái)說(shuō)不需要考慮,我們可以繼承 UITableViewCell 并自定義單元格的 UI,還可以創(chuàng)建額外的 xib 文件。總之,它僅僅和需求有關(guān)。
本教程中,我將向你展示一種簡(jiǎn)單但實(shí)用的可展開(kāi) tableview 創(chuàng)建方式。注意,實(shí)現(xiàn) tableview 展開(kāi)功能并不是只有本文介紹的這種方法。大部分實(shí)現(xiàn)都要考慮應(yīng)用的具體需求,但我旨在提供一個(gè)相對(duì)通用的可以在大多數(shù)情況下重用的方法。好了,下面我們來(lái)看看本文要實(shí)現(xiàn)什么應(yīng)用。
關(guān)于演示應(yīng)用
我們將看到如何創(chuàng)建并使用一個(gè)可展開(kāi)的 tableview ,我們會(huì)用一個(gè)包含 tableview 的視圖控制器來(lái)實(shí)現(xiàn)整個(gè)應(yīng)用。首先,我們來(lái)制作一個(gè)表單供用戶輸入數(shù)據(jù),該 tableview 包含以下三個(gè)部分:
- 個(gè)人信息( Personal )
- 愛(ài)好( Preferences )
- 工作經(jīng)驗(yàn)( Work Experience )
每個(gè) section 包含一些可展開(kāi)的單元格,用于觸發(fā)顯示或隱藏當(dāng)前 section 中其他單元格。每個(gè) section 的頂級(jí)單元格(用于展開(kāi)和折疊其他單元格)具體描述如下:
“Personal” section 內(nèi)容如下:
- Full name:顯示用戶的全名,當(dāng)點(diǎn)擊展開(kāi)時(shí),顯示兩個(gè)可用的子單元格用于鍵入 first name 以及 last name。
-
Data of birth:顯示用戶的出生日期。當(dāng)展開(kāi)該單元格時(shí),提供一個(gè)日期選擇視圖(
date pickerview)供用戶選擇日期,以及一個(gè)提交按鈕將所選日期顯示到對(duì)應(yīng)的頂級(jí)單元格中。 - Martial status:顯示用戶是已婚還是單身。展開(kāi)時(shí),提供一個(gè)開(kāi)關(guān)控件(switch control)用于設(shè)置用戶婚姻狀態(tài)。
“Preferences” section 內(nèi)容如下:
- Favorite sport:我們的表單還應(yīng)要求用戶選擇最喜歡的運(yùn)動(dòng),選中后顯示在該單元格中。當(dāng)該單元格呈展開(kāi)狀態(tài)時(shí),出現(xiàn)四個(gè)運(yùn)動(dòng)條目可供選擇,當(dāng)其中一個(gè)子條目選中后,單元格自動(dòng)折疊。
- Favorite color:基本和上面一致,這里我們將顯示三個(gè)不同的顏色條目供用戶選擇。
“Work Experience” section 內(nèi)容如下:
- Level:當(dāng)點(diǎn)擊展開(kāi)這個(gè)頂級(jí)單元格時(shí),顯示另外一個(gè)包含滑動(dòng)控件(slider control)的單元格,要求用戶指定一個(gè)大概的工作經(jīng)驗(yàn)水平。值的范圍限定在 [0,10] 之間,以整型數(shù)據(jù)保存。
下面的動(dòng)畫(huà)圖形展示了我們將要實(shí)現(xiàn)的內(nèi)容:

上面的動(dòng)畫(huà)中可以看到 tableview 展開(kāi)時(shí)顯示了各式各樣的單元格。所有這些都能在初始項(xiàng)目中找到,項(xiàng)目中已經(jīng)預(yù)先做好了一些準(zhǔn)備工作。所有自定義單元格均采用 xib 文件設(shè)計(jì),指定它們的 Custom Class 為自定義 CustomCell 類,繼承自 UITableViewCell:

項(xiàng)目中你可以找到以下單元格的 xib 文件:

它們的文件名已經(jīng)表明了每一個(gè)單元格的用途,你也可以對(duì)它們做深入探究。
除了單元格之外,你還可以找到一些已經(jīng)實(shí)現(xiàn)的代碼。盡管它們非常重要,完成了演示應(yīng)用程序的功能,但是那些代碼并不包含本教程的核心部分,所以我選擇直接跳過(guò),只是提供實(shí)現(xiàn)代碼。教程中我們感興趣的代碼將隨著章節(jié)學(xué)習(xí)逐步添加進(jìn)來(lái)。
好了,現(xiàn)在你已經(jīng)知道我們的最終目標(biāo)是什么了,是時(shí)候去創(chuàng)建一個(gè)可展開(kāi)的 tableview 了。
描述單元格
本教程中,我向你展示的所有有關(guān)可展開(kāi) tableview 的實(shí)現(xiàn)和技術(shù)都遵循一個(gè)單一和簡(jiǎn)單的思想:描述應(yīng)用中每個(gè)單元格的細(xì)節(jié)。通過(guò)這種方式,你就可以知曉哪些單元格是可展開(kāi)的、哪些是可見(jiàn)的、每一個(gè)單元格中的標(biāo)簽值是什么等等。確切來(lái)說(shuō),整體思想如下:為每一個(gè)單元格分配一組描述信息、描述屬性或特定的值,接著向應(yīng)用提供這些描述來(lái)正確顯示每一個(gè)單元格。
對(duì)于這個(gè)演示應(yīng)用程序,我創(chuàng)建和使用的所有屬性都顯示在下面列表中。注意,你可以新增屬性,也可以修改現(xiàn)有項(xiàng)。不管怎樣,最重要的是你能統(tǒng)籌全局,這樣你才能夠執(zhí)行所有你需要的改動(dòng)。屬性列表如下:
- isExpandable:這是一個(gè)布爾類型值,表明單元格是否允許被展開(kāi)。它在本教程中是一個(gè)相當(dāng)重要的屬性值。
- isExpanded:依舊是一個(gè)布爾類型值,指示一個(gè)可展開(kāi)的單元格的當(dāng)前狀態(tài)(展開(kāi)或折疊)。頂級(jí)單元格默認(rèn)是折疊的,因此所有頂級(jí)單元格的初始值均將設(shè)置為
NO。 - isVisible:顧名思義,指示單元格是否可見(jiàn)。它將在之后起到舉足輕重的作用,我們將根據(jù)該屬性在 tableview 中顯示合適的單元格。
- value:這個(gè)屬性對(duì)于保存 UI 控件的值(例如婚姻狀況中的
switch控件的狀態(tài)值)相當(dāng)有用。不是所有的單元格都有這樣的控件,所以它們中的絕大部分的 value 屬性值為空。 - primaryTitle:用于顯示單元格主標(biāo)題標(biāo)簽(main title label)中的文本內(nèi)容,還包含一些應(yīng)該顯示在單元格中的實(shí)際值。
- secondaryTitle:用于顯示單元格子標(biāo)題標(biāo)簽(subtitle lable)或二級(jí)標(biāo)簽的文本內(nèi)容,
- cellIdentifier:自定義單元格的標(biāo)識(shí)符所匹配的當(dāng)前描述。通過(guò)使用 cellIdentifier,應(yīng)用程序不僅能夠出列合適的單元格(tableview 中的 dequeue 方法),而且可以根據(jù)顯示的單元格來(lái)確定應(yīng)該執(zhí)行的 action ,以及指定每個(gè)單元格的高度。
- additionalRows:它包含的附加行總數(shù),即那些當(dāng)單元格展開(kāi)式需要顯示的額外行數(shù)。
我們將使用上文介紹的屬性集合來(lái)描述 tableview 中的每一個(gè)單元格。在應(yīng)用層面我們只需一個(gè)屬性列表(plist)文件即可實(shí)現(xiàn),簡(jiǎn)單易用。在 plist 文件中,我們將為所有單元格正確地填充上述屬性的值,這樣從應(yīng)用角度來(lái)說(shuō),我們最終只要一份完整的技術(shù)描述,無(wú)需編寫(xiě)一行代碼。這是不是灰常棒呢?
通常來(lái)說(shuō),我們會(huì)在項(xiàng)目中創(chuàng)建一個(gè)新的屬性列表文件,接著開(kāi)始往里面填充適當(dāng)?shù)臄?shù)據(jù)。但這里無(wú)需自己動(dòng)手,我已經(jīng)為你提供了.plist文件。所以,你只需下載并將它添加到啟動(dòng)項(xiàng)目即可。為所有單元格設(shè)置屬性非常麻煩并且毫無(wú)意義,那些填充缺省值的復(fù)制粘貼行為只可能會(huì)讓你感覺(jué)疲勞和枯燥。不過(guò),我們還是需要介紹一下 plist 文件內(nèi)容:
首先,你下載的文件名應(yīng)該為 CellDescriptor.plist(希望沒(méi)有錯(cuò))?;A(chǔ)結(jié)構(gòu)(請(qǐng)見(jiàn)下圖中的 Root 鍵名)是一個(gè)數(shù)組,其中每個(gè)條目項(xiàng)分別對(duì)應(yīng) tableview 中所呈現(xiàn)的 section。這意味著 plist 文件包含三個(gè)條目項(xiàng),和 tableview 中顯示的 section 數(shù)目保持一致。
每個(gè) section 中包含的條目項(xiàng)同樣是一個(gè)數(shù)組(類型為字典),分別用于描述當(dāng)前 section 中的每一個(gè)單元格。實(shí)際上,我們采用字典形式對(duì)上述屬性進(jìn)行分組,每一個(gè)字典匹配一個(gè)單獨(dú)的單元格描述。下面是屬性列表文件的一個(gè)示例:

現(xiàn)在是最佳時(shí)機(jī),抽點(diǎn)時(shí)間出來(lái),透徹地理解下所有我們將要顯示到 tableview 中的單元格描述屬性以及相關(guān)值。顯然,通過(guò)使用單元格描述,能夠幫助我們明顯減少創(chuàng)建和管理可展開(kāi)單元格的代碼,此外我們無(wú)需告知應(yīng)用關(guān)于這些單元格的狀態(tài)(例如,哪些單元格是可擴(kuò)展的,它是否允許特定單元格進(jìn)行展開(kāi),在代碼中確定單元格是否可見(jiàn)等等這些問(wèn)題)。所有這些信息已經(jīng)存儲(chǔ)在你剛剛下載的屬性列表文件之中。
加載單元格描述
終于可以開(kāi)始編寫(xiě)代碼了,盡管我們描述單元格的方式(即 plist 文件)節(jié)省了大量時(shí)間,但依舊需要向項(xiàng)目中添加代碼?,F(xiàn)在單元格的描述屬性列表文件已經(jīng)處于項(xiàng)目之中,我們首先要做的就是以編程方式把它的內(nèi)容加載到一個(gè)數(shù)組中。這個(gè)數(shù)組將在下一小節(jié)作為 tableview 的數(shù)據(jù)源(datasource)。
首先,請(qǐng)打開(kāi)項(xiàng)目中 ViewController.swift 文件,在類頂部聲明如下屬性:
var cellDescriptors: NSMutableArray!
該數(shù)組將包含所有單元格字典類型的描述,從屬性列表文件加載得到。
接著,讓我們實(shí)現(xiàn)一個(gè)自定義函數(shù),用于實(shí)現(xiàn)加載文件內(nèi)容到數(shù)組中。我們?yōu)樵摵瘮?shù)命名為 loadCellDescription():
func loadCellDescriptors() {
if let path = NSBundle.mainBundle().pathForResource("CellDescriptor", ofType: "plist") {
cellDescriptors = NSMutableArray(contentsOfFile: path)
}
}
我們這里的實(shí)現(xiàn)方法相當(dāng)簡(jiǎn)單:首先我們確保屬性列表文件在 bundle 中的路徑是有效的,接著我們加載文件內(nèi)容并初始化 cellDescriptors 數(shù)組。
下一步我們將調(diào)用上述方法,在視圖將要顯示之前、tableview 配置之后調(diào)用函數(shù)(我們希望先對(duì) tableview 進(jìn)行配置,然后在它上面顯示數(shù)據(jù))。
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
// 先配置tableview
configureTableView()
// 后加載數(shù)據(jù)
loadCellDescriptors()
}
如果你在上面代碼最后一行鍵入print(cellDescriptors)命令,運(yùn)行應(yīng)用,你將看到命令控制臺(tái)處打印了 plist 文件的所有內(nèi)容。這意味著它們已經(jīng)成功被加載到內(nèi)存中了。

按照慣例,我們本節(jié)的任務(wù)應(yīng)該到此結(jié)束,但恰恰相反;我們將繼續(xù)下去,接下來(lái)的部分至關(guān)重要。到目前為止,你已經(jīng)發(fā)現(xiàn)(特別是打印 CellDescriptor.plist 文件內(nèi)容之后),當(dāng)應(yīng)用程序啟動(dòng)之后并不是所有單元格都是可見(jiàn)的(譯者注: plist 文件中單元格的 Visible 屬性,有些為 YES,有些則為 NO)。實(shí)際上,我們不能知曉它們究竟是否將同時(shí)可見(jiàn),因?yàn)橹挥挟?dāng)每次用戶要求時(shí),它們才進(jìn)行展開(kāi)或折疊。
從編程角度來(lái)說(shuō),這意味著每個(gè)單元格的行索引值(row index)不允許為常量(一般我們處理單元格時(shí),都喜歡使用IndexPath.row這種編程方式),所以我們不能通過(guò)單元格行號(hào)遍歷數(shù)據(jù)源數(shù)組(cellDescriptors)并顯示單元格。解決方式如下:僅提供可見(jiàn)的單元格行索引值。任何嘗試顯示描述中標(biāo)記為不可見(jiàn)的單元格都會(huì)出錯(cuò),當(dāng)然還會(huì)導(dǎo)致其他異常應(yīng)用行為。
所以,為此我們將要實(shí)現(xiàn)一個(gè)新函數(shù)getIndicesOfVisibleRows()。它的名字已經(jīng)說(shuō)明了它的作用: 它僅獲取那些已經(jīng)標(biāo)記為可見(jiàn)的單元格。在我們繼續(xù)執(zhí)行之前,請(qǐng)?jiān)俅位氐筋惖捻敳?,新增如下聲明?/p>
var visibleRowsPerSection = [[Int]]()
該二維數(shù)組將用于存儲(chǔ)每個(gè) section 中可見(jiàn)的單元格行索引值(一維用作 section,另一維用作 rows)。
現(xiàn)在,讓我們來(lái)看新函數(shù)的實(shí)現(xiàn)。你可能已經(jīng)猜到,我們將檢查所有單元格的描述信息,接著將那些“isVisible”屬性值為YES的單元格索引值添加到二維數(shù)組中。很顯然,我們不得不通過(guò)一個(gè)嵌套循環(huán)來(lái)處理,但是它用起來(lái)不難。這里是函數(shù)實(shí)現(xiàn):
func getIndicesOfVisibleRows() {
visibleRowsPerSection.removeAll()
// 遍歷單元格描述數(shù)組
for currentSectionCells in cellDescriptors {
// 暫存每個(gè) section 中,isVisible = true 的行號(hào)
var visibleRows = [Int]()
for row in 0...((currentSectionCells as! [[String: AnyObject]]).count - 1) {
// 檢查每個(gè)單元格的isVisible屬性是否為true
if currentSectionCells[row]["isVisible"] as! Bool == true {
visibleRows.append(row)
}
}
// 將所有標(biāo)記為可見(jiàn)的單元格行號(hào)保存到該數(shù)組中
// 首次加載描述文件后 該數(shù)組值為 [[0, 3, 5], [0, 5], [0]]
visibleRowsPerSection.append(visibleRows)
}
}
請(qǐng)注意,函數(shù)一開(kāi)始需要清空visibleRowsPerSection數(shù)組中之前的所有內(nèi)容,否則后續(xù)調(diào)用該函數(shù)我們將最終得到錯(cuò)誤的數(shù)據(jù)。除此之外,實(shí)現(xiàn)方式非常簡(jiǎn)單,所以我不會(huì)過(guò)多介紹細(xì)節(jié)。
首次調(diào)用上述函數(shù)位置應(yīng)該在從文件加載單元格描述信息操作之后(我們將在之后再次調(diào)用它)。因此,重新審視我們?cè)谶@一部分中實(shí)現(xiàn)的第一個(gè)函數(shù),我們修改如下:
func loadCellDescriptors() {
if let path = NSBundle.mainBundle().pathForResource("CellDescriptor", ofType: "plist") {
cellDescriptors = NSMutableArray(contentsOfFile: path)
getIndicesOfVisibleRows()
tblExpandable.reloadData()
}
}
盡管 tableview 目前還不能正常使用(要知道還未實(shí)現(xiàn) Datasource 方法?。?,但我們提前調(diào)用reloadData()進(jìn)行 tableview 重載,確保應(yīng)用程序啟動(dòng)后,能夠正確顯示單元格內(nèi)容。
顯示單元格
別忘了每一次應(yīng)用程序啟動(dòng)時(shí)都要加載單元格描述,下面我們準(zhǔn)備處理和顯示這些單元格。本小節(jié)中,我們首先創(chuàng)建另一個(gè)新函數(shù),在 cellDescriptors 數(shù)組中查找并返回適當(dāng)?shù)膯卧衩枋鲂畔ⅰH缒慵磳⒃谙旅娲a片段中看到的一樣,從 visibleRowsPerSection 數(shù)組中獲取數(shù)據(jù)(即可見(jiàn)行的索引值)是新函數(shù)工作的先決條件。
func getCellDescriptorForIndexPath(indexPath: NSIndexPath) -> [String: AnyObject] {
// 步驟一:
let indexOfVisibleRow = visibleRowsPerSection[indexPath.section][indexPath.row]
// 步驟二:
let cellDescriptor = cellDescriptors[indexPath.section][indexOfVisibleRow] as! [String: AnyObject]
return cellDescriptor
}
上述函數(shù)接受某個(gè)單元格的路徑索引值(NSIndexPath),且該單元格此刻是 tableview 的處理項(xiàng);函數(shù)返回值為一個(gè)字典,包含匹配單元格的所有屬性。函數(shù)內(nèi)部實(shí)現(xiàn)的首要任務(wù)在給定路徑索引值(即 index path)的條件下,找到匹配的可見(jiàn)行的索引值,這一步很簡(jiǎn)單,只需要傳入每個(gè)單元格的 section 和 row 即可(請(qǐng)見(jiàn)步驟一)。到目前為止,我們還未接觸到 tableview 的代理方法,對(duì)上述內(nèi)容也一知半解,但是我可以提前給你打個(gè)“預(yù)防針”:每個(gè) section 的 row 總數(shù)將與每個(gè) section 中的可見(jiàn)單元格數(shù)目保持一致。這意味著,上述實(shí)現(xiàn)中任意一個(gè) indexPath.row 值(譯者注:section是固定的),在 visibleRowsPerSection 數(shù)組中都能找到一個(gè)可見(jiàn)單元格的索引值與之匹配。
通過(guò)每個(gè)單元格的行索引值,我們可以從 cellDescriptors 數(shù)組中“提取”到單元格描述信息(字典類型)。請(qǐng)注意提取過(guò)程中,數(shù)組的第二個(gè)維度值為 indexOfVisibleRow,而不是 indexPath.row。倘若使用第二個(gè)將導(dǎo)致返回錯(cuò)誤數(shù)據(jù)。
我們?cè)俅螛?gòu)建了一個(gè)非常有用的函數(shù),事實(shí)證明在之后的開(kāi)發(fā)中非常好用?,F(xiàn)在我們開(kāi)始實(shí)現(xiàn) viewController 類中的已存在的 tableview 方法。首先,我們需要指定 tableview 的 section 數(shù)量。
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if cellDescriptors != nil {
return cellDescriptors.count
}
else {
return 0
}
}
你要知道我們不能忽視 cellDescriptors 數(shù)組為nil的情況。當(dāng)數(shù)組已經(jīng)初始化完畢且填充了單元格描述信息,我們返回?cái)?shù)組的元素個(gè)數(shù)。
接著,我們指定每個(gè) section 的行數(shù)。正如我之前所說(shuō)的,行數(shù)和可見(jiàn)單元格數(shù)量保持一致,所以我們可以僅用一行代碼返回該信息。
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return visibleRowsPerSection[section].count
}
之后,確定 tableview 中每個(gè) section 的標(biāo)題:
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
return "Personal"
case 1:
return "Preferences"
default:
return "Work Experience"
}
}
接著,是時(shí)候指定每一行的高度了:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let currentCellDescriptor = getCellDescriptorForIndexPath(indexPath)
switch currentCellDescriptor["cellIdentifier"] as! String {
case "idCellNormal":
return 60.0
case "idCellDatePicker":
return 270.0
default:
return 44.0
}
}
這里我需要強(qiáng)調(diào)一些東西:這部分中我們首次調(diào)用早前實(shí)現(xiàn)的 getCellDescriptorForIndexPath:函數(shù)。我們需要獲得正確的單元格描述信息,緊接著有必要取得“cellIdentifier”屬性,只有依靠它的值才能指定行高。你可以在每個(gè) xib 文件中檢查每種類型的單元格行高(就是如下所示的行高)。
最后是顯示實(shí)際的單元格。起初,每個(gè)單元格必須被 dequeued:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let currentCellDescriptor = getCellDescriptorForIndexPath(indexPath)
// 每個(gè)單元格都是通過(guò)出列得到
let cell = tableView.dequeueReusableCellWithIdentifier(currentCellDescriptor["cellIdentifier"] as! String, forIndexPath: indexPath) as! CustomCell
return cell
}
再次,我們傳入當(dāng)前路徑索引值獲得正確的單元格描述。通過(guò)使用"cellIdentifier"屬性出列一個(gè)正確的單元格,這樣我們能夠?qū)γ總€(gè)單元格的特殊處理作進(jìn)一步的深入探討(譯者注:說(shuō)白了就是根據(jù) cellIdentifier 標(biāo)識(shí)符對(duì)單元格做分支處理)。
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let currentCellDescriptor = getCellDescriptorForIndexPath(indexPath)
let cell = tableView.dequeueReusableCellWithIdentifier(currentCellDescriptor["cellIdentifier"] as! String, forIndexPath: indexPath) as! CustomCell
if currentCellDescriptor["cellIdentifier"] as! String == "idCellNormal" {
if let primaryTitle = currentCellDescriptor["primaryTitle"] {
cell.textLabel?.text = primaryTitle as? String
}
if let secondaryTitle = currentCellDescriptor["secondaryTitle"] {
cell.detailTextLabel?.text = secondaryTitle as? String
}
}
else if currentCellDescriptor["cellIdentifier"] as! String == "idCellTextfield" {
cell.textField.placeholder = currentCellDescriptor["primaryTitle"] as? String
}
else if currentCellDescriptor["cellIdentifier"] as! String == "idCellSwitch" {
cell.lblSwitchLabel.text = currentCellDescriptor["primaryTitle"] as? String
let value = currentCellDescriptor["value"] as? String
cell.swMaritalStatus.on = (value == "true") ? true : false
}
else if currentCellDescriptor["cellIdentifier"] as! String == "idCellValuePicker" {
cell.textLabel?.text = currentCellDescriptor["primaryTitle"] as? String
}
else if currentCellDescriptor["cellIdentifier"] as! String == "idCellSlider" {
let value = currentCellDescriptor["value"] as! String
cell.slExperienceLevel.value = (value as NSString).floatValue
}
return cell
}
對(duì)于普通的單元格來(lái)說(shuō),我們僅需要設(shè)置 textLabel 標(biāo)簽的文本值為 primaryTitle,以及設(shè)置 detailTextLabel 標(biāo)簽的文本值為 secondaryTitle即可。在我們的演示應(yīng)用中,使用 idCellNormal 標(biāo)示符的單元格實(shí)際上就是頂級(jí)單元格( top-level cells),點(diǎn)擊可展開(kāi)和折疊內(nèi)容。
對(duì)于那些包含 textfiled 的單元格,我們僅需將它的占位符值(placeholder value)設(shè)置為單元格描述信息中的 primaryTitle 即可。
對(duì)于那些包含 switch 控件的單元格,我們需要做兩件事:首先指定 switch 控件前面的顯示文本內(nèi)容(示例中是常量,你可以通過(guò)修改 CellDescriptor.plist 文件改變它),其次我們需要為 switch 控件設(shè)置合適的狀態(tài),根據(jù)描述信息來(lái)決定“on”還是“off”。注意之后我們將有可能改變?cè)撝怠?/p>
這里還有一些標(biāo)識(shí)符為“idCellValuePicker”的單元格,這些單元格旨在提供一個(gè)選項(xiàng)列表。當(dāng)點(diǎn)擊選中某個(gè)選項(xiàng)時(shí),父單元格會(huì)自動(dòng)折疊當(dāng)前內(nèi)容。此時(shí)父單元格的文本標(biāo)簽值設(shè)置為選中值。
最后,有單元格包含了 slider 控件。這里我們從 currentCellDescriptor 字典中獲取到當(dāng)前值,將其轉(zhuǎn)換為 float 類型的數(shù)字,再賦值給 slider 控件,這樣它在可視情況下總能呈現(xiàn)正確的值。稍后我們會(huì)改變這個(gè)值,以及更新相應(yīng)的單元格描述。
而那些沒(méi)有添加上述幾種情況標(biāo)識(shí)符的單元格,在本演示應(yīng)用中不會(huì)起任何作用。但是,倘若你想以不同的方式處理它們,可以隨意修改代碼并添加任何缺失的部分。
現(xiàn)在你可以運(yùn)行應(yīng)用,看看目前的成果。期望不要過(guò)高,因?yàn)槟銉H僅看到的只是頂級(jí)單元格內(nèi)容。別忘了我們還未啟用展開(kāi)功能,所以當(dāng)你點(diǎn)擊它們時(shí)什么都不會(huì)出現(xiàn)。然而,不要?dú)怵H,正如你所看到的,到目前為止我們一切進(jìn)展順利。
展開(kāi)和折疊
我猜想本節(jié)內(nèi)容你可能期盼已久了,畢竟這是本教程實(shí)際目的所在。下面我們將通過(guò)每次點(diǎn)擊頂級(jí)單元格控制展開(kāi)和折疊,以及按要求顯示或隱藏正確的子單元格。
首先,我們需要知道點(diǎn)擊行的索引值(記住,不是實(shí)際的 indexPath.row,而是可見(jiàn)單元格中的行索引值),我們會(huì)首先將它分配給一個(gè)局部變量,如下 tableview 代理方法中所示:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
}
雖然實(shí)現(xiàn)單元格展開(kāi)和折疊的代碼量不大,但是我們還是會(huì)逐步深入,這樣你能理解每個(gè)步驟的作用?,F(xiàn)在我們獲取到了點(diǎn)擊行的實(shí)際索引值,我們必須檢查 cellDescriptors 數(shù)組中該單元格是否允許展開(kāi)。如果它允許展開(kāi),且當(dāng)前處于折疊狀態(tài)時(shí),我們將指示(我們將使用一個(gè) flag 標(biāo)志位)這個(gè)單元格必須展開(kāi),反之這個(gè)單元格必須折疊:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpandable"] as! Bool == true {
var shouldExpandAndShowSubRows = false
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpanded"] as! Bool == false {
// In this case the cell should expand.
shouldExpandAndShowSubRows = true
}
}
}
一旦上面的 flag 標(biāo)志位設(shè)置為相應(yīng)值,指示當(dāng)前單元格的展開(kāi)狀態(tài),這時(shí)候我們有責(zé)任將標(biāo)志位值保存到單元格描述集合中,即更新 cellDescriptors 數(shù)組。我們要為選中的單元格更新 “isExpanded” 屬性
,這樣在隨后的點(diǎn)擊中它都能正常運(yùn)行(當(dāng)它處于展開(kāi)時(shí)點(diǎn)擊折疊,當(dāng)折疊時(shí)點(diǎn)擊展開(kāi))。
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpandable"] as! Bool == true {
var shouldExpandAndShowSubRows = false
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpanded"] as! Bool == false {
shouldExpandAndShowSubRows = true
}
cellDescriptors[indexPath.section][indexOfTappedRow].setValue(shouldExpandAndShowSubRows, forKey: "isExpanded")
}
}
此刻,這里還有一個(gè)相當(dāng)重要的細(xì)節(jié)不容我們忽視:如果你還記得,前文中指定了一個(gè)名為“isVisible”的屬性表明單元格的顯示狀態(tài),就存在于單元格的描述中。該屬性必須隨著上文 flag 值改變而改變,所以當(dāng)單元格展開(kāi)時(shí),顯示其他附加的不可見(jiàn)行,反之當(dāng)單元格折疊時(shí),隱藏那些附加行。實(shí)際上,通過(guò)更改該屬性的值我們實(shí)現(xiàn)了單元格展開(kāi)和折疊的效果。所以一旦點(diǎn)擊了頂級(jí)單元格,需要立即更新附加單元格的信息,以下是修改后的代碼片段:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpandable"] as! Bool == true {
var shouldExpandAndShowSubRows = false
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpanded"] as! Bool == false {
shouldExpandAndShowSubRows = true
}
cellDescriptors[indexPath.section][indexOfTappedRow].setValue(shouldExpandAndShowSubRows, forKey: "isExpanded")
for i in (indexOfTappedRow + 1)...(indexOfTappedRow + (cellDescriptors[indexPath.section][indexOfTappedRow]["additionalRows"] as! Int)) {
cellDescriptors[indexPath.section][i].setValue(shouldExpandAndShowSubRows, forKey: "isVisible")
}
}
}
我們距離追尋已久的功能實(shí)現(xiàn)僅一步之遙,但是我們首先必須關(guān)注一個(gè)更重要的事情:在上面代碼片段中,我們僅改變了一些單元格的“isVisible”屬性值,這意味著所有可見(jiàn)行的總數(shù)也隨之改變了。所以,在我們重載 tableview 之前,我們必須重新向應(yīng)用詢問(wèn)可見(jiàn)行的索引值:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpandable"] as! Bool == true {
var shouldExpandAndShowSubRows = false
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpanded"] as! Bool == false {
shouldExpandAndShowSubRows = true
}
cellDescriptors[indexPath.section][indexOfTappedRow].setValue(shouldExpandAndShowSubRows, forKey: "isExpanded")
for i in (indexOfTappedRow + 1)...(indexOfTappedRow + (cellDescriptors[indexPath.section][indexOfTappedRow]["additionalRows"] as! Int)) {
cellDescriptors[indexPath.section][i].setValue(shouldExpandAndShowSubRows, forKey: "isVisible")
}
}
getIndicesOfVisibleRows()
tblExpandable.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
}
正如你看見(jiàn)的那樣,我僅對(duì)屬于點(diǎn)擊單元格的 section 部分進(jìn)行動(dòng)畫(huà)重載,倘若你不喜歡這種方式的話,可以自己來(lái)實(shí)現(xiàn)。
現(xiàn)在快啟動(dòng)應(yīng)用試試。點(diǎn)擊頂級(jí)單元格進(jìn)行展開(kāi)和折疊,和子單元格互動(dòng)下,盡管啥都不會(huì)發(fā)生,但是結(jié)果看起來(lái)相當(dāng)棒!

取值
從現(xiàn)在開(kāi)始,我們將把注意力完全集中在處理數(shù)據(jù)輸入以及用戶與子單元格內(nèi)的控件的交互上。首先我們將為那些標(biāo)識(shí)符為 “idCellValuePicker” 的單元格實(shí)現(xiàn)邏輯事務(wù),處理點(diǎn)擊事件。在我們的演示應(yīng)用中,這些單元格都屬于 tableview 中的 “Preferences” 部分,羅列最喜歡的運(yùn)動(dòng)和顏色選項(xiàng)內(nèi)容。即使早前已經(jīng)提及過(guò),但是我覺(jué)得還是有必要重新讓你回憶下,再次重申:當(dāng)你點(diǎn)擊選擇某個(gè)選項(xiàng)后,相應(yīng)的頂級(jí)單元格應(yīng)該隨之折疊(隱藏那些選項(xiàng)),并將選中的值顯示到頂級(jí)單元格中。
。
我之所以選擇處理這種類型的單元格為先,原因在于我可以繼續(xù)在上部分的 tableview 代理方法中進(jìn)行工作。方法中,我們將添加一個(gè) else 分支處理 non-expandable 單元格的情況,接著檢查點(diǎn)擊單元格的標(biāo)識(shí)符。如果標(biāo)識(shí)符為“idCellValuePicker”,這就是我們感興趣的單元格。
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpandable"] as! Bool == true {
...
}
else {
if cellDescriptors[indexPath.section][indexOfTappedRow]["cellIdentifier"] as! String == "idCellValuePicker" {
}
}
getIndicesOfVisibleRows()
tblExpandable.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
}
在 if 分支內(nèi),我們將執(zhí)行四種不同的任務(wù):
- 首先,我們需要找到頂級(jí)單元格的行索引值,即你點(diǎn)擊選中的單元格的“父母”。事實(shí)上,我們采用自下而上(即從點(diǎn)擊選中的單元格開(kāi)始向上遍歷)的方式對(duì)單元格描述數(shù)組執(zhí)行一次搜索,首個(gè)屬性
isExpandable = true的單元格就是我們想要的家伙。 - 接著,將頂級(jí)單元格中的 textLabel 標(biāo)簽值設(shè)置為選中單元格的值。
- 然后,設(shè)置頂級(jí)單元格的 isExpanded 等于 false ,即折疊狀態(tài)。
- 最后,標(biāo)記頂級(jí)單元格下的所有子單元格為不可見(jiàn)狀態(tài)。
現(xiàn)在代碼如下:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
if cellDescriptors[indexPath.section][indexOfTappedRow]["isExpandable"] as! Bool == true {
...
}
else {
if cellDescriptors[indexPath.section][indexOfTappedRow]["cellIdentifier"] as! String == "idCellValuePicker" {
var indexOfParentCell: Int!
// 任務(wù)一
for var i=indexOfTappedRow - 1; i>=0; --i {
if cellDescriptors[indexPath.section][i]["isExpandable"] as! Bool == true {
indexOfParentCell = i
break
}
}
// 任務(wù)二
cellDescriptors[indexPath.section][indexOfParentCell].setValue((tblExpandable.cellForRowAtIndexPath(indexPath) as! CustomCell).textLabel?.text, forKey: "primaryTitle")
// 任務(wù)三
cellDescriptors[indexPath.section][indexOfParentCell].setValue(false, forKey: "isExpanded")
// 任務(wù)四
for i in (indexOfParentCell + 1)...(indexOfParentCell + (cellDescriptors[indexPath.section][indexOfParentCell]["additionalRows"] as! Int)) {
cellDescriptors[indexPath.section][i].setValue(false, forKey: "isVisible")
}
}
}
getIndicesOfVisibleRows()
tblExpandable.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
}
我們?cè)俅涡薷牧藛卧裰械摹癷sVisible”屬性,所有可見(jiàn)行的數(shù)量也隨之改變。顯然調(diào)用上述代碼中的最后兩個(gè)函數(shù)是非常有必要的。
現(xiàn)在如果你運(yùn)行應(yīng)用,實(shí)現(xiàn)效果如下:
Responding to Other User Actions(求翻譯)
打開(kāi) CustomCell.swift 文件,找到 CustomCellDelegate 的協(xié)議聲明,其中定義了一系列需要的協(xié)議方法。通過(guò)在 ViewController 類中實(shí)現(xiàn)它們,我們將設(shè)法使應(yīng)用程序響應(yīng)所有缺省的用戶操作。
讓我們?cè)俅位氐?ViewController.swift 文件,首先我們需要遵循該協(xié)議。定位到類的頭部聲明行,添加如下內(nèi)容:
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellDelegate
接著,在 tableView:cellForRowAtIndexPath: 函數(shù)中,我們必須將每個(gè)自定義單元格的代理設(shè)置為 ViewController 類(即 self)。定位到那里,就在return cell 的上方添加一行代碼:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
...
// 設(shè)置代理
cell.delegate = self
return cell
}
干得不錯(cuò),現(xiàn)在我們開(kāi)始實(shí)現(xiàn)代理方法。首先,我們將 date picker 控件中選中的日期顯示到相應(yīng)頂級(jí)單元格中:
func dateWasSelected(selectedDateString: String) {
let dateCellSection = 0
let dateCellRow = 3
cellDescriptors[dateCellSection][dateCellRow].setValue(selectedDateString, forKey: "primaryTitle")
tblExpandable.reloadData()
}
一旦我們指定了正確的 section 和 row, 直接賦值字符串類型的日期值。注意該字符串是代委托方法中的一個(gè)參數(shù)。
接著,我們處理有關(guān) switch 控件的事務(wù)。當(dāng) switch 控件值改變時(shí),我們需要做兩件事:首先,將頂級(jí)單元格內(nèi)容設(shè)置為結(jié)果值(“Single” 或 “Married”),接著更新 cellDescriptor 數(shù)組中的 switch 控件值,這樣每次 tableview 刷新時(shí)它都擁有正確的狀態(tài)。下面的代碼片段中,你會(huì)注意我們首次根據(jù) switch 控件狀態(tài)來(lái)確定適當(dāng)?shù)闹担又鴮⑺鼈冑x值給相應(yīng)屬性:
func maritalStatusSwitchChangedState(isOn: Bool) {
let maritalSwitchCellSection = 0
let maritalSwitchCellRow = 6
let valueToStore = (isOn) ? "true" : "false"
let valueToDisplay = (isOn) ? "Married" : "Single"
cellDescriptors[maritalSwitchCellSection][maritalSwitchCellRow].setValue(valueToStore, forKey: "value")
cellDescriptors[maritalSwitchCellSection][maritalSwitchCellRow - 1].setValue(valueToDisplay, forKey: "primaryTitle")
tblExpandable.reloadData()
}
接下來(lái)是包含了 textField 控件的單元格。此處一旦有 first name 或 last name 輸入,我們會(huì)動(dòng)態(tài)組合成 full name。出于需要,我們將獲取到包含 textField 控件單元格的行索引值,這樣就能為 full name 設(shè)置給定值了(first name + last name)。最后我們更新頂級(jí)單元格內(nèi)的顯示本文內(nèi)容(full name)和刷新 tableview 。
func textfieldTextWasChanged(newText: String, parentCell: CustomCell) {
let parentCellIndexPath = tblExpandable.indexPathForCell(parentCell)
let currentFullname = cellDescriptors[0][0]["primaryTitle"] as! String
let fullnameParts = currentFullname.componentsSeparatedByString(" ")
var newFullname = ""
if parentCellIndexPath?.row == 1 {
if fullnameParts.count == 2 {
newFullname = "\(newText) \(fullnameParts[1])"
}
else {
newFullname = newText
}
}
else {
newFullname = "\(fullnameParts[0]) \(newText)"
}
cellDescriptors[0][0].setValue(newFullname, forKey: "primaryTitle")
tblExpandable.reloadData()
}
最后在 “Work Experience” 部分中,我們處理那些內(nèi)含 slider 控件的單元格。當(dāng)用戶改變 slider 控件值的同時(shí),我們需要做兩件事:
首先將頂級(jí)單元格中的文本標(biāo)簽內(nèi)容設(shè)置為新的 slider 控件值,接著將 slider 控件值保存到對(duì)應(yīng)的單元格描述中,這樣即使刷新 tableview 后,它始終是最新數(shù)據(jù)。
func sliderDidChangeValue(newSliderValue: String) {
cellDescriptors[2][0].setValue(newSliderValue, forKey: "primaryTitle")
cellDescriptors[2][1].setValue(newSliderValue, forKey: "value")
tblExpandable.reloadSections(NSIndexSet(index: 2), withRowAnimation: UITableViewRowAnimation.None)
}
最后的缺省代碼添加完畢,運(yùn)行應(yīng)用。
總結(jié)
正如一開(kāi)始我所說(shuō)的,創(chuàng)建一個(gè)可展開(kāi)的 tableview 有時(shí)真的很有用,它可以將你從麻煩中拯救出來(lái),無(wú)須再為應(yīng)用各部分創(chuàng)建一個(gè)新的視圖控制器。本教程的前部分中,我向你介紹了一種創(chuàng)建可展開(kāi)的 tableview 的方法,其主要特點(diǎn)是所有單元格的描述都存放在屬性列表文件(plist 文件)中。教程中,我向你展示了如何在顯示、展開(kāi)和選中單元格情況下,編寫(xiě)代碼處理單元格描述列表;另外,我還向你提供了一種方式來(lái)直接更新用戶輸入的數(shù)據(jù)。盡管演示應(yīng)用中的偽造表格在實(shí)際應(yīng)用開(kāi)發(fā)中所有作為,但想要作為一個(gè)完整的組件之前,你還需要實(shí)現(xiàn)一些功能(比如,把表單描述列表保存到文件中)。不過(guò),這已經(jīng)超出了我們的教學(xué)范疇;一開(kāi)始我們只想要實(shí)現(xiàn)一個(gè)可展開(kāi)的 tableview ,隨心所欲地顯示或隱藏單元格,最終也得以實(shí)現(xiàn)。我確信你會(huì)找到本教程
的價(jià)值。通過(guò)已有的代碼,你肯定能在此基礎(chǔ)上改進(jìn),并根據(jù)需求使用它?,F(xiàn)在留點(diǎn)時(shí)間給你;玩得開(kāi)心,切記學(xué)無(wú)止境!
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán),最新文章請(qǐng)?jiān)L問(wèn) http://swift.gg。