RxSwift+MVVM:更新ViewModel中Struct并触发ViewController表格更新
我来帮你梳理下问题核心并给出可行的解决方案——你现在的问题本质是ViewModel里的menu是普通属性,没有响应式监听能力,所以数据更新后ViewController没法感知到变化。之前尝试Subject没成功,大概率是没正确封装Rx序列、或者线程调度出了问题,咱们一步步改:
第一步:改造ViewModel,用BehaviorSubject实现数据响应式推送
首先把ViewModel里的普通menu属性换成BehaviorSubject(它能保存最新值,新订阅者会立即收到当前最新数据,很适合UI场景),同时对外只暴露可订阅的Observable,避免外部随意发送事件:
import Foundation import RxCocoa import RxSwift protocol MenuViewModelProtocol { // 对外暴露可订阅的menu数据 var menuObservable: Observable<MenuModel> { get } func viewDidLoad() } class MenuViewModel: MenuViewModelProtocol { // 用BehaviorSubject持有数据,初始值传空的MenuModel(根据你的结构调整) private let menuSubject = BehaviorSubject<MenuModel>(value: MenuModel()) // 对外只暴露Observable,保证封装性 var menuObservable: Observable<MenuModel> { menuSubject.asObservable() } private let menuIntermediary = MenuDataIntermediary() private let disposeBag = DisposeBag() func viewDidLoad() { debugPrint("MenuViewModel.viewDidLoad: Async request for menu was made") requestAndSetMenuToIntermediary() } private func requestAndSetMenuToIntermediary() { // 这里建议你调整中间层的闭包,同时返回success和解析后的menu(原来的代码里menu变量来源有点模糊) menuIntermediary.jsonDataToMenuConversion { [weak self] success, menu in guard let strongSelf = self else { debugPrint("MenuViewModel.requestMenuToIntermediary: couldn't create a strong self reference") return } guard success, let fetchedMenu = menu else { debugPrint("MenuViewModel.requestMenuToIntermediary: couldn't unwrap menu data") return } // 必须切到主线程发送事件!UI更新依赖主线程 DispatchQueue.main.async { strongSelf.menuSubject.onNext(fetchedMenu) debugPrint("MenuViewModel.requestAndSetMenuToIntermediary: Did get Menu successfully") } } } }
第二步:在ViewController中订阅并绑定到TableView
接下来在你的.xib对应的ViewController里,订阅ViewModel的menuObservable,直接用RxSwift的TableView绑定能力,不用手动实现DataSource:
import UIKit import RxSwift import RxCocoa class MenuViewController: UIViewController { @IBOutlet weak var tableView: UITableView! private let viewModel: MenuViewModelProtocol = MenuViewModel() private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setupTableView() viewModel.viewDidLoad() bindViewModelToTableView() } private func setupTableView() { // 注册你的自定义Cell(替换成你实际的Cell名称) tableView.register(UINib(nibName: "MenuCell", bundle: nil), forCellReuseIdentifier: "MenuCell") // 用Rx设置Delegate(如果需要自定义行高、头部等) tableView.rx.setDelegate(self).disposed(by: disposeBag) } private func bindViewModelToTableView() { viewModel.menuObservable // 把MenuModel转换成TableView需要的数据源数组(替换成你实际的数组属性) .map { $0.menuItems } // 直接绑定到TableView,自动处理Cell复用和数据填充 .bind(to: tableView.rx.items(cellIdentifier: "MenuCell", cellType: MenuCell.self)) { index, item, cell in // 这里是Cell的配置逻辑,比如给Cell的Label赋值 cell.titleLabel.text = item.name cell.subtitleLabel.text = item.description } .disposed(by: disposeBag) // 可选:处理TableView点击事件 tableView.rx.modelSelected(MenuItem.self) .subscribe(onNext: { [weak self] selectedItem in self?.handleItemTap(selectedItem) }) .disposed(by: disposeBag) } private func handleItemTap(_ item: MenuItem) { // 这里写点击Cell后的逻辑,比如跳转详情页 debugPrint("Clicked item: \(item.name)") } } // 可选:如果需要自定义行高、头部视图等,实现UITableViewDelegate extension MenuViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 80 // 替换成你需要的行高 } }
关键要点说明
为什么用BehaviorSubject?
它会保存最新的数据源,当ViewController刚订阅时,会立即收到初始的空MenuModel,后续数据更新时又会收到最新值,完美适配TableView从空到有数据的场景。如果不需要初始值,也可以用PublishSubject,但需要确保订阅在数据请求前完成。必须切主线程的原因
UI相关的操作(包括TableView的更新)必须在主线程执行,所以更新Subject时一定要用DispatchQueue.main.async,避免线程错误。封装Observable的意义
把menuSubject私有化,对外只暴露menuObservable,遵循了RxSwift的封装原则——外部只能订阅数据变化,不能随意发送事件,保证数据流的可控性。RxTableView绑定的优势
用tableView.rx.items可以完全替代传统的UITableViewDataSource实现,不用写繁琐的numberOfRowsInSection、cellForRowAt等方法,代码更简洁且不易出错。
内容的提问来源于stack exchange,提问作者unknownProgrammer

