zoukankan      html  css  js  c++  java
  • 在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.不要忘了我们还没有启动打开功能,所以你点击的时候不会发生任何事.但是,不要泄气,因为你所看到的意味着到目前为止我们所做的工作是完美的.


    未完待续~



    文/hrscy(简书作者)
    原文链接:http://www.jianshu.com/p/c7608a77b42e
    著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
  • 相关阅读:
    「日常训练」Single-use Stones (CFR476D2D)
    「日常训练」Greedy Arkady (CFR476D2C)
    「Haskell 学习」二 类型和函数(上)
    「学习记录」《数值分析》第二章计算实习题(Python语言)
    「日常训练」Alena And The Heater (CFR466D2D)
    Dubbo 消费者
    Dubbo 暴露服务
    Rpc
    git fail to push some refs....
    Spring Cloud (6)config 客户端配置 与GitHub通信
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/5895137.html
Copyright © 2011-2022 走看看