原文:http://www.appcoda.com.tw/expandable-table-view/

译者:kmyhy(appcoda的驻站译者)

幾乎全部的 App 都會以導航的方式向用戶展示多個 View Controller。這些 View Controller 分別擔任不同的職責,比方在屏幕上簡單顯示一些信息,或者從用戶輸入中收集複雜數據。針對 App 的不同功能設計新的 View Controller 是一件不可缺少的任務,有時候甚至是一種頗為艱鉅的任務。可是,許多時候,通過展開式 UITableView。我們卻能够不用創建新的 View Controller 就能完毕同樣的任務。

顧名思義,一個展開式 UITableView 是這樣一種表視圖,它“允許”其單元格(cell)展開或者收起,顯示或者隱藏,而在一般的表視圖中,它們的單元格仅仅能是顯示的狀態。當我們须要收集一些簡單的數據或者根據用戶的意願顯示/隱藏某些內容時。創建展開式 UITableView 是一種不錯的選擇。這樣,我們就沒有必要僅僅為了讓用戶輸入一些數據就創建新的 View Controller,無論怎样我們都仅仅须要呆在同一個 View Controller 裡面。即當前的 View Controller 中。

比如,通過展開式的 cell,我們顯示或隱藏一個用於給用戶輸入信息的表單,在顯示或隱藏這個表單時,根本不须要離開當前的 View Controller。

是否采用或者不采用展開式的 UITableView。全然取決於 App 的性質。可是。仅仅要是通過子類化UITableViewCell和自定義xib文件的方式來定製 cell 的情況。App 的外觀就不會是什麼問題。

因此,歸根結底。這仅仅是一個需求問題。

在本教程中。我將演示一種創建展開式 UITableView 的簡單有效的方法。

注意。這並不是唯一方法。

最好的方法要視 App 的须要而定,我的目的仅仅是展示一種一般化的解決方案。它在大部份情形下都是適用的。

因此。請進入下一部份,看看本教程最終將實現什麼樣的效果。

演示样例 App

我們將創建一個仅仅有一個 View Controller (其包括有一個 TableView)的 App,在這個 App 中我們將演示怎样創建一個展開式表視圖。我們將模擬一個允許用戶輸入的表單,為了演示,這個 TableView 將由 3 個 section 構成:

  1. Personal
  2. Preferences
  3. Work Experience

每個 section 都會包括展開式 cell。這些 cell 會隱藏/顯示該 section 的其他 cell。尤其是位於 section 頂部的 cell (該 cell 能夠展開或收起):

對於 “Personal” section:

  1. Full name: 這個 cell 用於顯示用戶的全名,當它處於展開狀態時。它以下會多出兩個子 cell。分別用於輸入名和姓。
  2. Date of birth: 用於顯示用戶的生日。

    當它被展開后,會顯示一個日期選擇器(UIDatePicker),允許用戶選擇某個日期并提供一個按鈕將用戶選擇的日期返回給它上面的 cell。

  3. Marital status: 顯示用戶的婚姻狀態:已婚或單身。當它被展開后。會顯示一個開關控件,允許用戶設置他們的婚姻狀態。

對於 “Preferences” section:

  1. Favorite sport: 我們模擬了一個運動種類列表。用於提供給用戶,讓他們從中選擇他們所喜愛的運動。當它被展開時,會列出 4 個運動種類,當用戶選擇某個子項,這個 cell 又自動會被收起。
  2. Favorite color: 和上面非常类似,仅仅不過這裡顯示了一個顏色列表供用戶選擇。

對於 “Work Experience” section:

Level: 當這個 cell 被點擊并展開后,將顯示还有一個包括有一個滑動條的 cell,允許用戶設置他們的工作經驗等級。這個級別用一個 0…10 之間的數字表示,我們仅仅取這個數值的整數部份。

通過以下的動畫會看得更清晰一些:

注意上面的样例,當我們展開 TableView 時會顯示不同類型的 cell。這些 cell 都被包括在

dl=0">开始項目中了,你能够下載這些代碼。項目已經完毕了一些前期的準備工作。全部的 cell 都在單獨的 xib 文件進行了必要的設計,同時它們的 Custom Class 也被指定為自定義的 UITableViewCell 子類(即 CustomCell):

在項目文件夾中,你將發現這些 cell 所使用的 xib 文件。包括:

它們作用分別如其名稱所看到的,你也能够下載开始項目深入探究一番。

除了 cell,你還會發現一些已經寫好的代碼。雖然這些代碼對於實現整個演示样例 App 的功能來說是不可缺少的。但卻不屬於本教程的核心內容,因此我會跳過這些代碼,僅僅是以現成的代碼提供在开始項目中。缺失的其餘代碼是本教程中我們最關心的內容,在接下來的教程中會以 step-by-step 的方式加入到項目中。

到此,你已經知道我們最後的目標是什麼了,接下來就讓我們开始學習怎样創建展開式的 UITableView。

描写叙述單元格

我將在本教程中演示的、全部與展開式 UITableView 相關的實現和技術,都基於這樣一種簡單的思路:向 App 描写叙述每個 cell 的細節。通過這種方式我們讓 App 知道每一個 cell 究竟是展開的還是收起的。是可見的還是隱藏的,每個 cell 的文字標籤顯示什麼內容。等等。實際上,整體思路都基於將屬性集進行編組,這些屬性要麼描写叙述了每個 cell 的屬性,要麼包括了 cell 的某些數值,然後將這些屬性告訴給 App,這樣 App 才干正確地顯示它們。

在本演示样例程序中,我創建和使用了一個屬性集合,如以下所列。注意在真正的 App 中。你可能须要添加新的屬性,或者對某些屬性進行改动。不過,此時你仅仅须要了解大致的情況就能够了。當然,仅仅要你願意你能够随意改动這些屬性。

我們所使用的屬性列表(plist)是這樣的:

  • isExpandable: 一個布爾值。標明 cell 能否夠展開或收起。

    在本教程中,這是我們非常關心的重要屬性。

  • isExpanded: 一個布爾值,標明一個展開式 cell 是處於展開狀態還是收起狀態。頂層的 cell 默認是收起狀態,因此這個值一开始都應該設成 NO。
  • isVisible: 顧名思義,標明這個 cell 是否應該顯示到表格中。

    稍後這個屬性會扮演一個重要的角色,因為我們會根據這個屬性讓表格中的某些 cell 得到顯示。

  • value: 這個屬性用於保存 UI 控件的值(比如,開關控件中的婚姻狀態)。不是全部 cell 都會有這樣的控件。因此大部份 cell 的這個屬性值將保留為空。
  • primaryTitle: cell 主標題的顯示文本。當這個屬性不為空時,這個屬性的值會顯示到 cell 上。

  • secondaryTitle: cell 子標題的顯示文本,或者 cell 第二個標籤的顯示文本。
  • cellIdentifier: 自定義 cell 的ID。用於唯一識別當前 cell 的描写叙述。

    這個 ID 不仅被 App 用於从缓存队列中弹出合适的 cell,并且还要根據这个 ID 对要顯示的 cell 進行相應的處理并指定 cell 的高度。

  • additionalRows: 用於表示當一個展開式單元格被展開時,它以下包括了幾個附屬的 cell。

每個 cell 都會用上面的屬性集進行描写叙述。從 App 的角度,我們使用一個屬性列表(plist)文件來保存它們會更加輕鬆。在這個plist文件里。我們會為每個 cell 使用一個上述屬性集來進行描写叙述。并適當地填充屬性集中的屬性值,這樣,我們將最終獲得全部 cell 的一個完整的描写叙述。這個描写叙述對於我們或 App 來說都非常easy理解。同時我們并沒有為之編寫一行代碼。非常不错吧?

現在,我們在項目中新建一個 plist 文件。然後用適當的數據來填充它。當然你也能够從這裡下載現成的.plist 文件。下載后記得將它加入到我們的开始項目中。手動設置全部 cell 的屬性會佔用大量空間,這是全然沒有必要的。同時拷貝-粘貼或者輸入全部屬性值也是一件非常繁瑣的事情。

然後,讓我們來討論一下這個 plist 文件:

首先,你下載的這個文件的文件名称叫做CellDescriptor.plist。它的根節點(root)是一個數組。当中的每個元素表示表格中的一個 section。

也就是說這個plist文件的 root 數組中有三個元素,就跟我們想在表格中顯示的 section 的數目一樣。

每個 section 本身也是一個數組。數組中包括了該 section 中所包括的全部 cell 的描写叙述。

實際上,這些編組的屬性集在這裡用字典來進行表示。每個字典代表了一個單獨的 cell。

這是一個 plist 文件的样例:

現在,是時候來完整地回顧一下我們將要顯示到表格中的 cell 的屬性集和屬性值了。非常顯然。擁有了這些 cell 描写叙述之後,我們须要編寫用於生成、管理 cell 的代碼大大減少了。

我們也不须要告訴 App 這些 cell 的各種狀態(比如,哪個單元格是可展開的,App 應當讓某個 cell 展開或收起。判斷某個 cell 是可見的還是隱藏的等等)。全部的這些信息都包括在你下載的 plist 文件裡面。

加載單元格描写叙述

終於能够編寫代碼了。雖然我們使用的單元格描写叙述技術為我們節省了許多時間,但在這個項目中我們仍然免不了要編寫代碼。現在,我們已經有了用於描写叙述 cell的 plist 文件了,接下來的事情自然是用代碼將文件內容加載到一個數組對象中。這個數組對象將在後面充當表格的數據源。

打開开始項目中的 ViewController.swift 文件。在類的頂部聲明例如以下屬性:

var cellDescriptors: NSMutableArray!

這個數組將用於包括全部來自于 plist 文件里的用於描写叙述每個 cell的字典。

然後,新增一個方法。用於將文件內容加載到數組對象。我們將這個方法命名為 loadCellDescriptors()

func loadCellDescriptors() {
if let path = NSBundle.mainBundle().pathForResource("CellDescriptor", ofType: "plist") {
cellDescriptors = NSMutableArray(contentsOfFile: path)
}
}

這個方法非常簡單:首先我們我們判斷指定的 plist 文件路徑在 bundle 中是否存在。假设存在我們從文件里加載一個數組并初始化 cellDescriptors 變量。

接下來就是調用這個方法,我們將在 TableView 已經配置好,並且視圖即將顯示之前(即在 TableView 已經創建並且還沒有顯示不论什么內容之前)調用這個方法:

override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
configureTableView() loadCellDescriptors()
}

假设在上述方法最後一行後加入 print(cellDescriptors) 一句,則運行 App 后你將看見 plist 文件的內容輸出到了控制台中。

這就說明文件已經成功加載到內存了。

通常,本節的內容應該到此結束,但這次有一點例外。我們還要補充一些對於下一節來說至關重要的內容。也許你想到了(尤其是當你檢查了CellDescriptor.plist文件之後),當 App 啟動后,并非全部的 cell 都應該被顯示。

事實上。我們根本無法得知它們是否會在同時顯示,因為它們是根據用戶的要求來進行展開和收起的。

從編程的角度,這意味著 每個 cell 的行索引不應該是常量 (這就是為什麼我們在處理每個 cell 時,將 indexPath.row 用代碼來生成)。同時,我們也不能用 cell 的行索引來遍歷數據源數組并顯示每個 cell。

我們仅仅能將可見的 cell 的行索引來提供給 App。假设將 cell 描写叙述中標記為不可見的 cell 顯示出來,這就大錯特錯了。那會導致 App 表現異常。

基於這樣的原因,我們须要實現一個新方法。叫做 getIndicesOfVisibleRows()

這個方法的作用是顯而易見的:它仅仅返回那些標記為可見的 cell 的行索引。在實現這個方法之前,請在類的頂部添加例如以下屬性:

var visibleRowsPerSection = [[Int]]()

這是一個二維數組,保存了全部 section 的可見的 cell 的行索引(一維用於表示 section。一維用於表示 cell)。

現在來實現這個方法。你也許想到了,我們會遍歷全部 cell 描写叙述并將 isVisible 屬性為 true 的 cell 的行索引加入到二維數組中。當然,我們不得不用到嵌套循環。但這也不是什麼大問題。

以下是這個方法的實現:

func getIndicesOfVisibleRows() {
visibleRowsPerSection.removeAll() for currentSectionCells in cellDescriptors {
var visibleRows = [Int]() for row in 0...((currentSectionCells as! [[String: AnyObject]]).count - 1) {
if currentSectionCells[row]["isVisible"] as! Bool == true {
visibleRows.append(row)
}
} visibleRowsPerSection.append(visibleRows)
}
}

注意,方法的一开始就將 visibleRowsPerSection 數組的內容清空了,否則連續多次調用這個方法之後數據就不正常了。接下來的實現就一目了然了,就不用我再多說了。

這個函數的第一次調用應該在從 plist 文件加載完 cell 描写叙述之後(我們還會在後面多次調用這個函數)。因此。回到本節實現的第一個方法。將它改动為:

func loadCellDescriptors() {
if let path = NSBundle.mainBundle().pathForResource("CellDescriptor", ofType: "plist") {
cellDescriptors = NSMutableArray(contentsOfFile: path)
getIndicesOfVisibleRows()
tblExpandable.reloadData()
}
}

雖然 TableView 還不能正常工作,但我們已經在 App 一啟動的時候就調用了它的刷新動作,這樣就能保證在接下來的的步驟中顯示正確的 cell。

顯示單元格

每當 App 一啟動,cell 描写叙述就會被加載。接下來我們應當處理和顯示表格中的 cell。

一开始,我們须要創建一個新的方法,用於在 cellDescriptors 數組中查找并返回指定 cell 的單元格描写叙述。

正如以下的代碼所看到的,這個方法能夠正常工作的前提。是你已經擁有一個填充好數據的 visibleRowsPerSection 數組。

func getCellDescriptorForIndexPath(indexPath: NSIndexPath) -> [String: AnyObject] {
let indexOfVisibleRow = visibleRowsPerSection[indexPath.section][indexPath.row]
let cellDescriptor = cellDescriptors[indexPath.section][indexOfVisibleRow] as! [String: AnyObject]
return cellDescriptor
}

這個方法的参數是某個 cell 的 IndexPath 值(NSIndexPath),這個 cell 就是 TableView 當前正在處理的那個 cell。這個方法返回了一個字典對象,包括了該 cell 的全部屬性值。在方法體中,首先须要根據給定的 IndexPath 去可見行數組中進行匹配,這個任務非常簡單。我們仅仅须要提供這個 cell 的 section 索引和行索引就能够了。現在你可能還有點摸不著頭腦,因為我們還沒有介紹 TableView 的委託方法。因此我必須先告訴你每個 section 的行數應當等於每 section 中可見 cell 的個數。也就是說,在上面的代碼中,我們必須保證每個 indexPath.row 都能在 visibleRowsPerSection 中找到對應的可見的 cell 的索引。

擁有了每個 cell 的行索引之後,我們就來處理和“讀取”從 cellDescriptors 數組獲取的 cell 描写叙述字典。

注意,我們在指定這個數組的第二個下標索引時。使用的是 indexOfVisibleRow 而不是 indexPath.row

假设你使用了後者,得到的數據是不正確的。

實現了這個工具方法之後,我們後面就比较輕鬆了。

接下來我們开始改动 ViewController 類中的 TableView 方法。首先,指定 TableView 的 sectio 數:

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if cellDescriptors != nil {
return cellDescriptors.count
}
else {
return 0
}
}

在這個方法中,我們必須考慮到 cellDescriptor 數組為 nil 的情況。仅仅有當它不為空且填充了 cell 描写叙述時我們才返回它的長度。

然後,讓我們指定每 section 的行數。就像我剛才所說的,這個數字應該等於可見 cell 的數目。我們仅仅须要一行代碼就能够搞定這個:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return visibleRowsPerSection[section].count
}

接下來。是每一個 section 的標題:

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
}
}

這裡须要說明一下:我們第一次使用了 getCellDescriptorForIndexPath: 方法。這個方法是我們在前面實現了的。我們须要獲得每個 cell 的描写叙述。因為我們接著還须要讀取 cellIdentifier 屬性。用這個值去決定行的高度。關於每個 cell 的高度,我們能够打開相應的 cell 的 xib 文件獲知(或者你也能够不用管,直接使用這裡提供的數值好了)。

最後,才是真正去顯示 cell。首先,從單元格重用隊列中出列一個 cell:

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
}

再一次,我們根據當前 IndexPath 來獲取正確的單元格描写叙述,并通過 cellIdentifier 屬性從單元格重用隊列中出列一個 cell,然後分別針對每種 cell 進行單獨的處理:

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
}

對於一般的 cell,我們仅仅是將 primaryTitlesecondaryTitle 的文本值賦給 textLabeldetailTextLabel 標籤。

在本演示样例程序中。ID 為 idCellNormal 的 cell 實際上是位於 section 頂層的 cell,正是這個 cell 能夠進行展開和收起的動作。

對於帶有一個 TextField 的 cell,我們仅仅是用單元格描写叙述的 primaryTitle 屬性去設置它的 placeholder 值。

對於帶有一個開關控件的 cell,我們须要做兩個動作:首先設置開關控件的顯示文本(在 CellDescriptor.plist 文件里這是一個常量,當然你能够改动它)。然後根據描写叙述中的 value 屬性是否為 true 來設置開關的 on 屬性。注意,之後我們還會改變這個值。

還有一種 ID 為 idCellValuePicker 的 cell。這種 cell 表示它會提供一個選擇列表,當我們選中列表中的某個選項,父 cell 將會自動收起。同時父 cell 的 textLabel 將做相應改變。

最後。是帶有一個滑動條的 cell。我們僅僅是從 currentCellDescriptor 字典中取出當前的 value 值轉換為一個 Float 數字,然後賦給滑動條,讓它總是(在可見的時候)顯示正確的值。稍後我們也會改變這個值以及與之對應的單元格描写叙述。

對於 ID 不在上述 if 語句檢查條件中的 cell,本演示样例 App 不會進行不论什么處理。

當然。假设你不想采取這種方式,仅仅须要改动上述代碼并加入缺少的語句就可以。

現在你能够先運行一下程序,看看運行的結果。當然不會看到很多其他的 cell。因為你仅仅能看到頂層的 cell。別忘記我們還沒有實現展開/收起功能。因此你點擊 cell 也不會發生什麼。但你不用沮喪,因為你看到的這個結果已經表明我們剛才所做的一切已經生效了。

展開/收起

這部份內容可能是你最感興趣的內容了,因為本教程的目標即將在這裡達成。首先我們將讓我們的頂層 cell 在被點擊之後展開/收起。同時子 cell 會適時地顯示/隱藏。

首先须要知道被點到的 cell 位於哪一行(注意,并非 indexPath.row,而是可見單元格的行索引),因此。我們须要在以下的 TableView 委託方法中將行索引保存到某個局部變量:

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let indexOfTappedRow = visibleRowsPerSection[indexPath.section][indexPath.row]
}

儘管讓我們的 cell 展開/收起用不著多少代碼,我仍然打算以 step-by-step 的方式進行講解。一則這會使我的思路更加清晰,二則也方便你了解每個動作的真正含義。現在,我們擁有了被點擊的 cell 的真正的行索引,我們能够用它來檢索 cellDescriptors 數組,看那個 cell 是否是一個“可展開的”的 cell。

假设它是“可展開”的。同時還沒有展開。則我們將認為它應該被展開(用一個標誌變量來表示),否則我們認為它應該被收起:

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
}
}
}

當我們通過一系列條件計算出 cell 是否該被展開或收起之後,我們须要將這個值存到單元格描写叙述集合里,也就是說,我們要改动 cellDescriptors 數組。我們要改动的是選中的 cell 的 isExpanded 屬性,這樣它才會在再次被點擊時表現正確(cell 的 isExpanded 為 true ,則再次點擊時它會收起。否則再次點擊后它會展開)。

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")
}
}

這裡我們不應該忘記一個重要的細節:回忆一下,在單元格描写叙述中,有一個表明 cell 是否應當顯示的屬性 isVisible。這個屬性也應當做相應的改變,這樣那些新增的行才會在 cell 被展開時從隱藏變為顯示。或者在 cell 被收起時由顯示變成隱藏。

事實上,仅仅有改變這個值才干真正實現展開(或相反)的效果。因此。我們须要改动上述代碼,在頂層 cell 被點擊后改动其附屬 cell 的 isVisible 屬性。

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")
}
}
}

我們已經離我們的目標不遠了。但我們還须要注意一件非常重要的事情:在上述代碼中,我們剛剛改动了某些 cell 的 isVisible 屬性。這導致整個可視 cell 的行數也改變了。因此,在我們刷新表格之前。我們還要讓 App 又一次計算可視 cell 的行索引:

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)
}

你也看到了,我以動畫的方式又一次加載了被點擊的 cell 的 section。

當然。假设你不喜歡這種方式的話,你能够改动它。

運行 App 進行测試。連續點擊頂層 cell。cell 隨之展開和收起,雖然現在與子 cell 進行交互還不會發生不论什么事情,但這個結果看起來非常不錯!

獲取輸入內容

從這裡开始,我們要將精力放在數據處理以及用戶和子 cell 控件進行的交互上。

對於 ID 為 idCellValuePicker 的 cell,我們將代碼邏輯實現在當 ID 為 idCellValuePicker 的 cell 被點擊的時候。對於本演示样例程序,在表格的 Preferences section中,有一些 cell 會羅列用戶喜愛的運動和顏色。雖然我已經說過,但這裡我仍然要再說一次。就當是加強一下我們的記憶:當這類 cell 被點擊時,我們想讓對應的頂層 cell 收起(或者隱藏),所選中的值會顯示到頂層 cell。

我之所以一开始就來處理這類 cell,是因為這是我們最後一次還须要和 TableView 的委託方法打交道。在這裡,我們會加入一個 else 分支來處理“不可展開的”cell,然後再對被點到的 cell 的 ID 值進行判斷。假设 ID 值等於 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 語句中。我們將分四個單獨的步驟進行處理:

  1. 找出頂層 cell 的行索引。也就是被點擊的 cell 的“父 cell”的行索引。實際上。我們仅仅须要從這個 cell 的單元格描写叙述向前搜索。所找到的第一個頂層 cell 就是我們要找的 cell(即第一個可展開的 cell)。
  2. 將選中的 cell 的顯示文本賦給頂層 cell 的 textLabel 的 text 屬性。
  3. 將頂層 cell 的 expanded 標記為 false。
  4. 將頂層 cell 的全部的子 cell 標記為隱藏。

實現為代碼則是:

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! for var i=indexOfTappedRow - 1; i>=0; --i {
if cellDescriptors[indexPath.section][i]["isExpandable"] as! Bool == true {
indexOfParentCell = i
break
}
} cellDescriptors[indexPath.section][indexOfParentCell].setValue((tblExpandable.cellForRowAtIndexPath(indexPath) as! CustomCell).textLabel? .text, forKey: "primaryTitle")
cellDescriptors[indexPath.section][indexOfParentCell].setValue(false, forKey: "isExpanded") 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)
}

當我們改动了某些 cell 的 isVisible 屬性之後,可見 cell 的數目就被改變。因此最後兩句是必須的。

現在運行程序,當你選擇了一個喜愛的運動或顏色后 App 會進行適當的響應:

響應其他動作

CustomCell.swift 文件里,找到 CustomCellDelegate 協議,這裡我們已經對全部 required 方法進行了定義。在 ViewController 類中實現這些方法。我們將使 App 能夠對其他動作進行響應。

打開 ViewController.swift 文件,聲明遵循於該協議。在類聲明的頂部加入該協議:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellDelegate

接著,在 tableViwe:cellForRowAtIndexPath: 方法中。將每個 CustomCell 的委託指定為 ViewController 類。

在方法體中,在方法即將返回之前,加入這句代碼:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
... cell.delegate = self return cell
}

好了,現在我們能够來實現委託方法了。

第一個方法是當用戶在 DatePicker 中選定一個日期后,我們將所選定的日期顯示在與之對應的頂層 cell:

func dateWasSelected(selectedDateString: String) {
let dateCellSection = 0
let dateCellRow = 3 cellDescriptors[dateCellSection][dateCellRow].setValue(selectedDateString, forKey: "primaryTitle")
tblExpandable.reloadData()
}

在指定好適當的 section 索引和行索引后,我們將選定日期以字符串格式賦給對應 cell 的單元格描写叙述。注意這個字符串是委託方法通過参數來傳遞給我們的。

然後是帶有開關控件的 cell。

當開關控件的值改變時。我們须要做兩件事情:首先。將合適的值( Single 或 Married)傳遞給對應的頂層 cell,同時用開關控件的值更新 cellDescriptors 數組。這樣當表格刷新后開關控件就會顯示正確的狀態。在以下的代碼中,注意我們首先基於開關控件的狀態來決定適當的值,然後將它們賦給對應的屬性:

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 的 cell。這裡,當用戶的姓或者名輸入有內容時,我們會動態組裝用戶的全名。因此,我們须要指明包括有 TextField 的 cell 的行索引。并根據索引的不同將字符串加入到全名中去(名在前。姓在后)。最後。我們须要更新頂層 cell 的文字,刷新表格。以使它反映出用戶輸入內容的改變:

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()
}

最後,是帶有滑動條的那個 cell,即“Work Experience” section 须要我們處理。

當用戶拖動滑塊。我們须要同時完毕兩件事:用新的滑動條的數值改动頂層 cell 的文本內容(即“經驗級別”),以及將滑動條的值保存到對應的 cell 描写叙述中,使其在刷新表格后能夠更新界面。

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)
}

最後一塊拼圖已經完毕。接下來就是運行 App 進行测試。

總結

正如我一开始所說,有時創建一個展開式 TableView 真的非常实用,因為它讓你直接在表格中處理曾经必須創建新 View Controller 才干解決的問題。在教程的前半部份,我演示了一種創建展開式 TableView 的方法,它的主要特點是在一個屬性列表文件(plist)中以屬性集的方式來描写叙述每個 cell。

我還演示了在單元格顯示、展開和選定時。怎样用代碼來處理單元格描写叙述列表;此外,我還教你怎样用用戶輸入的數據來改动這些 cell。

雖然演示样例App 中模擬的表單在真正的 App 中也是能够用的,但在要把它當做一個完整的組件仍然须要我們考慮很多其他的事情(比如,將 cell 描写叙述列表回寫到文件里)。當然,這已經超出了本文的範圍,我們仅仅是想實現一個展開式的 TableView,讓它的 cell 能够根據须要顯示或隱藏而已,也就是我們最終的實現的那個 App。我希望你能從本教程中發現不论什么對你实用的東西。當然。你能够設法改進教程中的代碼,或者根據须要進行調整。

又到了不得不說再見的時候了,祝你開心,永遠勇於嘗試新的事物!

為便於参考,你能够從 GitHub 下載完整的 Xcode 項目.

譯者簡介

楊宏焱。男,中國大陸籍人士,CSDN 博客專家(個人博客 http://blog.csdn.net/kmyhy)。2009 年开始學習蘋果 iOS 開發,精通 O-C/Swift 和 Cocoa Touch 框架,開發有多個商店應用和企業 App。熱愛寫作。著有多本技術專著,包括:《企業級 iOS 應用實戰》、《iPhone & iPad 企業移動應用開發秘笈》、《iOS8 Swift 編程指南》,《寫給大忙人看的 Swift》(合作翻譯)等。

怎样創建 iOS 展開式 UITableView?的更多相关文章

  1. 「Ionic」創建新項目

    1.創建新項目 创建一个名为myApp的还有tabs的项目(ionic start <project-name> <optional-template>) 可选模板为sidem ...

  2. 使用 docsify 創建自己的 markdown 文檔系統

    先來看一下我在碼雲上創建的demo: http://lin1270.gitee.io/nicedoc/#/ GIT自己clone一下: https://gitee.com/lin1270/nicedo ...

  3. 使用ReactiveCocoa实现iOS平台响应式编程

    使用ReactiveCocoa实现iOS平台响应式编程 ReactiveCocoa和响应式编程 在说ReactiveCocoa之前,先要介绍一下FRP(Functional Reactive Prog ...

  4. 如何創建一個自己的 Composer/Packagist 包 (PHP)

    如何創建一個自己的 Composer/Packagist 包 首先讓我們踏着歡快的腳步去Github創建一個新庫,這裏取名 composer-car,又歡快的將它克隆到本地: git clone ht ...

  5. 使用TFS創建團隊項目

    使用微軟賬號登錄Team Service,關聯一個TS賬戶,用來存放你所有的項目,可以從瀏覽器中直接訪問,地址類似yourname.visualstudio.com. 詳細鏈接 在TS賬戶主面板中,可 ...

  6. iOS开发UI篇—UITableview控件简单介绍

    iOS开发UI篇—UITableview控件简单介绍 一.基本介绍 在众多移动应⽤用中,能看到各式各样的表格数据 . 在iOS中,要实现表格数据展示,最常用的做法就是使用UITableView,UIT ...

  7. iOS开发UI篇—UITableview控件基本使用

    iOS开发UI篇—UITableview控件基本使用 一.一个简单的英雄展示程序 NJHero.h文件代码(字典转模型) #import <Foundation/Foundation.h> ...

  8. iOS开发UI篇—UITableview控件使用小结

    iOS开发UI篇—UITableview控件使用小结 一.UITableview的使用步骤 UITableview的使用就只有简单的三个步骤: 1.告诉一共有多少组数据 方法:- (NSInteger ...

  9. IOS中 什么是UITableView的索引放大镜字符

    IOS中 什么是UITableView的索引放大镜字符 [_dataSource addObject:UITableViewIndexSearch]; 版权声明:本文为博主原创文章,未经博主允许不得转 ...

随机推荐

  1. Python字典嵌套

    import copy menu = { "计算机科学与技术":{ "程序编程":{ "传统语言":{ "C++":&q ...

  2. js应用中的小细节-时间戳的转换和input输入框有效数字

    1 input输入框内value值保留有效数字,js自带的方法.toFixed(),但是直接使用会报错,因为不论输入框内输入汉字.字母还是数字,类型都是string.解决的办法是将其转换为number ...

  3. HDU 4325 Contest 3

    很明显的区间加减单点查询.但由于规模大,于是离散化.在离散化的时候,可以把要查询的点也加入离散化的数组中. #include <iostream> #include <algorit ...

  4. Pointcut is not well-formed: expecting 'name pattern' at character position 36

    Pointcut is not well-formed: expecting 'name pattern' at character position 36 学习了:http://blog.csdn. ...

  5. 零基础学python-5.2 数字表达式操作符

    表达式是处理数字最主要的工具 a=1#常量 a=a+1#表达式 操作符 操作符 描写叙述 yield 生成 器函数发送协议 lambda args:expression 生成匿名函数 x if y e ...

  6. JAVA设计模式之【模板方法模式】

    模板方法模式 提高代码的复用性 把常用的基本方法放入父类中 强调一种流程步骤 角色 抽象类 抽象方法 具体方法 钩子方法 空方法 通过bool控制 具体类 看例子 1.银行模板类 package Te ...

  7. xBIM 实战04 在WinForm窗体中实现IFC模型的加载与浏览

    系列目录    [已更新最新开发文章,点击查看详细]  WPF底层使用 DirectX 进行图形渲染.DirectX  能理解可由显卡直接渲染的高层元素,如纹理和渐变,所以 DirectX 效率更高. ...

  8. BZOJ 1283 费用流

    思路: 最大费用最大流 i->i+1 连边k 费用0 i->i+m (大于n的时候就连到汇) 连边1 费用a[i] //By SiriusRen #include <queue> ...

  9. word-wrap与word-break的区别,以及无效情况

    两种方法的区别说明: 1,word-break:break-all 例如div宽400px,它的内容就会到400px自动换行,如果该行末端有个英文单词很长(congratulation等),它会把单词 ...

  10. 74HC165应用

    管脚定义与内部逻辑图 注1:其中控制管脚有3个:SH/LD-QH-CLK,CLK INH硬件接VSS,SER和QH'悬空 原理:先拉低SH/LD,A-H置入芯片内部寄存器中,然后拉高SH/LD,锁住A ...