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

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

項目中你可以找到以下單元格的 xib 文件:

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

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

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

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