iOS带固定分区表头、可展开折叠子TableView的TableViewController实现求助
实现带固定表头、可展开折叠的嵌套TableView
我来帮你实现这个需求,下面是完整的思路和示例代码,涵盖固定表头、父单元格展开折叠、子TableView嵌套及跳转逻辑:
一、数据模型设计
首先定义清晰的数据结构,用来存储表头、父单元格状态和子单元格数据:
// 子单元格数据模型(存储跳转所需信息) struct SubCellModel { let title: String let targetPageTag: String? // 可自定义跳转标识或参数 } // 父单元格数据模型(控制展开折叠状态) struct ParentCellModel { let title: String var isExpanded: Bool let subCells: [SubCellModel] } // 表头分组模型 struct HeaderSectionModel { let headerTitle: String var parentCells: [ParentCellModel] }
二、主TableViewController实现
主TableView的每个Section对应一个固定表头,每个Row对应一个可展开的父单元格:
import UIKit class MainTableViewController: UITableViewController { // 模拟数据源 var sections: [HeaderSectionModel] = [ HeaderSectionModel( headerTitle: "Header 1", parentCells: [ ParentCellModel(title: "Cell 1.1", isExpanded: false, subCells: []), ParentCellModel(title: "Cell 1.2", isExpanded: true, subCells: [ SubCellModel(title: "subCell 1.2.1", targetPageTag: "Detail"), SubCellModel(title: "subCell 1.2.2", targetPageTag: "Detail") ]), ParentCellModel(title: "Cell 1.3", isExpanded: false, subCells: []) ] ), HeaderSectionModel( headerTitle: "Header 2", parentCells: [ ParentCellModel(title: "Cell 2.1", isExpanded: false, subCells: []), ParentCellModel(title: "Cell 2.2", isExpanded: false, subCells: [ SubCellModel(title: "subCell 2.2.1", targetPageTag: "Detail"), SubCellModel(title: "subCell 2.2.2", targetPageTag: "Detail") ]) ] ) ] override func viewDidLoad() { super.viewDidLoad() setupTableView() } private func setupTableView() { // 注册父单元格 tableView.register(ParentExpandableCell.self, forCellReuseIdentifier: "ParentCell") // 固定表头高度 tableView.sectionHeaderHeight = 50 // 自动计算行高(适配展开/折叠状态) tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 80 tableView.separatorStyle = .singleLine } // MARK: - TableView DataSource override func numberOfSections(in tableView: UITableView) -> Int { return sections.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return sections[section].parentCells.count } // 自定义固定表头 override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let headerView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.width, height: 50)) headerView.backgroundColor = .systemGray5 let titleLabel = UILabel(frame: CGRect(x: 16, y: 0, width: headerView.frame.width - 32, height: 50)) titleLabel.text = sections[section].headerTitle titleLabel.font = .boldSystemFont(ofSize: 16) headerView.addSubview(titleLabel) return headerView } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "ParentCell", for: indexPath) as! ParentExpandableCell let parentModel = sections[indexPath.section].parentCells[indexPath.row] cell.configure(with: parentModel) // 绑定子TableView代理 cell.subTableView.dataSource = cell cell.subTableView.delegate = cell // 展开折叠回调 cell.onExpandToggle = { [weak self] in self?.sections[indexPath.section].parentCells[indexPath.row].isExpanded.toggle() tableView.reloadRows(at: [indexPath], with: .automatic) } // 子单元格跳转回调 cell.onSubCellTap = { [weak self] subModel in self?.navigateToTargetPage(with: subModel) } return cell } } // MARK: - 跳转逻辑 extension MainTableViewController { private func navigateToTargetPage(with subModel: SubCellModel) { // 示例跳转至详情页,可根据实际需求修改 let detailVC = DetailViewController() detailVC.title = subModel.title navigationController?.pushViewController(detailVC, animated: true) } }
三、可展开的父单元格(含子TableView)
自定义ParentExpandableCell,封装标题、展开按钮和子TableView的逻辑:
class ParentExpandableCell: UITableViewCell, UITableViewDataSource, UITableViewDelegate { // UI组件 private let titleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 16) return label }() private let expandBtn: UIButton = { let btn = UIButton(type: .system) btn.setImage(UIImage(systemName: "chevron.right"), for: .normal) btn.tintColor = .darkGray return btn }() let subTableView: UITableView = { let tableView = UITableView() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "SubCell") tableView.separatorStyle = .singleLine // 隐藏子TableView的空表头/表尾 tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNormalMagnitude)) return tableView }() // 数据模型 private var parentModel: ParentCellModel! // 回调闭包 var onExpandToggle: (() -> Void)? var onSubCellTap: ((SubCellModel) -> Void)? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupLayout() setupActions() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupLayout() { contentView.addSubview(titleLabel) contentView.addSubview(expandBtn) contentView.addSubview(subTableView) // 禁用自动布局转换 [titleLabel, expandBtn, subTableView].forEach { $0.translatesAutoresizingMaskIntoConstraints = false } NSLayoutConstraint.activate([ titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), expandBtn.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), expandBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), subTableView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8), subTableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 32), subTableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), subTableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8) ]) } private func setupActions() { expandBtn.addTarget(self, action: #selector(expandBtnTapped), for: .touchUpInside) } @objc private func expandBtnTapped() { onExpandToggle?() // 更新按钮图标 let iconName = parentModel.isExpanded ? "chevron.down" : "chevron.right" expandBtn.setImage(UIImage(systemName: iconName), for: .normal) } // 配置单元格数据 func configure(with model: ParentCellModel) { self.parentModel = model titleLabel.text = model.title // 设置初始图标和子TableView显示状态 let iconName = model.isExpanded ? "chevron.down" : "chevron.right" expandBtn.setImage(UIImage(systemName: iconName), for: .normal) subTableView.isHidden = !model.isExpanded // 刷新子TableView数据 subTableView.reloadData() } // MARK: - Sub TableView DataSource & Delegate func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return parentModel.subCells.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "SubCell", for: indexPath) let subModel = parentModel.subCells[indexPath.row] cell.textLabel?.text = subModel.title cell.textLabel?.font = .systemFont(ofSize: 14) cell.accessoryType = .disclosureIndicator // 显示跳转箭头 return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let subModel = parentModel.subCells[indexPath.row] onSubCellTap?(subModel) } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 44 } }
四、示例详情页
简单的目标跳转页面示例:
class DetailViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white navigationItem.title = title } }
核心要点说明
- 数据驱动UI:通过
isExpanded字段控制父单元格的展开状态,子数据内嵌在父模型中,逻辑更清晰。 - 自动行高适配:主TableView启用自动行高,配合子TableView的显示/隐藏,自动调整父单元格高度。
- 组件解耦:父单元格自行处理子TableView的代理逻辑,通过闭包与主控制器通信,避免代码臃肿。
内容的提问来源于stack exchange,提问作者Vishwanath Deshmukh
相关产品推荐
相关产品推荐

