You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift 5 RxDataSource可展开TableView改造遇行数控制问题

解决RxSwift中可展开TableView的行数控制问题

我明白你现在遇到的核心痛点:原来的TableView通过numberOfRowsInSection在section未展开时返回0来隐藏行,但迁移到RxSwift的RxTableViewSectionedReloadDataSource时,没法直接复用这个逻辑,导致插入/删除行时出现错误。下面是一套完整的解决方案,既保留你原始的数据源结构,又能完美适配RxSwift的响应式逻辑:

核心思路

RxSwift的RxTableViewSectionedReloadDataSource默认会直接使用section模型的items数组长度作为行数,没法像原生代理那样直接返回0。所以我们需要:

  1. 用BehaviorSubject保存原始数据源,方便响应式更新状态
  2. 通过map操作将原始数据转换为「显示用的section数组」——未展开的section会被替换为空items数组
  3. 使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 08:45:51