幾乎全部的app都有一個共同特徵,它們向用戶提供了多個視圖控制器來導航和工做.這些視圖控制器能夠用在不少方面,例如,簡單地顯示某種信息在屏幕上,或者從用戶的輸入收集複雜的數據.爲不一樣功能的app建立新的視圖控制器常常是強制性的,而且好幾回都是有點讓人退縮的任務.然而,若是你只是使用可展開的tableview,有時也可能避免建立視圖控制器(以及在storyboard中它們各自的場景).javascript
正如這個詞所暗示的,一個可展開的tableView是一個tableView,它能夠"容許"它的cell打開和合攏,顯示和隱藏其餘的cell,在任何狀況下都老是可見.當須要收集簡單的數據或者顯示用戶所須要的信息的時候,建立可展開的tableView是一個不錯的選擇.使用可展開的tableView,在任何狀況下,只是向用戶請求已經存在的數據或是默認的視圖控制器,而不必建立新的視圖控制器.例如,有了可展開的cell,你能夠顯示和隱藏cell,沒必要離開這個視圖控制器收集數據.java
你是否使用可展開的tableView,並不老是取決於你開發的app的性質.然而,經過繼承UITableViewCell類以及建立額外的xib文件,cell的界面能夠自定義,app的外觀和感受一般不是一個問題.因此最終這只是一個要求.編程
在這個教程中,我將會向你展現一個簡單高效的方式來建立可展開的tableView.注意,你在這裏所看到的並非惟一的方法來實現這個功能.至關多的實現方法是基於app的須要,可是個人目標是是提出一種比較通用的方法,在大多數狀況下能夠被重複使用.因此,說了這麼多,前往下一個部分體會咱們將在這次教程中處理的內容吧.swift
經過實現一個包含tableView的視圖控制器的app,咱們將會看到可展開的tableView是如何建立和工做的.咱們將會作一個假的表格讓用戶輸入數據,爲此,tableView將要包含下面三個組:數組
每組(section)都將包含可展開的cell,這將觸發顯示或隱藏每組中附加的cell,具體來講,每組的頂級cell(那些將會打開或是合攏的cell)就是:markdown
對於"Personal"組來講app
Full name(全名):它顯示了用戶的全名,而且當它打開的時候,它底下還包括兩個可用於輸入姓和名cell.ide
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.
在這次教程中,我所提出的有關可展開的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文件裏.
是時候來寫代碼了,儘管咱們使用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.
瞭解了每次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.不要忘了咱們尚未啓動打開功能,因此你點擊的時候不會發生任何事.可是,不要泄氣,由於你所看到的意味着到目前爲止咱們所作的工做是完美的.
未完待續~