我的编程空间,编程开发者的网络收藏夹
学习永远不晚

如何在iOS Apps 创建展开式UITableView

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

如何在iOS Apps 创建展开式UITableView

顾名思义,一个展开式 UITableView 是这样一种表视图,它“允许”其单元格(cell)展开或者收起,显示或者隐藏,而在一般的表视图中,它们的单元格只能是显示的状态。当我们需要收集一些简单的数据或者根据用户的意愿显示/隐藏某些内容时,创建展开式 UITableView 是一种不错的选择。这样,我们就没有必要仅仅为了让用户输入一些数据就创建新的 View Controller,无论如何我们都只需要呆在同一个 View Controller 裡面,即当前的 View Controller 中。例如,通过展开式的 cell,我们显示或隐藏一个用于给用户输入信息的表单,在显示或隐藏这个表单时,根本不需要离开当前的 View Controller。

是否採用或者不採用展开式的 UITableView,完全取决于 App 的性质。但是,只要是通过子类化UITableViewCell和自定义xib文件的方式来定制 cell 的情况,App 的外观就不会是什麽问题。因此,归根结底,这只是一个需求问题。

在本教学中,我将演示一种创建展开式 UITableView 的简单有效的方法。注意,这并不是唯一方法。最好的方法要视 App 的需要而定,我的目的只是展示一种一般化的解决方案,它在大部份情形下都是适用的。因此,请进入下一部份,看看本教程最终将实现什麽样的效果。

示例 App

我们将创建一个只有一个 View Controller (其包含有一个 TableView)的 App,在这个 App 中我们将演示如何创建一个展开式表视图。我们将模拟一个允许用户输入的表单,为了演示,这个 TableView 将由 3 个 section 构成:

  1. Personal

  2. Preferences

  3. Work Experience

每个 section 都会包含展开式 cell,这些 cell 会隐藏/显示该 section 的其它 cell。尤其是位于 section 顶部的 cell (该 cell 能够展开或收起):

对于 “Personal” section:

  1. Full name: 这个 cell 用于显示用户的全名,当它处于展开状态时,它下面会多出两个子 cell,分别用于输入名和姓。

  2. Date of birth: 用于显示用户的生日。当它被展开后,会显示一个日期选择器(UIDatePicker),允许用户选择某个日期并提供一个按钮将用户选择的日期返回给它上面的 cell。

  3. Marital status: 显示用户的婚姻状态:已婚或单身。当它被展开后,会显示一个开关控件,允许用户设置他们的婚姻状态。

对于“Preferences”section:

  1. Favorite sport: 我们模拟了一个运动种类列表,用于提供给用户,让他们从中选择他们所喜爱的运动。当它被展开时,会列出 4 个运动种类,当用户选择某个子项,这个 cell 又自动会被收起。

  2. Favorite color: 和上面非常相似,只不过这裡显示了一个颜色列表供用户选择。

对于“Work Experience”section:

Level: 当这个 cell 被点击并展开后,将显示另一个包含有一个滑动条的 cell,允许用户设置他们的工作经验等级。这个级别用一个 0…10 之间的数字表示,我们只取这个数值的整数部份。

通过下面的动画会看得更清楚一些:

t45_7_expand_collapse

注意上面的例子,当我们展开 TableView 时会显示不同类型的 cell。这些 cell 都被包含在开始项目中了,你可以下载这些代码,项目已经完成了一些前期的准备工作。所有的 cell 都在单独的xib文件进行了必要的设计,同时它们的 Custom Class 也被指定为自定义的 UITableViewCell 子类(即 CustomCell):

t45_2_custom_class

在项目文件夹中,你将发现这些 cell 所使用的 xib 文件,包括:

t45_3_cell_list

它们作用分别如其名称所示,你也可以下载开始项目深入探究一番。

除了 cell,你还会发现一些已经写好的代码。虽然这些代码对于实现整个示例 App 的功能来说是必不可少的,但却不属于本教程的核心内容,因此我会跳过这些代码,仅仅是以现成的代码提供在开始项目中。缺失的其馀代码是本教程中我们最关心的内容,在接下来的教程中会以 step-by-step 的方式添加到项目中。

到此,你已经知道我们最后的目标是什麽了,接下来就让我们开始学习如何创建展开式的 UITableView。

描述单元格

我将在本教程中演示的、所有与展开式 UITableView 相关的实现和技术,都基于这样一种简单的思路:向 App 描述每个 cell 的细节。通过这种方式我们让 App 知道每一个 cell 到底是展开的还是收起的,是可见的还是隐藏的,每个 cell 的文字标籤显示什麽内容,等等。实际上,整体思路都基于将属性集进行编组,这些属性要麽描述了每个 cell 的属性,要麽包含了 cell 的某些数值,然后将这些属性告诉给 App,这样 App 才能正确地显示它们。

在本示例程序中,我创建和使用了一个属性集合,如下面所列。注意在真正的 App 中,你可能需要增加新的属性,或者对某些属性进行修改。不过,此时你只需要了解大致的情况就可以了。当然,只要你愿意你可以任意修改这些属性。我们所使用的属性列表(plist)是这样的:

  • isExpandable: 一个布尔值,标明 cell 是否能够展开或收起。在本教程中,这是我们非常关心的重要属性。

  • isExpanded: 一个布尔值,标明一个展开式 cell 是处于展开状态还是收起状态。顶层的 cell 默认是收起状态,因此这个值一开始都应该设成 NO。

  • isVisible: 顾名思义,标明这个 cell 是否应该显示到表格中。稍后这个属性会扮演一个重要的角色,因为我们会根据这个属性让表格中的某些 cell 得到显示。

  • value: 这个属性用于保存 UI 控件的值(例如,开关控件中的婚姻状态)。不是所有 cell 都会有这样的控件,因此大部份 cell 的这个属性值将保留为空。

  • primaryTitle: cell 主标题的显示文本,当这个属性不为空时,这个属性的值会显示到 cell 上。

  • secondaryTitle: cell 子标题的显示文本,或者 cell 第二个标籤的显示文本。

  • cellIdentifier: 自定义 cell 的ID,用于唯一识别当前 cell 的描述。这个 ID 不仅被 App 用于从缓存队列中弹出合适的 cell,而且还要根据这个 ID 对要显示的 cell 进行相应的处理并指定 cell 的高度。

  • additionalRows: 用于表示当一个展开式单元格被展开时,它下面包含了几个附属的 cell。

每个 cell 都会用上面的属性集进行描述。从 App 的角度,我们使用一个属性列表(plist)文件来保存它们会更加轻鬆。在这个plist文件中,我们会为每个 cell 使用一个上述属性集来进行描述,并适当地填充属性集中的属性值,这样,我们将最终获得所有 cell 的一个完整的描述,这个描述对于我们或 App 来说都很容易理解。同时我们并没有为之编写一行代码。很不错吧?

现在,我们在项目中新建一个 plist 文件,然后用适当的数据来填充它。当然你也可以从这裡下载现成的.plist 文件。下载后记得将它添加到我们的开始项目中。手动设置所有 cell 的属性会佔用大量空间,这是完全没有必要的,同时拷贝-粘贴或者输入所有属性值也是一件很繁琐的事情。
然后,让我们来讨论一下这个 plist 文件:

首先,你下载的这个文件的文件名叫做CellDescriptor.plist。它的根节点(root)是一个数组,其中的每个元素表示表格中的一个 section。也就是说这个plist文件的 root 数组中有三个元素,就跟我们想在表格中显示的 section 的数目一样。

每个 section 本身也是一个数组,数组中包含了该 section 中所包含的所有 cell 的描述。实际上,这些编组的属性集在这裡用字典来进行表示,每个字典代表了一个单独的 cell。这是一个 plist 文件的例子:

t45_4_plist_sample

现在,是时候来完整地回顾一下我们将要显示到表格中的 cell 的属性集和属性值了。很显然,拥有了这些 cell 描述之后,我们需要编写用于生成、管理 cell 的代码大大减少了。我们也不需要告诉 App 这些 cell 的各种状态(例如,哪个单元格是可展开的,App 应当让某个 cell 展开或收起,判断某个 cell 是可见的还是隐藏的等等)。所有的这些信息都包含在你下载的 plist 文件裡面。

加载单元格描述

终于可以编写代码了,虽然我们使用的单元格描述技术为我们节省了许多时间,但在这个项目中我们仍然免不了要编写代码。现在,我们已经有了用于描述 cell的 plist 文件了,接下来的事情自然是用代码将文件内容加载到一个数组对象中。这个数组对象将在后面充当表格的数据源。

打开开始项目中的 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 变量。

接下来就是调用这个方法,我们将在 TableView 已经配置好,并且视图即将显示之前(即在 TableView 已经创建并且还没有显示任何内容之前)调用这个方法:

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

    loadCellDescriptors()
}

如果在上述方法最后一行后添加 print(cellDescriptors) 一句,则运行 App 后你将看见 plist 文件的内容输出到了控制台中。这就说明文件已经成功加载到内存了。

t45_5_console_plist

通常,本节的内容应该到此结束,但这次有一点例外。我们还要补充一些对于下一节来说至关重要的内容。也许你想到了(尤其是当你检查了CellDescriptor.plist文件之后),当 App 启动后,并不是所有的 cell 都应该被显示。事实上,我们根本无法得知它们是否会在同时显示,因为它们是根据用户的要求来进行展开和收起的。

从编程的角度,这意味著 每个 cell 的行索引不应该是常量 (这就是为什麽我们在处理每个 cell 时,将 indexPath.row 用代码来生成)。同时,我们也不能用 cell 的行索引来遍历数据源数组并显示每个 cell。我们只能将可见的 cell 的行索引来提供给 App。如果将 cell 描述中标记为不可见的 cell 显示出来,这就大错特错了,那会导致 App 表现异常。

基于这样的原因,我们需要实现一个新方法,叫做 getIndicesOfVisibleRows()。这个方法的作用是显而易见的:它只返回那些标记为可见的 cell 的行索引。在实现这个方法之前,请在类的顶部增加如下属性:

var visibleRowsPerSection = [[Int]]()

这是一个二维数组,保存了所有 section 的可见的 cell 的行索引(一维用于表示 section,一维用于表示 cell)。

现在来实现这个方法。你也许想到了,我们会遍历所有 cell 描述并将 isVisible 属性为 true 的 cell 的行索引添加到二维数组中。当然,我们不得不用到嵌套循环,但这也不是什麽大问题。下面是这个方法的实现:

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 数组的内容清空了,否则连续多次调用这个方法之后数据就不正常了。接下来的实现就一目了然了,就不用我再多说了。

这个函数的第一次调用应该在从 plist 文件加载完 cell 描述之后(我们还会在后面多次调用这个函数)。因此,回到本节实现的第一个方法,将它修改为:

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

虽然 TableView 还不能正常工作,但我们已经在 App 一启动的时候就调用了它的刷新动作,这样就能保证在接下来的的步骤中显示正确的 cell。

显示单元格

每当 App 一启动,cell 描述就会被加载,接下来我们应当处理和显示表格中的 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 的 IndexPath 值(NSIndexPath),这个 cell 就是 TableView 当前正在处理的那个 cell。这个方法返回了一个字典对象,包含了该 cell 的全部属性值。在方法体中,首先需要根据给定的 IndexPath 去可见行数组中进行匹配,这个任务非常简单,我们只需要提供这个 cell 的 section 索引和行索引就可以了。现在你可能还有点摸不著头脑,因为我们还没有介绍 TableView 的委託方法,因此我必须先告诉你每个 section 的行数应当等于每 section 中可见 cell 的个数。也就是说,在上面的代码中,我们必须保证每个 indexPath.row 都能在 visibleRowsPerSection 中找到对应的可见的 cell 的索引。

拥有了每个 cell 的行索引之后,我们就来处理和“读取”从 cellDescriptors 数组获取的 cell 描述字典。注意,我们在指定这个数组的第二个下标索引时,使用的是 indexOfVisibleRow 而不是 indexPath.row。如果你使用了后者,得到的数据是不正确的。

实现了这个工具方法之后,我们后面就比较轻鬆了。接下来我们开始修改 ViewController 类中的 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 属性,用这个值去决定行的高度。关于每个 cell 的高度,我们可以打开相应的 cell 的 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
}

再一次,我们根据当前 IndexPath 来获取正确的单元格描述,并通过 cellIdentifier 属性从单元格重用队列中出列一个 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

    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,我们只是将 primaryTitlesecondaryTitle 的文本值赋给 textLabeldetailTextLabel 标籤。在本示例程序中,ID 为 idCellNormal 的 cell 实际上是位于 section 顶层的 cell,正是这个 cell 能够进行展开和收起的动作。

对于带有一个 TextField 的 cell,我们只是用单元格描述的 primaryTitle 属性去设置它的 placeholder 值。

对于带有一个开关控件的 cell,我们需要做两个动作:首先设置开关控件的显示文本(在 CellDescriptor.plist 文件中这是一个常量,当然你可以修改它),然后根据描述中的 value 属性是否为 true 来设置开关的 on 属性。注意,之后我们还会改变这个值。

还有一种 ID 为 idCellValuePicker 的 cell。这种 cell 表示它会提供一个选择列表,当我们选中列表中的某个选项,父 cell 将会自动收起,同时父 cell 的 textLabel 将做相应改变。

最后,是带有一个滑动条的 cell。我们仅仅是从 currentCellDescriptor 字典中取出当前的 value 值转换为一个 Float 数字,然后赋给滑动条,让它总是(在可见的时候)显示正确的值。稍后我们也会改变这个值以及与之对应的单元格描述。

对于 ID 不在上述 if 语句检查条件中的 cell,本示例 App 不会进行任何处理。当然,如果你不想採取这种方式,只需要修改上述代码并添加缺少的语句即可。

现在你可以先运行一下程序,看看运行的结果。当然不会看到更多的 cell,因为你只能看到顶层的 cell。别忘记我们还没有实现展开/收起功能,因此你点击 cell 也不会发生什麽。但你不用沮丧,因为你看到的这个结果已经表明我们刚才所做的一切已经生效了。

t45_6_top_level_cells

展开/收起

这部份内容可能是你最感兴趣的内容了,因为本教程的目标即将在这裡达成。首先我们将让我们的顶层 cell 在被点击之后展开/收起,同时子 cell 会适时地显示/隐藏。

首先需要知道被点到的 cell 位于哪一行(注意,并不是 indexPath.row,而是可见单元格的行索引),因此,我们需要在下面的 TableView 委託方法中将行索引保存到某个局部变量:

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

儘管让我们的 cell 展开/收起用不著多少代码,我仍然打算以 step-by-step 的方式进行讲解。一则这会使我的思路更加清晰,二则也方便你了解每个动作的真正含义。现在,我们拥有了被点击的 cell 的真正的行索引,我们可以用它来检索 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 是否该被展开或收起之后,我们需要将这个值存到单元格描述集合里,也就是说,我们要修改 cellDescriptors 数组。我们要修改的是选中的 cell 的 isExpanded 属性,这样它才会在再次被点击时表现正确(cell 的 isExpandedtrue ,则再次点击时它会收起,否则再次点击后它会展开)。

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 的 isVisible 属性。

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 属性,这导致整个可视 cell 的行数也改变了。因此,在我们刷新表格之前,我们还要让 App 重新计算可视 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")
        }
    }

    getIndicesOfVisibleRows()
    tblExpandable.reloadSections(NSIndexSet(index: indexPath.section), withRowAnimation: UITableViewRowAnimation.Fade)
}

你也看到了,我以动画的方式重新加载了被点击的 cell 的 section。当然,如果你不喜欢这种方式的话,你可以修改它。

运行 App 进行测试。连续点击顶层 cell,cell 随之展开和收起,虽然现在与子 cell 进行交互还不会发生任何事情,但这个结果看起来非常不错!

t45_7_expand_collapse

获取输入内容

从这裡开始,我们要将精力放在数据处理以及用户和子 cell 控件进行的交互上。对于 ID 为 idCellValuePicker 的 cell,我们将代码逻辑实现在当 ID 为 idCellValuePicker 的 cell 被点击的时候。对于本示例程序,在表格的 Preferences section中,有一些 cell 会罗列用户喜爱的运动和颜色。虽然我已经说过,但这裡我仍然要再说一次,就当是加强一下我们的记忆:当这类 cell 被点击时,我们想让对应的顶层 cell 收起(或者隐藏),所选中的值会显示到顶层 cell。

我之所以一开始就来处理这类 cell,是因为这是我们最后一次还需要和 TableView 的委託方法打交道。在这裡,我们会添加一个 else 分支来处理“不可展开的”cell,然后再对被点到的 cell 的 ID 值进行判断。如果 ID 值等于 idCellValuePicker,则进行相应的处理。

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 语句中,我们将分四个单独的步骤进行处理:

  1. 找出顶层 cell 的行索引,也就是被点击的 cell 的“父 cell”的行索引。实际上,我们只需要从这个 cell 的单元格描述向前搜索,所找到的第一个顶层 cell 就是我们要找的 cell(即第一个可展开的 cell)。

  2. 将选中的 cell 的显示文本赋给顶层 cell 的 textLabel 的 text 属性。

  3. 将顶层 cell 的 expanded 标记为 false

  4. 将顶层 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 会进行适当的响应:

t45_8_select_preferences

响应其它动作

CustomCell.swift 文件中,找到 CustomCellDelegate 协议,这裡我们已经对所有 required 方法进行了定义。在 ViewController 类中实现这些方法,我们将使 App 能够对其它动作进行响应。

打开 ViewController.swift 文件,声明遵循于该协议。在类声明的顶部加入该协议:

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellDelegate

接著,在 tableView:cellForRowAtIndexPath: 方法中,将每个 CustomCell 的委託指定为 ViewController 类。在方法体中,在方法即将返回之前,加入这句代码:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    ...

    cell.delegate = self

    return cell
}

好了,现在我们可以来实现委託方法了。第一个方法是当用户在 DatePicker 中选定一个日期后,我们将所选定的日期显示在与之对应的顶层 cell:

func dateWasSelected(selectedDateString: String) {
    let dateCellSection = 0
    let dateCellRow = 3

    cellDescriptors[dateCellSection][dateCellRow].setValue(selectedDateString, forKey: "primaryTitle")
    tblExpandable.reloadData()
}

在指定好适当的 section 索引和行索引后,我们将选定日期以字符串格式赋给对应 cell 的单元格描述。注意这个字符串是委託方法通过参数来传递给我们的。

然后是带有开关控件的 cell。当开关控件的值改变时,我们需要做两件事情:首先,将合适的值( Single 或 Married)传递给对应的顶层 cell,同时用开关控件的值更新 cellDescriptors 数组,这样当表格刷新后开关控件就会显示正确的状态。在下面的代码中,注意我们首先基于开关控件的状态来决定适当的值,然后将它们赋给对应的属性:

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

接下来是带有 TextField 的 cell。这裡,当用户的姓或者名输入有内容时,我们会动态组装用户的全名。因此,我们需要指明包含有 TextField 的 cell 的行索引,并根据索引的不同将字符串添加到全名中去(名在前,姓在后)。最后,我们需要更新顶层 cell 的文字,刷新表格,以使它反映出用户输入内容的改变:

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

最后,是带有滑动条的那个 cell,即“Work Experience” section 需要我们处理。当用户拖动滑块,我们需要同时完成两件事:用新的滑动条的数值修改顶层 cell 的文本内容(即“经验级别”),以及将滑动条的值保存到对应的 cell 描述中,使其在刷新表格后能够更新界面。

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 真的很有用,因为它让你直接在表格中处理以前必须创建新 View Controller 才能解决的问题。在教程的前半部份,我演示了一种创建展开式 TableView 的方法,它的主要特点是在一个属性列表文件(plist)中以属性集的方式来描述每个 cell。我还演示了在单元格显示、展开和选定时,如何用代码来处理单元格描述列表;此外,我还教你如何用用户输入的数据来修改这些 cell。虽然示例App 中模拟的表单在真正的 App 中也是可以用的,但在要把它当做一个完整的组件仍然需要我们考虑更多的事情(例如,将 cell 描述列表回写到文件中)。当然,这已经超出了本文的范围,我们只是想实现一个展开式的 TableView,让它的 cell 可以根据需要显示或隐藏而已,也就是我们最终的实现的那个 App。我希望你能从本教程中发现任何对你有用的东西。当然,你可以设法改进教程中的代码,或者根据需要进行调整。又到了不得不说再见的时候了,祝你开心,永远勇于尝试新的事物!

为便于参考,你可以从 GitHub 下载完整的 Xcode 项目.

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

如何在iOS Apps 创建展开式UITableView

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

如何在iOS Apps 创建展开式UITableView

顧名思義,一個展開式 UITableView 是這樣一種表視圖,它「允許」其單元格(cell)展開或者收起,顯示或者隱藏,而在一般的表視圖
2022-06-08

如何在 Rust中创建 PHP 扩展

这期内容当中小编将会给大家带来有关如何在 Rust中创建 PHP 扩展,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。可以编译的Rust代码到一个库里面,并写为它一些C的头文件,在C中为被调用的PHP做一个
2023-06-17

如何在Marketing Cloud里创建extension field扩展字段

首先在Marketing Cloud里找到创建扩展字段的tile入口,搜索关键字extension:
2023-06-03

如何在 Golang 中创建正则表达式?

如何在 golang 中创建正则表达式?使用 regexp.mustcompile 函数创建正则表达式对象。使用 regexp.matchstring 函数匹配字符串。使用 regexp.findstringsubmatch 函数查找子匹配
如何在 Golang 中创建正则表达式?
2024-05-13

如何在Linux命令行中创建和展示幻灯片

这篇文章主要介绍“如何在Linux命令行中创建和展示幻灯片”,在日常操作中,相信很多人在如何在Linux命令行中创建和展示幻灯片问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何在Linux命令行中创建和展示
2023-06-13

Slack如何在松弛中创建交互式按钮

本篇内容主要讲解“Slack如何在松弛中创建交互式按钮”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Slack如何在松弛中创建交互式按钮”吧!可以在template域文件的部分中定义按钮,例如:
2023-06-03

如何使用HTML和CSS创建一个响应式图标展示布局

随着移动设备的普及,响应式设计已经成为了网页设计的标准之一。在设计网页时,我们需要确保网页能够在不同尺寸的设备上展示良好,并且能够适应不同的屏幕大小。本文将介绍如何使用HTML和CSS来创建一个简单的响应式图标展示布局,并提供具体的代码示例
2023-10-21

如何使用HTML和CSS创建一个响应式图片展示布局

在现代Web设计中,响应式布局已经成为一种标配,因为越来越多的人使用不同尺寸和分辨率的设备浏览网页。在本文中,我们将介绍如何使用HTML和CSS来创建一个响应式图片展示布局。首先,我们需要一个HTML文件来构建页面结构。在该文件中,我们使用
2023-10-21

如何使用HTML和CSS创建一个响应式产品展示页面

随着移动设备的普及,响应式网页设计已成为现代网页设计的重要要求。产品展示页面作为企业或个人网站中的一个重要组成部分,同样需要具备响应式设计的特点。本文将详细介绍如何使用HTML和CSS创建一个响应式产品展示页面,并提供具体的代码示例。首先,
2023-10-21

如何使用HTML和CSS创建一个响应式视频展示布局

如何使用HTML和CSS创建一个响应式视频展示布局随着移动设备使用的普及,响应式设计已经成为现代网页设计中必不可少的一部分。在本文中,我们将学习如何使用HTML和CSS创建一个响应式视频展示布局。这个布局将适应不同大小的屏幕,并在任何设备上
2023-10-25

如何使用HTML和CSS创建一个响应式商品展示布局

概述:在现代社会中,越来越多的人通过互联网购物。为了吸引更多的消费者,网站开发者需要创建漂亮且响应式的商品展示页面。本文将向你介绍如何使用HTML和CSS来实现一个简单而有效的商品展示布局。步骤1: 设置HTML结构首先,我们需要设置一个基
2023-10-21

如何使用HTML和CSS创建一个响应式相册展示布局

相册展示布局是网站中常见的一种页面布局类型,可以用于展示图片、照片、图像等内容。在今天移动设备普及的环境下,一个好的相册展示布局需要具备响应式设计,能够适应不同尺寸的屏幕,并且在不同设备上具有良好的显示效果。本文将介绍如何使用HTML和CS
2023-10-21

电脑版淘宝在桌面如何创建快捷方式

这篇文章主要介绍“电脑版淘宝在桌面如何创建快捷方式”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“电脑版淘宝在桌面如何创建快捷方式”文章能帮助大家解决问题。电脑版淘宝在桌面创建快捷方式方法介绍:1、首
2023-07-01

如何使用HTML和CSS创建一个响应式图片展示墙布局

HTML和CSS是前端开发中常用的技术,可以用来创建各种布局效果。在本文中,我们将学习如何使用HTML和CSS来创建一个响应式的图片展示墙布局,让图片在不同设备上都能够自适应显示。首先,我们需要创建一个HTML文件,命名为index.htm
2023-10-21

Win8如何在桌面创建快捷方式(3种简单的方式)

很多网友在问Win8如何在桌面创建快捷方式,本次介绍3种简单的方式。下面小编就以联想的“联想远程软件服务”应用程序为例。告诉大家如何在在桌面创建快捷方式。1. 找到安装应用程序的安装路径,然后直接将应用程序的图标右击
2022-06-04

Android开发中如何为程序创建桌面快捷方式

这期内容当中小编将会给大家带来有关Android开发中如何为程序创建桌面快捷方式,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。具体如下:/*** 为程序创建桌面快捷方式 ,这样写,在程序卸载的时候,快捷方
2023-05-31

电脑如何创建运行快捷方式到开始菜单

这篇文章主要介绍了电脑如何创建运行快捷方式到开始菜单,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1、在桌面空白处点击右键,然后在打开的菜单项中新建 - 快捷方式;2、在创建
2023-06-27

如何使用HTML和CSS创建一个响应式图片集锦展示布局

在网页设计中,展示图片集锦是一种常见的需求。为了提供更好的用户体验,我们希望这些图片在不同设备上都能以适当的方式展示,而不仅仅是简单地缩放。这就需要设计一个响应式的图片集锦展示布局。在本文中,我们将介绍如何使用HTML和CSS来创建这样一个
2023-10-21

如何使用HTML和CSS创建一个响应式市场展示页面布局

市场展示页面是电子商务网站的重要组成部分,通过展示商品和服务,吸引用户的关注并促使其进行购买。在当今移动互联网时代,越来越多的用户通过手机和平板电脑访问网页,因此需要为市场展示页面创建一个响应式布局,以适应不同屏幕尺寸。本文将介绍如何使用H
2023-10-21

如何在Windows8的桌面模式下创建软件快捷方式方法

在Windows 8的桌面模式下,可以使用以下方法创建软件快捷方式:1. 找到想要创建快捷方式的软件程序的可执行文件(通常为一个 .exe 文件),这些文件通常位于“C:\Program Files”或“C:\Program Files (
2023-09-12

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录