Swift 5 RxDataSource可展开TableView改造遇行数控制问题
解决RxSwift中可展开TableView的行数控制问题
我明白你现在遇到的核心痛点:原来的TableView通过numberOfRowsInSection在section未展开时返回0来隐藏行,但迁移到RxSwift的RxTableViewSectionedReloadDataSource时,没法直接复用这个逻辑,导致插入/删除行时出现错误。下面是一套完整的解决方案,既保留你原始的数据源结构,又能完美适配RxSwift的响应式逻辑:
核心思路
RxSwift的RxTableViewSectionedReloadDataSource默认会直接使用section模型的items数组长度作为行数,没法像原生代理那样直接返回0。所以我们需要:
- 用BehaviorSubject保存原始数据源,方便响应式更新状态
- 通过
map操作将原始数据转换为「显示用的section数组」——未展开的section会被替换为空items数组 - 使用
RxTableViewSectionedAnimatedDataSource替代Reload版本,实现和原生一致的插入/删除动画
具体实现步骤
1. 让Section模型遵循SectionModelType
首先让CategoriesSectionData实现RxDataSources要求的协议,这样才能被数据源识别:
import RxDataSources struct CategoriesSectionData: SectionModelType { typealias Item = CategoriesCellData var header: String var items: [CategoriesCellData] var isExpanded: Bool var icon: String // 协议要求的初始化方法,用于修改items时保留其他属性 init(original: CategoriesSectionData, items: [CategoriesCellData]) { self = original self.items = items } }
2. 用BehaviorSubject管理原始数据
把你的静态数据放到BehaviorSubject里,这样每次修改isExpanded时,能自动通知Rx更新TableView:
private let dataSubject = BehaviorSubject<[CategoriesSectionData]>(value: [ CategoriesSectionData(header: "Elektro", items: [CategoriesCellData(price: 12.99, title: "Gärtner 1"), CategoriesCellData(price: 15.30, title: "Gärtner 2"), CategoriesCellData(price: 25.99, title: "Gärtner 3")], isExpanded: false, icon: "home_menu_1"), CategoriesSectionData(header: "Gartenpflege", items: [CategoriesCellData(price: 14.0, title: "Gärtner 1")], isExpanded: false, icon: "home_menu_2"), CategoriesSectionData(header: "Sanitär", items: [], isExpanded: false, icon: "home_menu_3"), CategoriesSectionData(header: "Hausmeisterdienste", items: [], isExpanded: false, icon: "home_menu_4"), CategoriesSectionData(header: "Meisterprüfung", items: [], isExpanded: false, icon: "home_menu_3"), CategoriesSectionData(header: "Gartenpflege", items: [], isExpanded: false, icon: "home_menu_2"), CategoriesSectionData(header: "Sanitär", items: [], isExpanded: false, icon: "home_menu_1"), CategoriesSectionData(header: "Hausmeisterdienste", items: [], isExpanded: false, icon: "home_menu_4") ])
3. 转换数据为显示用的Section数组
通过map操作,将原始数据转换为适合显示的结构——未展开的section会被替换为空items数组,这样数据源就会自动返回0行:
private let displayedSections: Observable<[CategoriesSectionData]> = dataSubject.map { originalSections in originalSections.map { section in // 未展开时返回空items,展开时返回原始items CategoriesSectionData(original: section, items: section.isExpanded ? section.items : []) } }
4. 创建带动画的数据源并绑定
使用RxTableViewSectionedAnimatedDataSource来实现原生的插入/删除动画,配置cell和header的逻辑和你原来的一致:
private func setupTableViewDataSource() { let animationConfig = AnimationConfiguration( insertAnimation: .none, reloadAnimation: .none, deleteAnimation: .none ) let dataSource = RxTableViewSectionedAnimatedDataSource<CategoriesSectionData>( animationConfiguration: animationConfig, configureCell: { ds, tv, indexPath, item in let cell: HomeViewCell = tv.dequeueReusableCell(forIndexPath: indexPath) cell.categoryLabel.text = item.title ?? "" return cell }, titleForHeaderInSection: { ds, index in return ds.sectionModels[index].header } ) // 绑定数据到TableView displayedSections .bind(to: categoriesTableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) // 设置代理处理Header点击 categoriesTableView.rx.setDelegate(self).disposed(by: disposeBag) }
5. 处理Header点击的展开/收起逻辑
在UITableViewDelegate中实现Header的创建和点击事件,更新原始数据的isExpanded状态:
extension YourViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let headerView = UITableViewHeaderFooterView(reuseIdentifier: "Header") headerView.contentView.backgroundColor = .white // 添加标题Label let titleLabel = UILabel(frame: CGRect(x: 16, y: 0, width: tableView.bounds.width - 32, height: 44)) titleLabel.text = try? dataSubject.value()[section].header headerView.addSubview(titleLabel) // 添加点击手势 let tapGesture = UITapGestureRecognizer() headerView.addGestureRecognizer(tapGesture) // 绑定手势点击事件,更新数据源 tapGesture.rx.event .subscribe(onNext: { [weak self] _ in guard let self = self else { return } var currentData = try! self.dataSubject.value() currentData[section].isExpanded.toggle() self.dataSubject.onNext(currentData) }) .disposed(by: disposeBag) return headerView } // 可选:设置Header高度 func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return 44 } }
方案优势
- 完全保留原始数据源结构,没有清空或修改原始
items数组,符合你的需求 - 借助BehaviorSubject的响应式特性,每次修改
isExpanded后,TableView会自动刷新对应的section,实现和原生一致的插入/删除动画 - 用
RxTableViewSectionedAnimatedDataSource替代Reload版本,避免了全量刷新,提升了性能和用户体验
内容的提问来源于stack exchange,提问作者George Heints
相关产品推荐
相关产品推荐

