原文地址
本文作者:gabriel theodoropoulos
原文:How To Create an Expandable Table View in iOS
原文鏈接
幾乎所有的app都有一個共同特征,它們向用戶提供了多個視圖控制器來導航和工作.這些視圖控制器可以用在很多方面,例如,簡單地顯示某種信息在屏幕上,或者從用戶的輸入收集復雜的數據.為不同功能的app創建新的視圖控制器經常是強制性的,并且好幾次都是有點讓人退縮的任務.然而,如果你只是使用可展開的tableview,有時也可能避免創建視圖控制器(以及在storyboard中它們各自的場景).
正如這個詞所暗示的,一個可展開的tableView是一個tableView,它可以"允許"它的cell打開和合攏,顯示和隱藏其他的cell,在任何情況下都總是可見.當需要收集簡單的數據或者顯示用戶所需要的信息的時候,創建可展開的tableView是一個不錯的選擇.使用可展開的tableView,在任何情況下,只是向用戶請求已經存在的數據或是默認的視圖控制器,而沒必要創建新的視圖控制器.例如,有了可展開的cell,你可以顯示和隱藏cell,不必離開這個視圖控制器收集數據.
你是否使用可展開的tableView,并不總是取決于你開發的app的性質.然而,通過繼承UITableViewCell類以及創建額外的xib文件,cell的界面可以自定義,app的外觀和感覺通常不是一個問題.所以最終這只是一個要求.
在這個教程中,我將會向你展示一個簡單高效的方式來創建可展開的tableView.注意,你在這里所看到的并不是唯一的方法來實現這個功能.相當多的實現方法是基于app的需要,但是我的目標是是提出一種比較通用的方法,在大多數情況下可以被重復使用.所以,說了這么多,前往下一個部分體會我們將在此次教程中處理的內容吧.
關于演示的app
通過實現一個包含tableView的視圖控制器的app,我們將會看到可展開的tableView是如何創建和工作的.我們將會做一個假的表格讓用戶輸入數據,為此,tableView將要包含下面三個組:
- 個人(Personal)
- 偏好(Preferences)
- 工作經驗(Work Experience)
每組(section)都將包含可展開的cell,這將觸發顯示或隱藏每組中附加的cell,具體來說,每組的頂級cell(那些將會打開或是合攏的cell)就是:
對于"Personal"組來說
Full name(全名):它顯示了用戶的全名,并且當它打開的時候,它底下還包括兩個可用于輸入姓和名cell.
Date of birth(生日):它顯示了用戶的出生日期,當它打開的時候,提供了一個日期選擇器(date picker view),底部還有一個按鈕,當選中一個日期的時候,點擊按鈕可以把設置的日期顯示到頂部cell上.
Marital status(婚姻狀況):這個cell顯示了用戶的婚姻狀況(已婚或者單身).當它打開的時候,提供了一個開關控件來設置用戶的婚姻狀態.
對于"Preferences"組來說:
Favorite sport:我們的假表格要求用戶選擇最喜歡的運動.當這個cell打開的時候,四個包含運動名的選項就出現了,并且當一個選項被點擊后,這個cell就會"自動地"合攏起來.
Favorite color:和上面一樣,這個時候就會顯示三種不同的顏色來供用戶選擇.
對于“Work Experience”組來說:
Level:當頂級cell被點擊打開的時候,另一個帶有滑塊控件的cell就出現了,讓用戶指定一個假設的工作經驗.允許的值在0...10這個范圍之間,我們將保持唯一的整數值.
下面的動態圖可以清楚的表明我們將要做什么:
你可以注意到上面的tableview打開的時候有多種類型的cell.所有這些你都可以在啟動項目里找到,可供你下載,還包括一些其他將要實現的東西.設計的所有自定義cell都在單獨的xib文件中,同時一個自定義的UITableViewCell子類(命名為CustomCell)已經被分配為他們的自定義類:
在項目中你會發現有如下自定義cell的xib文件:
它們的名字說明了每個cell所代表的含義,你可以在啟動項目中更深的區探索它們.
除了這些cell,你也可以找到一些已經被實現的代碼.雖然這些代碼是重要的并且完成了demo的功能,但是它們并不是此次教程的核心代碼,所以就跳過了編寫代碼并且已經提供了寫好的代碼.當我們通過下面的部分,缺失的那些我們所感興趣的代碼都會在下面一步一步地增加.
所以,現在你知道我們最終的目標了,因此下面我們將要學習如何創建一個可展開的tableView.
描述這些cell
在此次教程中,我所提出的有關可展開的tableView,其中涉及的所有實現和技術都是基于一個簡單的想法:為app描述每一個cell的細節.這樣讓它知道是可能的,cell是否可以展開,是否可見,以及每個cell的文本標簽的值是什么,等等.事實上,整個想法都是基于分組的屬性,那既描述了屬性也包含了每個cell的某些值,然后把它們提供給app,以便正確地顯示它們.
對于這個示例app,我創建并且使用了在下一列表里中顯示的屬性.注意,一個真實的app可以添加新的屬性,或者修改現有的屬性.在任何情況下,重要的是你設法在這里學到有用的東西.然后你就可以完成所有你期望的改變.屬性列表如下:
isExpandable:它是一個布爾值,表示一個cell是否可以展開.對于我們來說,在這篇教程中,它是最重要的屬性之一.
isExpanded:也是一個布爾值,表示一個可以展開的cell是展開狀態還是合攏狀態.頂級的cell默認是合攏的,所以,所有的cell初始值都會設置成 NO.
isVisible:正如名字所暗示的,表示cell是否可見.稍后,它將發揮重要作用,我們將基于屬性,所以我們要在tableView里顯示合適的cell.
value:這個屬性對保持UI控制的值是有用的(例如,婚姻狀態開關控制的值).并不是所有的cell都有哪些控制,所以大多數情況,這個屬性會保持為空.
primaryTitle:它是cell主標題上的文本,很多次都包含了應該被顯示在一個cell上實際的值.
secondaryTitle:它是cell子標題上的文本,或者是第二個標簽的文本.
cellIdentifier:它是匹配當前描述的自定義cell的標識符.它不僅僅被app用來出隊合適的cell,而且它也會決定應該采取適當地行動,取決于顯示的cell,以及每個cell具體的高度.
additionalRows:當一個可以展開的cell被打開的時候,它包含了應該被顯示附加行的總數.
上面的這些屬性,將會被用來描述每一個我們在tableView中有的cell.在app級的術語,我們要做的就是使用一個簡單易用的屬性列表(plist)文件.在這個plist文件中,我們需要合適地填充這些在所有cell上的屬性,這樣,我們將會有一個完整地技術描述,可以讓我們和這個app使用.并且所有這些沒有寫一行代碼,是不是很好?
在這一點上,我們通常會在我們的工程中創建一個新的plist文件,然后我們將開始填充合適的數據.當然你也可以不這么做,你可以下載.plist文件.所以,下載它并把它添加到起始項目里去吧.設置所有cell的屬性需要大量的空間,這將是沒有意義的,并且你只是拷貝-粘貼或是輸入缺失的值,也是又累又無聊的.然而,讓我們討論一下這一點:
首先,你(希望)下載的文件名為CellDescriptor.plist.根節點(root)是一個數組,它的每一項在tableView里都代表一組.這就意味著,在plist文件里,根數組里包含三個項(item),和我們想要在tableView里顯示的數量一樣多.
上面的item也是數組,并且它們自己的item描述了每組的cell.實際上,上面的屬性被歸類為字典,并且每個字典匹配單一的cell.下面就是一個簡單地plist文件:
現在是最好花費你時間的時候了,更徹底地看這些屬性以及所有那些我們將要顯示在tableView上cell的值.在我們處理所需的代碼時候,通過cell描述很容易理解,我們需要為創建并且管理可擴展的cell所寫的已經明顯變少了,那樣,我們將不必控制關于app cell的各種狀態了(例如,哪一個cell是可展開的,是否它允許一個特定cell的展開,用代碼決定一個cell是否可見,等等).所有這些信息都存在你剛剛下載的plist文件里.
加載cell描述
是時候來寫代碼了,盡管我們使用plist文件已經節省了很多代碼,但是還是需要在工程中添加一些代碼.現在描述cell的plist文件已經存在了,我們要做的第一件事就是要用編程把plist文件的內容加載到一個數組里.在下面的部分,這個數組將會被用作tableView數據源的一部分.
首先,打開工程中的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數組.
下一步是調用上面的函數,在view正確出現之前,tableView已經配置之后(我們需要在顯示數據之前就創建號tableView)我們要做的才是調用函數:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
configureTableView()
loadCellDescriptors()
}
如果你在上面代碼的最后一行寫了print(cellDescriptors)命令并且運行app,你將會在控制臺上看見所有的plist文件里的內容.這就意味著它們已經成功地加載到了內存.
正常來說,我們的工作到這部分已經結束了,但是我們不會那么做的;我們還有別的要增加,下面的部分才是至關重要的.正如你到目前為止所發現的(特別是如果你檢查了CellDescriptor.plist文件),不是所有的cell都會在app運行的時候顯示.實際上,我們不知道它們是否能在一起同時看到,因為當用戶需要的時候,它們可以展開或合攏.
在程序的世界中,那就意味著每個cell的行索引(index)不是不變的(我們寫index.row來處理cell),因此我們在使用cell行的時候,不能僅僅通過數據源數組.這是強制性的工作以及拿出提供可見cell的行索引的解決方案.因為不可見的cell會導致一個實現錯誤,當然,app也會有異常.
所以,由于這個原因,我們將會實現一個新的方法getIndicesOfVisibleRows().它的名字說明了它的作用:這個方法會取得那些已經標記為僅可見的cell行的索引值.在我們實現之前,請再一次移到類的頂部加入如下代碼:
var visibleRowsPerSection = [[Int]]()
這個二維數組將會存儲每組中可見cell的索引(其中一維是組,另一維是行).
現在讓我們實現這個新的函數吧.你可能猜到了,我們將通過所有的cell描述和我們在上面添加的cell索引的2D數組,把"可見"屬性設置為YES.顯然,我們需要處理一個嵌套循環,但是卻不難處理.下面是這個函數的實現:
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數組中先前所有的內容,否則隨后我們在調用這個函數的時候會得到錯誤的數據.
第一次上面的函數應該可以被正確地調用,之后cell描述符會從文件加載.所以,再看一下我們實現的第一個函數,我們做如下修改:
func loadCellDescriptors() {
if let path = NSBundle.mainBundle().pathForResource("CellDescriptor", ofType: "plist") {
cellDescriptors = NSMutableArray(contentsOfFile: path)
getIndicesOfVisibleRows()
tblExpandable.reloadData()
}
}
盡管tableView還沒有起作用,我們觸發一個預先加載的活動,所以我們要確保在app啟動之后,會顯示合適的cell.
顯示cell
了解了每次app運行的時候cell描述符都會被加載,我們繼續吧,在tableView上顯示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的索引路徑值(NSIndexPath),它返回了一個字典,包含了所有cell匹配的屬性.在它函數體里的第一個任務就是找出匹配索引路徑的可見行的索引,這很容易做,因為我們需要的是cell的組合行(section and row).到目前為止我們沒有處理過tableView的代理方法,所以我必須提前說,每組的總行數將會匹配在每一個組里可見cell的個數.也就是說,在上面的實現中,任意indexPath.row的值匹配到了在visibleRowsPerSection里合適的可見cell的索引.
通過讓每個cell都有行號,我們可以從cellDescriptors數組中,"提取"cell描述的字典.注意,指定為二維的索引是indexOfVisibleRow,而不是indexPath.row.使用第二個會返回錯誤的數據.
我們又創建了一個有用的工具,接下來它將會變得非常方便,所以讓我們來修改ViewController類中已存在的tableView方法吧.首先,讓我們指定tableView的組數:
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
if cellDescriptors != nil {
return cellDescriptors.count
}
else {
return 0
}
}
你要明白,我們不能忽略cellDescriptor為nil這種情況.如果子數組已經被初始化,并且填充了cell描述符的值,那么我們返回的是子數組的大小.
然后,讓我們指定每組的行數.正如我之前說的,這個數量總是等于可見cell的數量,我們可以在一行cell上返回信息:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return visibleRowsPerSection[section].count
}
在那之后,讓我們設置tableView每組的標題:
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"屬性,它的值依賴于具體的行高.你可以驗證各自的xib文件cell的高度值.
最后,實際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
}
我們又一次基于當前的索引值獲得了合適的cell描述符.通過使用"cellIdentifier"屬性,正確的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來說,我們只是把primaryTitle和
secondaryTitle的值分別設置了給了textLabel和detailTextLabel.在我們的demo里,帶有idCellNormal標識符的cell實際上是頂層可展開和合攏的cell.
對于含一個文本輸入框的cell來說,我們只需通過cell描述符的primaryTitle屬性來設置placeholder的值.
關于包含開關控件的cell,我們需要做有兩件事:在開關顯示之前,我們就需要制定它的顯示文本(在我們的例子中是不變的,你可以在CellDescriptor.plist文件里修改里賣弄的值),之后我們就看到了開關的狀態,根據它是否被設置為"on"或者沒有描述符.注意,之后我們會修改這個值.
也有一些cell有"idCellValuePicker"標識符.那些cell意味著提供了一列選項,并且一個選項的父cell被選中的時候,它將會自動合攏.在上面顯示的情況,將會指定cell的文本標簽.
最后,還有一種包含滑塊的cell的情況.我們只是從currentCellDescriptor字典里取得了當前的值,我們把它轉換成一個浮點數字,我們將把它分配給滑塊設置,所以在任何時候,它都顯示了合適的值(當它可見的時候).稍后我們將更改值,以及我們將會更新各自的cell描述符.
對于cell來說,在上述語句中,cell的標識符沒有顯示地增加,app也沒有任何改變.然而,如果你想以一種不同的方式處理,隨意修改代碼并且添加任何丟失的部分.
現在你可以運行app看一下結果了.不要期望看到太多東西,你將會看到頂層的cell.不要忘了我們還沒有啟動打開功能,所以你點擊的時候不會發生任何事.但是,不要泄氣,因為你所看到的意味著到目前為止我們所做的工作是完美的.
未完待續~