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

RxSwift-MVVM架构中弹窗、加载指示器的最优触发方式咨询

嗨,这个问题在RxSwift+MVVM的实践里太常见了!核心思路就是让ViewModel只负责业务逻辑和状态输出,把UI相关的操作完全交回给ViewController,绝对不用打破解耦原则。我给你分享两个最实用的方案:

方案1:给ViewModel添加状态流(最推荐,逻辑清晰)

ViewModel不需要持有任何UI组件的引用,而是通过暴露专门的状态Observable,把加载状态、错误事件传递给ViewController,由VC来处理具体的UI操作。

修改后的ViewModel代码:

import RxSwift
import RxCocoa

class ViewModel {
    // 数据源
    let source = BehaviorRelay<[Student]>(value: [])
    // 加载状态(默认未加载)
    let isLoading = BehaviorRelay<Bool>(value: false)
    // 错误事件流(专门传递API调用的错误)
    let errorSignal = PublishSubject<Error>()
    private let bag = DisposeBag()
    private let api: APIService // 假设你的API服务是这个类型

    init(id: String, api: APIService) {
        self.api = api
        fetchSource(id: id)
    }

    private func fetchSource(id: String) {
        // 发送"开始加载"的状态
        isLoading.accept(true)

        api.fetchSourceOutput(id: id)
            .subscribe(onNext: { [weak self] students in
                // 数据源更新,结束加载
                self?.source.accept(students)
                self?.isLoading.accept(false)
            }, onError: { [weak self] error in
                // 发送错误事件,结束加载,空数据兜底
                self?.errorSignal.onNext(error)
                self?.isLoading.accept(false)
                self?.source.accept([])
            })
            .disposed(by: bag)
    }
}

ViewController的绑定代码:

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var loadingIndicator: UIActivityIndicatorView!
    private let viewModel: ViewModel
    private let bag = DisposeBag()

    // 用构造注入ViewModel,进一步解耦
    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        super.init(nibName: "ViewController", bundle: nil)
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        bindViewModel()
    }

    private func bindViewModel() {
        // 绑定数据源到表格
        viewModel.source
            .bind(to: tableView.rx.items(cellIdentifier: "StudentCell", cellType: StudentCell.self)) { _, student, cell in
                cell.nameLabel.text = student.name
                cell.ageLabel.text = "\(student.age)"
            }
            .disposed(by: bag)

        // 绑定加载状态到指示器
        viewModel.isLoading
            .observe(on: MainScheduler.instance) // 确保在主线程更新UI
            .bind(to: loadingIndicator.rx.isAnimating)
            .disposed(by: bag)

        // 订阅错误事件,显示弹窗
        viewModel.errorSignal
            .observe(on: MainScheduler.instance)
            .subscribe(onNext: { [weak self] error in
                let alert = UIAlertController(
                    title: "加载失败",
                    message: error.localizedDescription,
                    preferredStyle: .alert
                )
                alert.addAction(UIAlertAction(title: "确定", style: .default))
                self?.present(alert, animated: true)
            })
            .disposed(by: bag)
    }
}

这个方案的优势很明显:

  • ViewModel完全和UI解耦,只负责输出业务状态,可测试性极强
  • ViewController专注于UI映射,逻辑单一
  • 状态流清晰,所有UI变化都由ViewModel的状态驱动,符合MVVM的核心思想
方案2:用Driver/Signal优化UI场景(更安全)

如果想进一步简化UI层的线程处理,可以把状态流转换成Driver或者Signal——这两个是RxSwift专门为UI场景设计的序列,默认在主线程运行,且Driver会自动处理错误、共享订阅,避免重复请求。

修改ViewModel的状态流:

class ViewModel {
    let source: Driver<[Student]>
    let isLoading: Driver<Bool>
    let errorSignal: Signal<Error>
    
    private let sourceRelay = BehaviorRelay<[Student]>(value: [])
    private let isLoadingRelay = BehaviorRelay<Bool>(value: false)
    private let errorSubject = PublishSubject<Error>()
    private let bag = DisposeBag()
    private let api: APIService

    init(id: String, api: APIService) {
        self.api = api
        // 转换成Driver/Signal
        source = sourceRelay.asDriver()
        isLoading = isLoadingRelay.asDriver()
        errorSignal = errorSubject.asSignal(onErrorSignalWith: .empty())
        
        fetchSource(id: id)
    }

    private func fetchSource(id: String) {
        isLoadingRelay.accept(true)

        api.fetchSourceOutput(id: id)
            .subscribe(onNext: { [weak self] students in
                self?.sourceRelay.accept(students)
                self?.isLoadingRelay.accept(false)
            }, onError: { [weak self] error in
                self?.errorSubject.onNext(error)
                self?.isLoadingRelay.accept(false)
                self?.sourceRelay.accept([])
            })
            .disposed(by: bag)
    }
}

ViewController里的绑定可以更简洁:

private func bindViewModel() {
    // Driver默认主线程,不用手动切换
    viewModel.source
        .drive(tableView.rx.items(cellIdentifier: "StudentCell", cellType: StudentCell.self)) { _, student, cell in
            cell.configure(with: student)
        }
        .disposed(by: bag)

    viewModel.isLoading
        .drive(loadingIndicator.rx.isAnimating)
        .disposed(by: bag)

    viewModel.errorSignal
        .emit(onNext: { [weak self] error in
            // 显示弹窗逻辑
        })
        .disposed(by: bag)
}

这样处理后,UI层的代码更简洁,也避免了线程错误的风险。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 06:40:15