在iOS中怎样创建可展开的Table View?(下)

接上篇:在iOS中怎样创建可展开的Table View?(上)

展开和合拢

我猜这部分可能是你最期望的了,因为本次教程的目标将会在在部分实现.第一次我们设法让顶层的cell,在它们点击的时候展开或者合拢.以及显示或者隐藏合适的子cell.

开始我们需要知道点击行的索引(记住,不是实际的indexPath.row)而是可见cell的行索引,所以我们将会开始在下面的tableView代理方法里给它分配一个局部变量:

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

虽然为了让我们的cell展开或合拢并没有太多代码,但是我们要将一步一步地走.现在我们已经有了点击行的真正索引,我们必须要检查cellDescriptors数组,指定的cell是否展开.某个cell是可展开的,但是现在还没有展开,那么我们要标示(我们将使用一个flag标记)那个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数组.我们想更新选中行的"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的描述.这个属性必须根据上面的flag来改变,所以的添加的不可见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 {
            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"的值,那意味着,可见行的总数已经改变了.所以,在我们重新加载tableView之前,我们需要app找到可见行的索引值:

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的组,但是如果你不喜欢这种方式,你可以修改.

现在运行app.顶层的cell可以在点击之后展开或是合拢了,尽管点击子cell还没有发生任何改变,但结果令人印象深刻.

拾取值

从现在开始我们可完全专注于处理输入数据和与用户交互的子cell的控制了.我们通过实现逻辑,当cell的"idCellValuePicker"标识符被点击的时候,将会才去行动.在我们的demo里,那是在tableView的"Preferences"组里,列出了最喜欢的运动和颜色的cell.尽管我已经提到它了,我想那是一个好的想法,刷新我们的内存,并且再说一遍,当一个cell被点击的时候,我们希望各自的顶层cell合拢(以及隐藏选项).

真正的原因是因为我选择开始处理cell的类型,我继续在tableView的代理方法里修改,在里面,我将添加一个else来处理没有展开cell的情况,然后我们将检查点击cell的标识符的值.如果标识符等于"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 case里,我们将执行诗歌不同的任务:

  1. 我们要找到那个被点击的顶级cell的行索引.事实上,我们会执行一个搜索指向cell描述符的起始位置,以及第一个顶层cell被发现是可展开的才是我们想要的.
  2. 我们设置了显示选中cell的值,作为顶层cell的textLabel的文本内容.
  3. 当顶层cell不是展开的时候,我们做了标记.
  4. 我们会把所有的子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"属性,因此可见行的数量改变了.

如果你现在运行app,你将会看到当选中一个喜欢的运动或颜色后,app的响应.

响应其他用户操作

CustomCell.swift文件中,你可以发现CustomCellDelegate协议的所需的代理方法都已经被声明.通过在ViewController类里实现它们我们需要设法让app在所有的其他缺少用户操作的活动得到响应.

让我们再一次修改ViewController.swift文件,采用上面的协议.移到类的顶部,添加一个协议,如下:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellDelegate

接下来,在tableView:cellForRowAtIndexPath: 函数里,我们必须让ViewController类实现自定义cell的代理方法.看这儿:

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

好极了,现在我们可以开始实现得里函数了.我们会开始实现在日期选择器里显示选中的日期到顶级cell上:

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

一旦我们指定组和行的个数,我们直接将选中的日期设置为了一个字符串.注意,这个字符串在代理方法中是一个字符串.

接下来,让我们处理在cell的开关吧.当改变了开关的值,我们需要做两件事情:首先,设置合适的值("Single"或"Married"),显示到对应的顶级cell上;之后,在cellDescriptors数组里更新开关的值,那样当tableView刷新的时候,它就会有合适的状态.在下面的代码片段里,你将会注意到我们首先确定基于开关状态合适的值,然后我们分配给他们各自的属性:

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

下面是带有文本框的cell.我们要动态地组成全名,一旦姓和名都输入了.我们需要指定包含文本框的cell的索引.最后我们会在顶级cell更新显示的文本(全名),并且会刷新tableView,如下代码:

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

最后,是控制"Work Experience"组的滑块控件的cell.当用户改变了滑块的值,我们想要两件事情同时发生:用滑块的值更新顶级cell文本(在app中就是"experience level")同时存储滑块的值:

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在某些时候真的很有用,从麻烦当中创建新的视图控制器,可以用这种tableView来处理,它可以为app节省时间.在这次教程先前的部分,我向你提出了一种创建可展开tableView的方法,主要的特点就是在一个plist文件中,所有cell的描述都使用具体的属性.我向你展示了当cell显示,打开或是选中的时候,如何使用代码处理cell的描述列表;此外,我给了你一个方法通过用户输入数据来直接更新它.尽管这个示例app的表单是假的,但是也是可以存在真实的app中的.在它代表一个完整组件之前,仍然有很多事情需要做.(例如,将cell描述列表保存到文件),然而,那已经超出了我们的目标;我们最开始所想的是实现一个可展开的tableView,根据需求显示或隐藏cell,以及我们最终所做的.我相信,在这篇教程中你会找到左右有用的信息.肯定你会发现方法来改进给定的代码,或者根据你的需要来调整它.是时候说再见了,玩的开心,永远不要停止尝试!


供参考,你可以在GitHub下载完整的代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容