iOS UITableView 中的下拉列表

Drop-Down List in UITableView in iOS

如何在 iOS 中创建这种类型的表视图??

在这里,如果我们点击第 1 行 'Account',它会自动滚动更多行,显示在图像中。 如果我们再次点击帐户,则该视图将被隐藏。

通常我通过设置行高来实现。例如,您有两个带有下拉列表的菜单项:

  • 菜单 1
    • 项目 1.1
    • 项目 1.2
    • 项目 1.3
  • 菜单 2
    • 项目 2.1
    • 项目 2.2

因此您必须创建一个包含 2 个部分的 table 视图。第一部分包含 4 行(菜单 1 及其项目),第二部分包含 3 行(菜单 2 及其项目)。

您始终只为部分的第一行设置高度。如果用户点击第一行,您可以通过设置高度展开此部分行并重新加载此部分。

如果您不喜欢使用任何外部库,那么您可以制作 2 个自定义单元格。一个在扩展前显示,另一个在扩展后显示(具有不同的标识符)。当您单击该单元格时,检查该单元格是否已展开。如果不是,则使用扩展单元标识符,否则使用非扩展单元标识符。

这是制作展开的 table 视图单元格的最佳且干净的方法。

执行此操作的简单方法是使用 UITableView 部分 header 作为单元格-> 并将行数设置为 0,将 section.count 用于折叠和展开状态。

  • .This is TableViewSection Header, isExpand -> section.count else return0.

    -正常细胞

    -正常细胞

    -正常细胞

  • .This is TableViewSection Header, isExpand -> section.count else return0.

    -正常细胞

    -正常细胞

    -正常细胞

您需要一个可折叠的 TableView。为了实现这一点,在您的 TableView 中,您必须跟踪哪些部分折叠(收缩),哪些部分展开。为此,您需要维护一组展开部分的索引,或一个布尔数组,其中每个索引的值指示相应部分是否展开。在为特定行分配高度时检查特定索引处的值。查看 this link 以获得更多帮助。

您可以了解分段 TableViews here

Github 上提供了第三方库,可以让您远离喧嚣。看一下 CollapsableTableView or CollapsableTable-Swift

您可以将帐户作为一个单元格,点击它会展开以显示三个按钮("Profile"、"Activate Account"、"Change Password"),但这会产生一个问题:在每个按钮周围点击这三个按钮将算作 "user selected the Account cell" 并触发 -tableView:didSelectRowAtIndexPath:,结果是单元格的 expand/collapse。

或者您可以将每个隐藏选项("Profile"、"Activate Account"、"Change Password")设为单独的 table 视图单元格。但我不知道如何将三个单元格 作为一个整体 展开和收缩进行动画处理(而不是分别从零高度展开到完全展开)。

所以,也许最好的解决办法是:

  1. even 个单元格(索引:0、2、4...)来完成 "Menu title" 和 "Toggle menu open/close" 的角色(朝向下面描述的相关奇数单元格)。
  2. 交错(最初折叠的)"menu body" 个单元格,每个单元格每个选项有一个按钮(例如 "Profile"、"Activate Account"、"Change Password"),垂直布局,在奇数索引(1、3、5 ...)。使用 target-action 响应用户选择每个 option/button.
  3. 实现table视图委托方法,以便只有偶数单元格(菜单headers)被选择table,并将选择逻辑实现到expand/collapse相应的奇数单元格(在 -tableView:didSelectRowAtIndexPath: 内)。例如,选择索引 0 ("Account") 处的单元格会导致 expanding/collapsing 索引 1 处的单元格(带有选项 "Profile"、"Activate Account"、"Change Password" 的菜单) .

这不是 UITableView 最优雅的用法,但可以完成工作。

您可以轻松地将单元格设置为看起来像 header,并设置 tableView: didSelectRowAtIndexPath 以手动展开或折叠它所在的部分。 如果我存储一个布尔值数组,对应于每个部分的 "expended" 值。然后,您可以让每个自定义 header 行上的 tableView:didSelectRowAtIndexPath 切换此值,然后重新加载该特定部分。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.row == 0) {
        ///it's the first row of any section so it would be your custom section header

        ///put in your code to toggle your boolean value here
        mybooleans[indexPath.section] = !mybooleans[indexPath.section];

        ///reload this section
        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade];
    }
}

然后您将设置您的号码 numberOfRowsInSection 以检查 mybooleans 值和 return 1(如果该部分未展开)或 1+ 中的项目数该部分(如果已展开)。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    if (mybooleans[section]) {
        ///we want the number of people plus the header cell
        return [self numberOfPeopleInGroup:section] + 1;
    } else {
        ///we just want the header cell
        return 1;
    }
}

您还必须将 cellForRowAtIndexPath 更新为 return 任何 section 中第一行的自定义 header 单元格。

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section 是提供 "own custom header" 的更好方式,因为这正是它的设计目的。

有关详细信息,请参阅此 Answer or this PKCollapsingTableViewSections

此外,您可以使用 setIndentationLevel 获得此类表格视图。此示例请参考此 DemoCode。我认为这是 Drop-Down tableviews 的最佳解决方案。

如果你想做一个简单的header和单元格下拉,那么请参考STCollapseTableView

希望,这就是您要找的。有任何疑虑,请回复我。 :)

如果通过 table 视图单元格,则实现这一点的更简单和最自然的方法。没有展开的单元格视图,没有 headers 部分,简单的单元格(毕竟我们在 table 视图中)。

设计如下:

  • 使用 MVVM 方法,创建一个 CollapsableViewModel class 来保存配置单元格所需的信息:标签、图像
  • 除了上面那个,还有两个额外的字段:children,它是一个CollapsableViewModel objects的数组,和isCollapsed,它保存着状态下拉菜单
  • 视图控制器包含对 CollapsableViewModel 层次结构的引用,以及包含将在屏幕上呈现的视图模型的平面列表(displayedRows 属性 )
  • 每当点击一个单元格时,检查它是否有 children,并通过 insertRowsAtIndexPaths()displayedRows 和 table 视图中添加或删除行和 deleteRowsAtIndexPaths() 个函数。

Swift代码如下(注意代码只使用了view model的label 属性,保持简洁):

import UIKit

class CollapsableViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsableViewModel]
    var isCollapsed: Bool
    
    init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed
    }
}

class CollapsableTableViewController: UITableViewController {
    let data = [
        CollapsableViewModel(label: "Account", image: nil, children: [
            CollapsableViewModel(label: "Profile"),
            CollapsableViewModel(label: "Activate account"),
            CollapsableViewModel(label: "Change password")]),
        CollapsableViewModel(label: "Group"),
        CollapsableViewModel(label: "Events", image: nil, children: [
            CollapsableViewModel(label: "Nearby"),
            CollapsableViewModel(label: "Global"),
            ]),
        CollapsableViewModel(label: "Deals"),
    ]
    
    var displayedRows: [CollapsableViewModel] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        displayedRows = data
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return displayedRows.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier") ?? UITableViewCell()
        let viewModel = displayedRows[indexPath.row]
        cell.textLabel!.text = viewModel.label
        return cell
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
        let viewModel = displayedRows[indexPath.row]
        if viewModel.children.count > 0 {
            let range = indexPath.row+1...indexPath.row+viewModel.children.count
            let indexPaths = range.map { IndexPath(row: [=10=], section: indexPath.section) }
            tableView.beginUpdates()
            if viewModel.isCollapsed {
                displayedRows.insert(contentsOf: viewModel.children, at: indexPath.row + 1)
                tableView.insertRows(at: indexPaths, with: .automatic)
            } else {
                displayedRows.removeSubrange(range)
                tableView.deleteRows(at: indexPaths, with: .automatic)
            }
            tableView.endUpdates()
        }
        viewModel.isCollapsed = !viewModel.isCollapsed
    }
}

Objective-C 版本很容易翻译,我添加 Swift 版本只是因为它更短且更易读。

通过几处小改动,代码可用于生成多级下拉列表。

编辑

人们问我关于分隔符的问题,这可以通过添加一个自定义的 class CollapsibleTableViewCell 来实现,它配置了一个视图模型(最后,将单元格配置逻辑从控制器移动到哪里它属于 - 细胞)。仅部分单元格的分隔符逻辑归功于回答 this SO 问题的人。

首先,更新模型,添加一个 needsSeparator 属性 告诉 table 视图单元格是否呈现分隔符:

class CollapsableViewModel {
    let label: String
    let image: UIImage?
    let children: [CollapsableViewModel]
    var isCollapsed: Bool
    var needsSeparator: Bool = true
    
    init(label: String, image: UIImage? = nil, children: [CollapsableViewModel] = [], isCollapsed: Bool = true) {
        self.label = label
        self.image = image
        self.children = children
        self.isCollapsed = isCollapsed
        
        for child in self.children {
            child.needsSeparator = false
        }
        self.children.last?.needsSeparator = true
    }
}

然后,添加单元格 class:

class CollapsibleTableViewCell: UITableViewCell {
    let separator = UIView(frame: .zero)
    
    func configure(withViewModel viewModel: CollapsableViewModel) {
        self.textLabel?.text = viewModel.label
        if(viewModel.needsSeparator) {
            separator.backgroundColor = .gray
            contentView.addSubview(separator)
        } else {
            separator.removeFromSuperview()
        }
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        let separatorHeight = 1 / UIScreen.main.scale
        separator.frame = CGRect(x: separatorInset.left,
                                 y: contentView.bounds.height - separatorHeight,
                                 width: contentView.bounds.width-separatorInset.left-separatorInset.right,
                                 height: separatorHeight)
    }
}

cellForRowAtIndexPath则需要修改为return这种单元格:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = (tableView.dequeueReusableCell(withIdentifier: "CollapsibleTableViewCell") as? CollapsibleTableViewCell) ?? CollapsibleTableViewCell(style: .default, reuseIdentifier: "CollapsibleTableViewCell")
        cell.configure(withViewModel: displayedRows[indexPath.row])
        return cell
    }

最后一步,删除默认的 table 视图单元格分隔符 - 来自 xib 或代码 (tableView.separatorStyle = .none)。

根据@sticker 的回答,您可以绑定运行时

objc_setAssociatedObject

用于章节索引,并使用他的逻辑。在 header 视图上使用触屏时,您可以获得部分索引

objc_getAssociatedObject.

UITapGestureRecognizer *singleTapRecogniser = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(gestureHandler:)] autorelease];
[singleTapRecogniser setDelegate:self];
singleTapRecogniser.numberOfTouchesRequired = 1;
singleTapRecogniser.numberOfTapsRequired = 1;   
[sectionHeaderView addGestureRecognizer:singleTapRecogniser];

如果你想要任何第三方库,那么你可以尝试this解决方案。

这是一个基于 MVC 的解决方案。

Create a Model class ClsMenuGroup for your Sections

class ClsMenuGroup: NSObject {

    // We can also add Menu group's name and other details here.
    var isSelected:Bool = false
    var arrMenus:[ClsMenu]!
}

Create a Model class ClsMenu for your Rows

class ClsMenu: NSObject {

    var strMenuTitle:String!
    var strImageNameSuffix:String!
    var objSelector:Selector!   // This is the selector method which will be called when this menu is selected.
    var isSelected:Bool = false

    init(pstrTitle:String, pstrImageName:String, pactionMehod:Selector) {

        strMenuTitle = pstrTitle
        strImageNameSuffix = pstrImageName
        objSelector = pactionMehod
    }
}

Create groups array in your ViewController

 class YourViewController: UIViewController, UITableViewDelegate {

    @IBOutlet var tblMenu: UITableView!
    var objTableDataSource:HDTableDataSource!
    var arrMenuGroups:[AnyObject]!

    // MARK: - View Lifecycle

    override func viewDidLoad() {
        super.viewDidLoad()
        if arrMenuGroups == nil {
            arrMenuGroups = Array()
        }

        let objMenuGroup = ClsMenuGroup()
        objMenuGroup.arrMenus = Array()

        var objMenu = ClsMenu(pstrTitle: "Manu1", pstrImageName: "Manu1.png", pactionMehod: "menuAction1")
        objMenuGroup.arrMenus.append(objMenu)

        objMenu = ClsMenu(pstrTitle: "Menu2", pstrImageName: "Menu2.png", pactionMehod: "menuAction2")
        objMenuGroup.arrMenus.append(objMenu)

        arrMenuGroups.append(objMenuGroup)
        configureTable()
    }


    func configureTable(){

        objTableDataSource = HDTableDataSource(items: nil, cellIdentifier: "SideMenuCell", configureCellBlock: { (cell, item, indexPath) -> Void in

            let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
            let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]
            let objCell:YourCell = cell as! YourCell

            objCell.configureCell(objTmpMenu)  // This method sets the IBOutlets of cell in YourCell.m file.
        })

        objTableDataSource.sectionItemBlock = {(objSection:AnyObject!) -> [AnyObject]! in

            let objMenuGroup = objSection as! ClsMenuGroup
            return (objMenuGroup.isSelected == true) ? objMenuGroup.arrMenus : 0
        }

        objTableDataSource.arrSections = self.arrMenuGroups
        tblMenu.dataSource = objTableDataSource
        tblMenu.reloadData()
    }

    // MARK: - Tableview Delegate

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

        let objTmpGroup = self.arrMenuGroups[indexPath.section] as! ClsMenuGroup
        let objTmpMenu = objTmpGroup.arrMenus[indexPath.row]

        if objTmpMenu.objSelector != nil && self.respondsToSelector(objTmpMenu.objSelector) == true {
            self.performSelector(objTmpMenu.objSelector)  // Call the method for the selected menu.
        }

        tableView.reloadData()
    }

    func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

        let arrViews:[AnyObject] = NSBundle.mainBundle().loadNibNamed("YourCustomSectionView", owner: self, options: nil)
        let objHeaderView = arrViews[0] as! UIView
        objHeaderView.sectionToggleBlock = {(objSection:AnyObject!) -> Void in

            let objMenuGroup = objSection as! ClsMenuGroup
            objMenuGroup.isSelected = !objMenuGroup.isSelected
            tableView.reloadData()
        }
        return objHeaderView
    }

    // MARK: - Menu methods

    func menuAction1(){

    }

    func menuAction2(){

    }
}

我已经使用 HDTableDataSource 代替了 Tableview 的数据源方法。您可以从 Github.

中找到 HDTableDataSource 的示例

Advantages of above code is

  1. You can anytime change the order of any menu or section or interchange menu and section, without changing other functions.
  2. You will not need to add long code of else if ladder in your tableview's delegate methods
  3. You can specify icon, title or other attribute for your menu item separately like adding badge count, changing selected menu's color etc.
  4. You may also use multiple cells or sections by applying minor changes to existing code
@interface TestTableViewController ()
{
    BOOL showMenu;
}

@implementation TestTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // Uncomment the following line to preserve selection between presentations.
    // self.clearsSelectionOnViewWillAppear = NO;

    // Uncomment the following line to display an Edit button in the navigation bar for this view controller.
    // self.navigationItem.rightBarButtonItem = self.editButtonItem;
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"accountMenu"];
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"accountSubMenu"];
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 2;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (section == 0) {
        // Account Menu
        return 1;
    }
    if (showMenu) {
        // Profile/Private Account/Change Password
        return 3;
    }
    // Hidden Account Menu
    return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell;

    if (indexPath.section == 0) {
        cell = [tableView dequeueReusableCellWithIdentifier:@"accountMenu" forIndexPath:indexPath];
        cell.textLabel.text = @"Account";
    }
    else
    {
        cell = [tableView dequeueReusableCellWithIdentifier:@"accountSubMenu" forIndexPath:indexPath];
        switch (indexPath.row) {
            case 0:
                cell.textLabel.text = @"Profile";
                break;
            case 1:
                cell.textLabel.text = @"Private Account";
                break;
            case 2:
                cell.textLabel.text = @"Change Password";
                break;

            default:
                break;
        }
    }


    return cell;
}

-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (indexPath.section == 0) {
        // Click on Account Menu
        showMenu = !showMenu;
        [tableView reloadSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationAutomatic];
    }
}

希望对您有所帮助:)

tree-view 没有像 iOS 框架中的视图那样的内置控件 - UIKit。正如其他用户指出的那样,可能最简单的解决方案(不使用任何外部库)是向 UITableView 的委托和数据源添加一些自定义逻辑以模仿所需的行为。

幸运的是,有一些开源库允许您实现所需的树视图,如视图,而无需担心 expand/collapse 操作的细节。 iOS 平台有 couple of them 可用。在大多数情况下,这些库只是包装 UITableView 并为您提供 programmer-friendly 接口,使您可以专注于您的问题,而不是树视图的实现细节。

就我个人而言,我是 RATreeView library which purpose it to minimalize the cost needed to create tree view like views on iOS. You can check out example projects (available in Objective-c and Swift) 的作者,负责检查此控件的工作方式和行为方式。使用我的控件,创建你想要的视图真的很简单:

  1. DataObject 结构将用于保存有关树视图节点的信息——它将负责保存有关单元格标题、其图像(如果单元格有图像)及其 children(如果单元格有 children)。
class DataObject
{
    let name : String
    let imageURL : NSURL?
    private(set) var children : [DataObject]

    init(name : String, imageURL : NSURL?, children: [DataObject]) {
        self.name = name
        self.imageURL = imageURL
        self.children = children
    }

    convenience init(name : String) {
        self.init(name: name, imageURL: nil, children: [DataObject]())
    }
}
  1. 我们将声明协议 TreeTableViewCell 并实现两个符合该协议的单元格。其中一个单元格将用于显示根项,另一个单元格将用于显示 children 个根项。
protocol TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool)
}

class ChildTreeTableViewCell : UITableViewCell, TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
       //implementation goes here 
    }
}

class RootTreeTableViewCell : UITableViewCell, TreeTableViewCell {
    func setup(withTitle title: String, imageURL: NSURL?, isExpanded: Bool) {
       //implementation goes here
    }
}
  1. 在视图控制器 (MVC) 或视图模型 (MVVM) 中,我们定义了负责备份树视图的数据结构。
let profileDataObject = DataObject(name: "Profile")
let privateAccountDataObject = DataObject(name: "Private Account")
let changePasswordDataObject = DataObject(name: "Change Password")
let accountDataObject = DataObject(name: "Account", imageURL: NSURL(string: "AccountImage"), children: [profileDataObject, privateAccountDataObject, changePasswordDataObject])

let groupDataObject = DataObject(name: "Group", imageURL: NSURL(string: "GroupImage"), children: [])
let eventDataObject = DataObject(name: "Event", imageURL: NSURL(string: "EventImage"), children: [])
let dealsDataObject = DataObject(name: "Deals", imageURL: NSURL(string: "DealsImage"), children: [])

data = [accountDataObject, groupDataObject, eventDataObject, dealsDataObject]
  1. 接下来我们需要从 RATreeView 的数据源中实现几个方法。
func treeView(treeView: RATreeView, numberOfChildrenOfItem item: AnyObject?) -> Int {
    if let item = item as? DataObject {
        return item.children.count //return number of children of specified item
    } else {
        return self.data.count //return number of top level items here
    }
}

func treeView(treeView: RATreeView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
    if let item = item as? DataObject {
        return item.children[index] //we return child of specified item here (using provided `index` variable)
    } else {
        return data[index] as AnyObject //we return root item here (using provided `index` variable)
    }
}

func treeView(treeView: RATreeView, cellForItem item: AnyObject?) -> UITableViewCell {
    let cellIdentifier = item ? “TreeTableViewChildCell” : “TreeTableViewCellRootCell”
    let cell = treeView.dequeueReusableCellWithIdentifier(cellIdentifier) as! TreeTableViewCell

    //TreeTableViewCell is a protocol which is implemented by two kinds of
    //cells - the one responsible for root items in the tree view and another 
    //one responsible for children. As we use protocol we don't care
    //which one is truly being used here. Both of them can be
    //configured using data from `DataItem` object.

    let item = item as! DataObject
    let isExpanded = treeView.isCellForItemExpanded(item) //this variable can be used to adjust look of the cell by determining whether cell is expanded or not

    cell.setup(withTitle: item.name, imageURL: item.imageURL, expanded: isExpanded)

    return cell
}

请注意,使用我的库您不必关心单元格的展开和折叠 - 它由 RATreeView 处理。您只需对用于配置单元格的数据负责 - 其余部分由控件本身处理。

我喜欢@Cristik 的解决方案,前段时间我遇到了同样的问题,我的解决方案遵循了相同的原则;所以这就是我根据我的要求提出的建议:

  1. 为了更通用,table 的项目不应该继承自专门用于扩展功能的 class,而是应该有一个协议来定义必要的属性

  2. 我们可以扩展的关卡数量应该没有限制。所以table可以有option,sub option,sub sub option等

  3. table 视图应使用任何常用动画显示或隐藏单元格(无 reloadData

  4. 展开动作不必附加到用户选择单元格,例如单元格可以有一个 UISwitch

简化版的实现(https://github.com/JuanjoArreola/ExpandableCells)如下:

首先是协议:

protocol CellDescriptor: class {
    var count: Int { get }
    var identifier: String! { get }
}

不可展开的单元格总是计数为 1:

extension CellDescriptor {
    var count: Int { return 1 }
}

然后是可扩展单元协议:

protocol ExpandableCellDescriptor: CellDescriptor {
    var active: Bool { get set }
    var children: [CellDescriptor] { get set }

    subscript(index: Int) -> CellDescriptor? { get }
    func indexOf(cellDescriptor: CellDescriptor) -> Int?
}

关于 swift 的一个很酷的事情是我们可以在协议扩展中编写一些实现,所有符合的 classes 都可以使用默认实现,所以我们可以编写 count subscriptindexOf 实现以及其他一些有用的功能,例如:

extension ExpandableCellDescriptor {
    var count: Int {
        var total = 1
        if active {
            children.forEach({ total += [=13=].count })
        }
        return total
    }

    var countIfActive: Int {
        ...
    }

    subscript(index: Int) -> CellDescriptor? {
        ...
    }

    func indexOf(cellDescriptor: CellDescriptor) -> Int? {
        ...
    }

    func append(cellDescriptor: CellDescriptor) {
        children.append(cellDescriptor)
    }
}

完整的实现在文件中 CellDescriptor.swift

此外,在同一个文件中,有一个名为 CellDescriptionArray 的 class 实现了 ExpandableCellDescriptor 并且它本身不显示单元格

现在,任何 class 都可以符合以前的协议,而无需从特定的 class 继承,对于 github 中的示例代码,我创建了几个 classes:OptionExpandableOption,这就是 ExpandableOption 的样子:

class ExpandableOption: ExpandableCellDescriptor {

    var delegate: ExpandableCellDelegate?

    var identifier: String!
    var active: Bool = false {
        didSet {
            delegate?.expandableCell(self, didChangeActive: active)
        }
    }

    var children: [CellDescriptor] = []
    var title: String?
}

这是 UITableViewCell 子class之一:

class SwitchTableViewCell: UITableViewCell, CellDescrptionConfigurable {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var switchControl: UISwitch!

    var cellDescription: CellDescriptor! {
        didSet {
            if let option = cellDescription as? ExpandableOption {
                titleLabel.text = option.title
                switchControl.on = option.active
            }
        }
    }

    @IBAction func activeChanged(sender: UISwitch) {
       let expandableCellDescriptor = cellDescription as! ExpandableCellDescriptor
       expandableCellDescriptor.active = sender.on
    }
}

请注意,您可以配置单元格,它是 class 您喜欢的方式,您可以添加图像、标签、开关等;没有限制,也没有改变所需的协议。

最后在 TableViewController 中我们创建选项树:

var options = CellDescriptionArray()

override func viewDidLoad() {
   super.viewDidLoad()

   let account = ExpandableOption(identifier: "ExpandableCell", title: "Account")
   let profile = Option(identifier: "SimpleCell", title: "Profile")
   let isPublic = ExpandableOption(identifier: "SwitchCell", title: "Public")
   let caption = Option(identifier: "SimpleCell", title: "Anyone can see this account")
   isPublic.append(caption)
   account.append(profile)
   account.append(isPublic)
   options.append(account)

   let group = ExpandableOption(identifier: "ExpandableCell", title: "Group")
   group.append(Option(identifier: "SimpleCell", title: "Group Settings"))
   options.append(group)
   ...
}

其余的实现现在非常简单:

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
   return options.count
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
   let option = options[indexPath.row]!
   let cell = tableView.dequeueReusableCellWithIdentifier(option.identifier, forIndexPath: indexPath)

   (cell as! CellDescrptionConfigurable).cellDescription = option
   (option as? ExpandCellInformer)?.delegate = self
   return cell
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    guard let option = options[indexPath.row] else { return }
    guard let expandableOption = option as? ExpandableOption else { return }
    if expandableOption.identifier == "ExpandableCell" {
        expandableOption.active = !expandableOption.active
    }
}

func expandableCell(expandableCell: ExpandableCellDescriptor, didChangeActive active: Bool) {
    guard let index = options.indexOf(expandableCell) else { return }
    var indexPaths = [NSIndexPath]()
    for row in 1..<expandableCell.countIfActive {
        indexPaths.append(NSIndexPath(forRow: index + row, inSection: 0))
    }
    if active {
        tableView.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
    } else {
        tableView.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.Fade)
    }
}

它可能看起来很多代码,但大部分只写一次,正确绘制 table 视图所需的大部分信息都存在于 CellDescriptor.swift 文件中,单元格配置代码存在于 UITableViewCell subclasses 中,而 TableViewController 本身的代码相对较少。

希望对您有所帮助。