如何使用RxSwift与RxDataSources仅更新TableView中变更的单元格
嘿,这个场景太常见了——用RxDataSources的时候,总不想因为单个单元格数据变化就刷新整个列表对吧?其实RxDataSources本身就内置了差异计算的能力,只要你正确配置模型和数据源,就能轻松实现局部刷新。我给你一步步拆解:
1. 先让你的数据模型“支持差异识别”
RxDataSources要能找出哪些数据变了,需要你的数据模型实现两个协议:IdentifiableType(用来唯一标识每个item)和Equatable(用来对比item是否有变化)。
举个实际的例子,假设你有一个待办事项模型:
import RxDataSources struct TodoItem: IdentifiableType, Equatable { let id: String // 唯一标识,比如UUID生成 var title: String var isCompleted: Bool // IdentifiableType 要求:指定唯一标识的类型和值 typealias Identity = String var identity: Identity { id } // Equatable 实现:对比需要关注的属性,只有这些属性变化时才认为item不同 static func == (lhs: TodoItem, rhs: TodoItem) -> Bool { return lhs.id == rhs.id && lhs.title == rhs.title && lhs.isCompleted == rhs.isCompleted } }
⚠️ 注意:Equatable的实现一定要准确——如果你的item还有其他需要刷新的属性,记得也加进对比逻辑里;反之,如果某个属性变化不需要刷新单元格,就不要放在这里。
2. 配置RxDataSources的数据源
接下来创建RxTableViewSectionedAnimatedDataSource(如果是CollectionView就用对应的版本),这里的核心是让数据源负责单元格的配置,并且开启它的差异计算能力:
let dataSource = RxTableViewSectionedAnimatedDataSource<AnimatableSectionModel<String, TodoItem>>( // 配置单元格的闭包:当数据源发现item变化时,会重新调用这个闭包刷新对应单元格 configureCell: { dataSource, tableView, indexPath, item in let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell", for: indexPath) as! TodoCell // 直接赋值或者用Rx绑定都可以,这里用简单赋值举例 cell.titleLabel.text = item.title cell.completeButton.isSelected = item.isCompleted return cell }, // 可选:设置刷新动画,让局部刷新更流畅 animationConfiguration: AnimationConfiguration( insertAnimation: .fade, reloadAnimation: .fade, deleteAnimation: .fade ) )
3. 响应式绑定数据到TableView
现在你需要把你的数据序列(比如BehaviorSubject或者Observable)转换为AnimatableSectionModel的序列,然后绑定到TableView:
import RxSwift let disposeBag = DisposeBag() // 用BehaviorSubject存储当前的所有数据,方便后续更新 let itemsSubject = BehaviorSubject<[TodoItem]>(value: []) // 转换为section模型序列,然后绑定到TableView itemsSubject .map { todos in // 这里我们只有一个section,模型用字符串标识即可 [AnimatableSectionModel(model: "待办列表", items: todos)] } .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag)
4. 更新单个item时的正确姿势
当你需要修改某个item的数据时,不要直接修改原数组,而是创建一个新数组替换变化的元素,然后发射新的数组到itemsSubject:
func toggleTodoCompletion(at index: Int) { guard var currentTodos = try? itemsSubject.value() else { return } // 修改指定位置的item属性 currentTodos[index].isCompleted.toggle() // 发射新数组,触发数据源的差异计算 itemsSubject.onNext(currentTodos) }
这时候RxDataSources会自动对比新旧数组的差异,只刷新那个发生变化的单元格,而不是整个TableView!
5. 进阶:单元格交互的响应式处理
如果你的单元格有交互控件(比如完成按钮),可以把控件的事件绑定回ViewModel,实现全响应式的流程:
// 先给自定义单元格添加一个事件Observable class TodoCell: UITableViewCell { @IBOutlet weak var titleLabel: UILabel! @IBOutlet weak var completeButton: UIButton! let disposeBag = DisposeBag() // 对外暴露按钮点击事件 var onToggleCompletion: Observable<Void> { completeButton.rx.tap.asObservable() } } // 在configureCell里绑定事件 configureCell: { [weak self] dataSource, tableView, indexPath, item in let cell = tableView.dequeueReusableCell(withIdentifier: "TodoCell", for: indexPath) as! TodoCell cell.titleLabel.text = item.title cell.completeButton.isSelected = item.isCompleted // 绑定按钮点击事件到更新逻辑 cell.onToggleCompletion .subscribe(onNext: { self?.toggleTodoCompletion(at: indexPath.row) }) .disposed(by: cell.disposeBag) return cell }
这样用户点击按钮时,会自动触发数据更新,然后数据源自动刷新对应的单元格,全程都是响应式的,不用手动管理刷新逻辑。
常见误区避坑
- ❌ 不要手动调用
tableView.reloadData():这会覆盖RxDataSources的局部刷新逻辑,直接刷新整个列表。 - ❌ 不要用普通的
tableView.rx.items(cellIdentifier:)绑定:这种方式会每次刷新整个列表,必须用RxDataSources的数据源绑定。 - ❌ 不要错误实现Equatable:如果Equatable返回true但实际数据已经变化,单元格不会刷新;如果返回false但数据没变化,会导致不必要的刷新。
内容的提问来源于stack exchange,提问作者Oleg Gordiichuk

