iOS TableView 多级目录的实现

在 iOS 开发中,UITableView 是一个非常常用的组件,用于展示一系列数据。在某些情况下,我们需要实现多级目录(也称为嵌套表格),例如在展示文件夹结构、产品分类等场景。本文将介绍如何使用 UITableView 创建多级目录,包括必要的代码示例。

目录结构

为了更清晰的展示多级目录,我们可以将它抽象为一棵树形结构。以下是一个简单的文件夹结构示例:

  • 文件夹 A
    • 文件夹 A1
      • 文件 A1-a
      • 文件 A1-b
    • 文件夹 A2
  • 文件夹 B
    • 文件夹 B1

我们可以使用 ER 图表示这个目录结构:

erDiagram
    FILE_FOLDER {
        string name
    }
    FILE_FOLDER ||--o{ FILE_FOLDER : contains

这里的 FILE_FOLDER 表示文件夹,具有一个 name 属性,且一个文件夹可以包含多个子文件夹。

数据模型

首先,我们需要定义我们的数据模型。为了实现多级目录,我们可以创建一个 FileFolder 类,表示文件夹,并包含子文件夹的数组。

class FileFolder {
    var name: String
    var subfolders: [FileFolder]
    
    init(name: String) {
        self.name = name
        self.subfolders = []
    }
}

数据源

接下来,让我们定义一些示例数据,用于填充我们的 UITableView。这里,我们将创建一个包含文件夹 A 和 B 的根文件夹。

var rootFolders: [FileFolder] = {
    let folderA = FileFolder(name: "文件夹 A")
    let folderA1 = FileFolder(name: "文件夹 A1")
    folderA1.subfolders.append(FileFolder(name: "文件 A1-a"))
    folderA1.subfolders.append(FileFolder(name: "文件 A1-b"))
    folderA.subfolders.append(folderA1)
    folderA.subfolders.append(FileFolder(name: "文件夹 A2"))
    
    let folderB = FileFolder(name: "文件夹 B")
    folderB.subfolders.append(FileFolder(name: "文件夹 B1"))
    
    return [folderA, folderB]
}()

UITableView 数据源的实现

现在,让我们开始实现 UITableView。我们将实现 UITableViewDataSourceUITableViewDelegate 协议,以便为我们的表格视图提供数据。

第一步:设置基本的 UITableView

在您的视图控制器中,设置 UITableView 的基本结构:

import UIKit

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    let tableView = UITableView()
    var expandedFolders: Set<Int> = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
        tableView.frame = view.bounds
    }
    
    // DataSource methods
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return rootFolders.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        let folder = rootFolders[indexPath.row]
        cell.textLabel?.text = folder.name
        return cell
    }
}

第二步:实现展开和收起功能

为了支持展开和收起子文件夹,我们需要在 cellForRowAt 方法中检测文件夹的展开状态,并为每个文件夹增加一个手势识别器。

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let folder = rootFolders[indexPath.row]
    
    if expandedFolders.contains(indexPath.row) {
        expandedFolders.remove(indexPath.row)
    } else {
        expandedFolders.insert(indexPath.row)
    }
    
    tableView.reloadData()
}

func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    if expandedFolders.contains(indexPath.row) {
        let subfolders = rootFolders[indexPath.row].subfolders
        for (index, subfolder) in subfolders.enumerated() {
            let subIndexPath = IndexPath(row: index + 1, section: 0)
            tableView.insertRows(at: [subIndexPath], with: .automatic)
        }
    }
}

第三步:优化细节

我们可以通过改变 numberOfRowsInSection 方法,提供更好的体验。针对每个文件夹,返回它的子文件夹数量。

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    var totalRows = rootFolders.count
    for (index, folder) in rootFolders.enumerated() {
        if expandedFolders.contains(index) {
            totalRows += folder.subfolders.count
        }
    }
    return totalRows
}

完整示例

import UIKit

class FileFolder {
    var name: String
    var subfolders: [FileFolder]
    
    init(name: String) {
        self.name = name
        self.subfolders = []
    }
}

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
    
    let tableView = UITableView()
    var rootFolders: [FileFolder] = {
        let folderA = FileFolder(name: "文件夹 A")
        let folderA1 = FileFolder(name: "文件夹 A1")
        folderA1.subfolders.append(FileFolder(name: "文件 A1-a"))
        folderA1.subfolders.append(FileFolder(name: "文件 A1-b"))
        folderA.subfolders.append(folderA1)
        folderA.subfolders.append(FileFolder(name: "文件夹 A2"))
        
        let folderB = FileFolder(name: "文件夹 B")
        folderB.subfolders.append(FileFolder(name: "文件夹 B1"))
        
        return [folderA, folderB]
    }()
    
    var expandedFolders: Set<Int> = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
        view.addSubview(tableView)
        tableView.frame = view.bounds
    }
    
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        var totalRows = rootFolders.count
        for (index, folder) in rootFolders.enumerated() {
            if expandedFolders.contains(index) {
                totalRows += folder.subfolders.count
            }
        }
        return totalRows
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        
        var folder: FileFolder
        var currentRow = indexPath.row
        
        for (index, f) in rootFolders.enumerated() {
            if expandedFolders.contains(index) {
                if currentRow == index {
                    folder = f
                    break
                }
                currentRow -= 1
                if currentRow < f.subfolders.count {
                    folder = f.subfolders[currentRow]
                    break
                }
                currentRow -= f.subfolders.count - 1 // 退回到父文件夹
            } else {
                if index == currentRow {
                    folder = f
                    break
                }
            }
        }
        
        cell.textLabel?.text = folder.name
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        if expandedFolders.contains(indexPath.row) {
            expandedFolders.remove(indexPath.row)
        } else {
            expandedFolders.insert(indexPath.row)
        }
        tableView.reloadData()
    }

}

结尾

通过以上步骤,我们成功实现了一个带有多级目录的 UITableView。该示例展示了如何使用简单的树形结构来模拟文件夹的展开与收起效果。在实际开发中,可以根据需要添加更多功能,例如支持长按删除、编辑文件夹名称等。抓住这些基本思路,开发出功能丰富的用户界面将会变得轻松!