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

RxSwift+MVVM:更新ViewModel中Struct并触发ViewController表格更新

解决RxSwift下ViewModel通知ViewController更新TableView的问题

我来帮你梳理下问题核心并给出可行的解决方案——你现在的问题本质是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 // 替换成你需要的行高
    }
}

关键要点说明

  1. 为什么用BehaviorSubject?
    它会保存最新的数据源,当ViewController刚订阅时,会立即收到初始的空MenuModel,后续数据更新时又会收到最新值,完美适配TableView从空到有数据的场景。如果不需要初始值,也可以用PublishSubject,但需要确保订阅在数据请求前完成。

  2. 必须切主线程的原因
    UI相关的操作(包括TableView的更新)必须在主线程执行,所以更新Subject时一定要用DispatchQueue.main.async,避免线程错误。

  3. 封装Observable的意义
    把menuSubject私有化,对外只暴露menuObservable,遵循了RxSwift的封装原则——外部只能订阅数据变化,不能随意发送事件,保证数据流的可控性。

  4. RxTableView绑定的优势
    用tableView.rx.items可以完全替代传统的UITableViewDataSource实现,不用写繁琐的numberOfRowsInSection、cellForRowAt等方法,代码更简洁且不易出错。

内容的提问来源于stack exchange,提问作者unknownProgrammer

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 09:16:08