在iOS中怎樣創建可展開的Table View?(上)

原文地址

本文作者: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將要包含下面三個組:

  1. 個人(Personal)
  2. 偏好(Preferences)
  3. 工作經驗(Work Experience)

每組(section)都將包含可展開的cell,這將觸發顯示或隱藏每組中附加的cell,具體來說,每組的頂級cell(那些將會打開或是合攏的cell)就是:

對于"Personal"組來說

  1. Full name(全名):它顯示了用戶的全名,并且當它打開的時候,它底下還包括兩個可用于輸入姓和名cell.

  2. Date of birth(生日):它顯示了用戶的出生日期,當它打開的時候,提供了一個日期選擇器(date picker view),底部還有一個按鈕,當選中一個日期的時候,點擊按鈕可以把設置的日期顯示到頂部cell上.

  3. Marital status(婚姻狀況):這個cell顯示了用戶的婚姻狀況(已婚或者單身).當它打開的時候,提供了一個開關控件來設置用戶的婚姻狀態.

對于"Preferences"組來說:

  1. Favorite sport:我們的假表格要求用戶選擇最喜歡的運動.當這個cell打開的時候,四個包含運動名的選項就出現了,并且當一個選項被點擊后,這個cell就會"自動地"合攏起來.

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

你要明白,我們不能忽略cellDescriptornil這種情況.如果子數組已經被初始化,并且填充了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的值分別設置了給了textLabeldetailTextLabel.在我們的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.不要忘了我們還沒有啟動打開功能,所以你點擊的時候不會發生任何事.但是,不要泄氣,因為你所看到的意味著到目前為止我們所做的工作是完美的.

未完待續~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容