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
相关产品推荐
相关产品推荐

