如何将UITableViewRowAction展示逻辑从代理移至MVVM架构的ViewModel
用RxSwift + RxDataSource + MVVM实现左滑操作的解耦
这问题我之前做类似邮件类项目时也折腾过,要把左滑操作的逻辑从VC的代理里搬到ViewModel,核心思路就是把操作的配置和事件流转都交给ViewModel管理,VC只负责UI层的绑定和事件传递。下面给你一步步拆解具体实现:
1. 定义带左滑操作配置的Cell ViewModel
首先,给每个列表项的ViewModel添加左滑操作的元数据,让ViewModel能根据业务逻辑决定每个item显示哪些左滑按钮:
import RxSwift struct MailItemViewModel { let id: UUID // 唯一标识,用来区分不同item let title: String let content: String var swipeActions: [SwipeAction] = [] // 左滑操作的配置模型,包含UI信息和操作类型 struct SwipeAction { let title: String let backgroundColor: UIColor let actionType: ActionType } // 定义操作类型,让ViewModel能区分不同操作 enum ActionType { case delete case more } }
2. 在ViewModel中管理数据和操作逻辑
ViewModel要负责:
- 提供列表数据的Observable
- 接收VC传递的操作事件,处理业务逻辑(比如删除item、触发更多操作)
- 根据业务规则配置每个item的左滑操作
class MailViewModel { // 对外暴露的列表数据Observable let items: Observable<[MailItemViewModel]> // 用来接收VC传递的操作事件 private let actionSubject = PublishSubject<(itemId: UUID, action: MailItemViewModel.ActionType)>() // 内部保存当前列表数据,方便更新 private let itemsSubject = BehaviorSubject<[MailItemViewModel]>(value: []) private let disposeBag = DisposeBag() init() { // 初始化模拟数据,实际项目中替换为接口请求或本地数据 let initialItems = [ MailItemViewModel( id: UUID(), title: "团队周会通知", content: "本周三下午2点召开团队周会,请准时参加", swipeActions: [ MailItemViewModel.SwipeAction(title: "删除", backgroundColor: .systemRed, actionType: .delete), MailItemViewModel.SwipeAction(title: "更多", backgroundColor: .systemGray, actionType: .more) ] ), MailItemViewModel( id: UUID(), title: "项目进度更新", content: "项目第一阶段已完成,进入第二阶段开发", swipeActions: [ MailItemViewModel.SwipeAction(title: "删除", backgroundColor: .systemRed, actionType: .delete) ] ) ] itemsSubject.onNext(initialItems) items = itemsSubject.asObservable() // 订阅操作事件,处理业务逻辑 setupActionHandling() } private func setupActionHandling() { actionSubject .subscribe(onNext: { [weak self] itemId, action in guard let self = self else { return } switch action { case .delete: // 处理删除逻辑:从当前数据中移除对应item var currentItems = try! self.itemsSubject.value() currentItems.removeAll { $0.id == itemId } self.itemsSubject.onNext(currentItems) // 这里还可以添加网络请求,比如通知服务器删除该邮件 case .more: // 处理更多操作,比如弹出菜单、跳转详情等 print("触发更多操作,itemId: \(itemId)") } }) .disposed(by: disposeBag) } // 对外暴露发送操作事件的方法,VC可以调用这个方法传递事件 func sendAction(for itemId: UUID, action: MailItemViewModel.ActionType) { actionSubject.onNext((itemId, action)) } }
3. 在VC中绑定数据和左滑操作
VC的职责就是把ViewModel的数据绑定到TableView,同时把左滑按钮的点击事件传递给ViewModel。这里需要扩展RxTableViewDelegateProxy来支持editActionsForRowAt代理方法的Rx化配置:
扩展RxTableViewDelegateProxy
import RxSwift import RxCocoa extension RxTableViewDelegateProxy { func setEditActionsForRowAt(_ handler: @escaping (UITableView, IndexPath) -> [UITableViewRowAction]?) { self.methodInvoker.selector = #selector(UITableViewDelegate.tableView(_:editActionsForRowAt:)) self.methodInvoker.method = { [weak self] tableView, indexPath in guard let self = self else { return nil } return handler(tableView, indexPath) } } }
VC中的绑定逻辑
import RxSwift import RxCocoa import RxDataSources class MailViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private let viewModel = MailViewModel() private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setupTableView() bindViewModel() } private func setupTableView() { tableView.register(UITableViewCell.self, forCellReuseIdentifier: "MailCell") tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 60 } private func bindViewModel() { // 配置RxDataSource let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, MailItemViewModel>>( configureCell: { _, tableView, indexPath, item in let cell = tableView.dequeueReusableCell(withIdentifier: "MailCell", for: indexPath) cell.textLabel?.text = item.title cell.detailTextLabel?.text = item.content return cell } ) // 绑定列表数据到TableView viewModel.items .map { [SectionModel(model: "", items: $0)] } .bind(to: tableView.rx.items(dataSource: dataSource)) .disposed(by: disposeBag) // 配置左滑操作 RxTableViewDelegateProxy.proxy(for: tableView).setEditActionsForRowAt { [weak self] tableView, indexPath in guard let self = self, let item = dataSource[indexPath] else { return nil } // 根据ViewModel配置的swipeActions生成UITableViewRowAction return item.swipeActions.map { swipeConfig in UITableViewRowAction(style: .default, title: swipeConfig.title) { _, _ in // 把点击事件传递给ViewModel self.viewModel.sendAction(for: item.id, action: swipeConfig.actionType) } } } } }
核心思路总结
- 职责分离:ViewModel负责决定每个item有哪些左滑操作,以及处理操作对应的业务逻辑;VC只负责UI渲染和事件传递。
- Rx化代理:通过扩展
RxTableViewDelegateProxy,把原生的代理方法转化为可配置的闭包,避免VC中充斥代理代码。 - 事件流转:用
PublishSubject把操作事件从VC传递到ViewModel,实现真正的MVVM数据驱动。
这样一来,左滑操作的逻辑就完全从VC的代理方法中迁移到ViewModel里了,后续修改操作逻辑或者新增操作类型,只需要修改ViewModel即可,不用动VC的代码。
内容的提问来源于stack exchange,提问作者Jefferson Setiawan
相关产品推荐
相关产品推荐

