您的位置:首页 > 移动开发 > IOS开发

Expandable Table的Demo

2016-01-27 09:02 651 查看
在网上看到一个可点击cell展开的TableView的demo,原文是swift语言,我写了个oc版,有兴趣的朋友可以看下:

效果:



oc源码(带中文注释):http://download.csdn.net/detail/dolacmeng/9419612

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

部分译文:

    一般app是通过切换多个view controllers来与用户交互,这些view controllers可能只是简单地在屏幕上显示某类信息、让用户输入数据,有时我们可以使用expandable tableviews来实现需求,而不用创建新的view controllers。

    和表面意思一样,expandable tableview是一种可以让它的cell展开或收起的tableview,当需要显示可选信息时,使用expandable tableviews是一个不错的选择,这样就不用创建多个view controllers来让用户输入数据了。例如,可以在不离开当前view controller的情况下,显示和藏起表格的某些选项。

    在这篇博客,我将演示使用一个简单但高效的方法来创建expandable tableviews,当然这并不是实现这个效果的唯一的方法,更多的应该是根据你的app需求。但是我的目标是展示一个比较常规的、可以在大多数情况下重用的方法。所以,说了那么多,我们下一节就开始体验一下我们将要实现的功能。

关于这个Demo App

    我们将要看下在一个包含tableview的view controller中,expandable tableview是怎么创建和工作的。我们先弄一个包含用户需要录入的数据的模拟表格,它包含下面三个section:

1
Personal  个人信息

2
Preferences  喜好

3
Work Experience 工作经验

每一个section将包含可展开的cell,它们可以显示和隐藏子cell。

每一个section的最顶层cell如下:

“Personal” section:

1   Full name:显示用户的全名

2   Date of birth:显示用户出生日期,当它展开时,会出现一个可选择日期的date picker view,和一个确定日期选择的按钮

3   Marital status:显示用户是否已经结婚,当它展开时,会出现一个switch控件来设置婚姻状况。

“Preferences” section:

1  喜欢的体育运动:我们的模拟表单会询问用户喜欢的体育运动,当用户点了这个cell,会展开四个选项,当用户从中选择后,会在此cell中显示选择的结果,并且四个选项被收起。

2  喜欢的颜色:和上面类似,只是这里是显示三个不同的颜色让用户选择。

“Work Experience” section:

Level: 当点击顶层cell展开后,会显示一个包含slider滑块的cell,让用户设设置工作经验等级,我们设定可选择的范围为0-10,并且为integer值。

下面的动态图会让你对我们要实现的效果有更清晰的了解:



你在前文中可能已经注意到了展开tableview后会有多种不同样式的cell,它们都是用xib来设计的,都是继承自UITableViewCell的子类—CustomerCell



在项目中,你会找到下面这些cell对应的xib文件:



它们的名字暗示了它们是哪种类型的cell,不过你可以先在项目工程中深入的探索一下。

除了cell,你会发现已经实现了的其它一些代码。虽然它们也很重要,但是这些代码不是这篇博客的核心,所以我已经写好并且会跳过它们。我们真正感兴趣的代码会在下面一步一步添加。

你已经了解了我们最终要实现的效果,所以我们就开始学习怎么去创建expandable tableview吧。

描述cell

我将要完成的expandable tableview的所有实现方法,都是基于一个简单思想:给app提供每个cell的描述信息。也就是让app知道哪些cell需要展开,哪些不需要、哪些需要显示,哪些不需要、label要显示什么字以及其它更多信息。实际上,全局思想是将包含cell的标记、cell中需要存储的值,以属性组的方式保存下来。然后,让app取得这些数据,正确地显示它们。

在这个demo app中,我创建和使用了下面表格中列出的属性。注意在实际app中,可以根据需求添加新的属性或者重命名它们。不管怎样,重要的是你得到下面这些数据,然后可以根据你期望的效果去修改。属性列表:


isExpandable
:是个bool值,指示是否一个可以展开的cell,它是这篇博文中最重要的属性之一。


isExpanded
: 是个bool值,指示一个可展开cell当前是展开还是收起状态。顶层的cell默认是收起,所以初始值都是NO

• isVisible: 真如名字,表示是否可见。它在后面会扮演一个很重要的角色,基于这个属性来显示tableview中正确的cell.

• value:这个属性是用来记录cell里面控件的值(例如,婚姻状况的switch开关的值)。并不是所有cell都含有控件,所以大部分这这个值会是空的。

• primaryTitle: 主要标题的文字,有时是cell需要显示的实际值。


secondaryTitle
: 副标题

• cellIdentifier:对应custom cell的identifier,是用来获得对应cell的,同时也用来确定对应的动作事件以及确定cell的高度。

• additionalRows: 包含子cell的总数量,就是展开时需要增加的总行数。

 每一个单独的cell会用到上面这些属性,而我们会使用property list(plist)文件。在这个plist文件中,我们会给每个cell的属性都正确地赋值,而我们一行代码都不需要写。是不是很爽?

我们先在项目中创建一个新的plist文件,然后填充数据。但你没必要这样做,你可以直接下载已经准备好的.plist文件。下载并且添加到项目中即可,而不需要你再花费时间做意义不大的复制粘贴工作。

首先,你下载的文件名字为CellDescriptor.plist,最外层结构(root)是array数组,每一项代表tableview的一个section。也就是说最外层的array数组包含三项,数量等于我们希望在tableview中显示的数量(三项)。上面所说的这三项,分别也是一个array数组,每一项分别包含了每一个section中的cell的描述。实际上,每一个单独的cell的属性以字典的形式存储。这里是plist文件的例子:



现在,我们就花时间进一步看看我们准备在tableview中显示的cell的属性和对应的值。很容易理解,通过使用处理好的数据,我们需要编写来创建和管理可收起cell的代码显著地减少了,我们不再口述app中不同cell的状态(例如,哪些cell是展开的,是否可以展开cell,由代码决定某个cell是可见的还是不可见的,等等)。所有的信息都在你刚才下载的plist文件中。

加载Cell属性

是时候写一些代码了,虽然通过使用属性减少了很多代码量,还是需要往项目中添加一些代码。现在描述cell的plist文件已经存在了,首先我们必须通过代码来把内容加载为array数组。数组会在下一节中用作tableview的datasources。打开项目中的ViewController.swift 文件,在类的顶部声明下面的属性:

var cellDescriptors: NSMutableArray!

这个array数组将包含从plist加载的描述cell的字典。

下一步,实现一个新的自定义函数,来加载文件内容为array数组。我们把函数命名为loadCellDescriptors():

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

我们这里做的比较简单:确保plist文件在bundle的路径是有效的,然后通过加载文件内容初始化cellDescriptors数组

我们会在view显示(appear)之前和tableview配置好之后,才调用上面的函数。

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

如果你在上面代码片段最后一行调用打印命令,你会在控制台看到plist文件的内容。那代表了它们已经被成功地加载到内存中了。



一般地,我们这一节的内容应该完成了,但我们还会给下一节做必不可少的铺垫。或许你已经发现,并不是所有的cell都在app启动后是可见的,因为按不同的需求,它们可能是展开或者收起的。

从编程来说,行(row)的索引(index)不是固定的(我们写程序处理cell时的indexPath.row),因此,我们不能仅仅使用数据源的array数组的cell row来显示每一个cell。我们必须要有一个解决方案来提供可见行的索引(row index)。

所以,我们要实现一个新的函数,命名为getIndicesOfVisibleRows()。它的名字已经很清楚描述了它的目的:获得行索引(row index)对应的cell的值,而这里的cell指的是被标记为可见的cell。在我们实现之前,请再一次在类的头部声明:

var visibleRowsPerSection = [[Int]]()

这个二维数组会存储每一个section的可见的cell(第一维是section,第二维是row)。

现在我们来关注下这个新函数的实现。可能正如你所猜想,我们会遍历所有的cell描述,然后把所有“isVisible” 属性为YES的cell的索引(index)添加到上面的二维数组中。明显地,我们会处理一个嵌套循环,不过并不困难。这里是函数实现:

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数组中所有之前的内容,否则我们会在随后的调用中出现错误的数据。另外,实现的代码是比较简单的,我就不进一步解释。

第一次调用上面函数是在文件被加载之后(后面还会再次调用)。所以重新看下我们这一节实现的第一个函数,我们改成下面这样:

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

虽然tableview还不能工作,但我们触发了reload动作,所以我们可以确定在app启动后,是已经显示cell了。

显示Cells

每次启动app,都会加载cell的描述,我们现在开始处理并在tableview中显示cell。我们这节创建另外一个新的函数,来从cellDescriptors数组中返回正确的cell描述:

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

上面函数接收的参数是tableview需要处理的cell的索引值,它会返回一个包含对应cell的所有属性的字典。首要任务是由传递来的索引找到可见行的实际索引,我们只需要cell的section和row。由于我们此时还未处理tableview的代理方法,所以你不需要知道代码的全貌,但我必须提前说明:每一个section的所有row的数目等于每一个section的可见cell的数目。也就是说上面实现中的任何indexPath.row的值与visibleRowsPerSection对应存储的cell的index相等。

通过每个cell的行索引(index of the row),我们可以从cellDescriptors数组中取出描述cell的字典。注意第二维数组的下标是indexOfVisibleRow,而不是indexPath.row。使用后者会返回错误数据。

让我们确定tableview方法。首先,确定tableview的section的数量:

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”属性,需要根据它的值来确定行高。你可以在各自的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
}

我们再一次通过当前index的值获得正确的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,我们只设置textLabel的主标题和detailTextLabel的副标题的文字值。在我们的demo app中,identifier 为idCellNormal的cell是顶层可以展开和收起的cell。

对于包含textfield的cell,我们只要通过“primaryTitle”属性来设置它的placeholder文字。

对于包含switch control的cell,我们需要做两件事:首先确定需要显示的文字(在我们的例子中这是固定的,可以在CellDescriptor.plist文件中修改),然后我们设置switch的正确状态,根据描述中是否是“on”来决定。注意后面我们可以改变这个值。

还有identifier为“idCellValuePicker” 的cell。这些cell会提供选项列表,当一个选项被选中时,父cell会自动收起。这些cell的文字在plist文件中确定。

最后,还有一种情况是包含一个slider。这里我们只是从currentCellDescriptor字典获得当前的值,我们把他转换为float数值,然后把它赋值给slider,使其始终显示正确的值(当其可见时)。如果我们改变它的值,需要更新cell描述中对应的值。

在这个demo中,对于identifiers没有在上面明确规定的,什么都不需要做。不过,如果你想用别的方法来处理它们,随意添加或修改代码。

现在,你可以运行这个app,看看现在的效果。不要期望太多,现在你只能看到顶层的cell。不要忘记我们还没有实现扩展的功能,所以你点击它们,并不会发生什么。不过,不要灰心,到目前为止我们完成的部分已经可以完美运行。



扩展和收起

我认为这一部分是你最期待的,因为这篇博文的实际目标会在这一节实现。首先我们要实现最顶层的cell被点击后扩展或者收起,然后对应的子cell根据需求出现或者隐藏。

首先我们需要知道被点击cell所在的行(记住,不是真正的indexPath.row,而是可见cell的行),所以我们在下面的tableview代理方法中通过一个变量来确定它:

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

虽然不需要太多的代码来实现展开和收起,但我们还是一步步来,确保我能讲的彻底并且你能够明白。现在我们获得了被点击行真实的索引,我们必须检查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是否需要展开,我们就需要保存这个值到cell的描述中,换句话说,就是更新cellDescriptors数组。我们希望更新选中cell的“isExpanded”属性,它才能在以后的点击事件中正确地表现(在展开状态收起,在收起状态展开)。

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的输入数据以及用户交互了。首先,我们实现点击identifier为“idCellValuePicker”的cell的逻辑。在我们的demo的tableview中,“Preferences” section的cell以列表的形式列出了了喜爱的运动和颜色。虽然我已经提过它了,我认为复述一遍是个明智的选择,我们希望各自的顶层cell会收起并隐藏选项,同时选中的值在顶层cell中显示:

我选择从这种类型的cell开始讲起的实际原因是我们最后会继续用tableview的代理方法。我们会添加ELSE子句来处理不能展开的cell,然后,我们会检查被点击cell的identifier,如果identifier是“idCellValuePicker”,我们就得到了我们真正感兴趣的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" {
 
        }
    }
 
    getIndicesOfVisibleRows()
    tblExpandable.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
}

在if子句内部,我们要完成明确的目的:

我们要找到被点击的cell的父cell的行索引。实际上,我们从点击行往前找,找到的第一个顶层cell就是我们要找的。
我们会把选中的cell的值显示到父cell中。
我们要标记父cell为非展开状态。
我们要标记找到的顶层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,你会看到app是怎么处理选择喜爱的运动和颜色的。



转载请注明出处:http://blog.csdn.net/dolacmeng/article/details/50591914
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ios tableview