RxSwift:实现两个ViewController共享单次网络请求的事件数据
问题
刚接触RxSwift,正在开发一个简易货币兑换应用,应用包含allCurrenciesList和userFavouritesView两个页面。目前所有逻辑均可运行,但必须在任一页面的didAppear/didLoad中每次调用网络请求方法才行。需求是仅执行一次网络请求,在需要时让多个页面接收数据。
应用会获取货币字典数据,在ViewModel中传递给BehaviorRelay,页面加载/显示时订阅该数据并用于UITableView。现有代码如下:
ListViewModel
class ListViewModel { let service: CurrencyService! var curriencies = BehaviorRelay<[Currency]>(value: []) var currienciesObservable: Observable<[Currency]> { return curriencies.asObservable().share() } let disposeBag = DisposeBag() init(service: CurrencyService) { self.service = service } func fetchAllCurrencies() { self.service.fetchAllSymbols { result in switch result{ case .success(let currencies): self.dictionaryIntoArray(currencies: currencies["symbols"] as! [String : Any]) case .failure: print("error") } } } private func dictionaryIntoArray(currencies: [String: Any]) { var currencyArray = [Currency]() for (symbol, name) in currencies { currencyArray.append(Currency(symbol: symbol, fullName: name as! String)) } let sortedArray = currencyArray.sorted { $0.fullName < $1.fullName } self.curriencies.accept(sortedArray) }
allCurrenciesList页面
override func viewDidLoad() { super.viewDidLoad() setupView() configureTableViewDataSource() tableView.delegate = self fetchData() } private func fetchData() { viewModel.fetchAllCurrencies() // 目前每次都需要调用此方法 viewModel.currienciesObservable.subscribe(onNext: { curriencies in self.applySnapshot(curriencies: curriencies) }).disposed(by: disposeBag) }
userFavouritesView页面
override func viewDidLoad() { super.viewDidLoad() viewModel.fetchAllCurrencies() // 目前每次都需要调用此方法 viewModel.currienciesObservable.subscribe(onNext: { allCurencies in let selectedItems = UserDefaults.standard.array(forKey: "SelectedCells") as? [Int] ?? [Int]() var currenciesArray: [Currency] = [] selectedItems.forEach { int in self.pickerValues.append(allCurencies[int]) currenciesArray.append(allCurencies[int]) } self.applySnapshot(curriencies: currenciesArray) }).disposed(by: disposeBag) }
解决方案
核心思路是确保网络请求仅触发一次,同时利用BehaviorRelay的特性让多个页面订阅时能获取到最新数据。以下是具体修改方案:
1. 修改ListViewModel
添加请求状态标记,将原有的fetchAllCurrencies改为仅在需要时执行的fetchIfNeeded,同时优化数据转换逻辑(避免强制解包):
class ListViewModel { let service: CurrencyService! private let currienciesRelay = BehaviorRelay<[Currency]>(value: []) var currienciesObservable: Observable<[Currency]> { return currienciesRelay.asObservable() } private let disposeBag = DisposeBag() // 标记是否已发起过网络请求 private var hasFetched = false init(service: CurrencyService) { self.service = service } func fetchIfNeeded() { // 仅当未请求过且当前数据为空时发起请求 guard !hasFetched, currienciesRelay.value.isEmpty else { return } hasFetched = true self.service.fetchAllSymbols { [weak self] result in guard let self = self else { return } switch result{ case .success(let currencies): guard let symbols = currencies["symbols"] as? [String : Any] else { print("数据格式错误") return } self.dictionaryIntoArray(currencies: symbols) case .failure(let error): print("请求失败: \(error.localizedDescription)") } } } private func dictionaryIntoArray(currencies: [String: Any]) { // 用compactMap替代for循环,避免强制解包 let currencyArray = currencies.compactMap { symbol, name in guard let fullName = name as? String else { return nil } return Currency(symbol: symbol, fullName: fullName) }.sorted { $0.fullName < $1.fullName } self.currienciesRelay.accept(currencyArray) } }
2. 修改页面代码
将两个页面中调用fetchAllCurrencies的地方替换为fetchIfNeeded,确保仅在需要时触发请求:
allCurrenciesList页面
override func viewDidLoad() { super.viewDidLoad() setupView() configureTableViewDataSource() tableView.delegate = self fetchData() } private func fetchData() { viewModel.fetchIfNeeded() // 仅在需要时发起请求 viewModel.currienciesObservable.subscribe(onNext: { curriencies in self.applySnapshot(curriencies: curriencies) }).disposed(by: disposeBag) }
userFavouritesView页面
override func viewDidLoad() { super.viewDidLoad() viewModel.fetchIfNeeded() // 仅在需要时发起请求 viewModel.currienciesObservable.subscribe(onNext: { allCurencies in let selectedItems = UserDefaults.standard.array(forKey: "SelectedCells") as? [Int] ?? [] // 用compactMap处理索引越界问题 let currenciesArray = selectedItems.compactMap { index in guard index < allCurencies.count else { return nil } let currency = allCurencies[index] self.pickerValues.append(currency) return currency } self.applySnapshot(curriencies: currenciesArray) }).disposed(by: disposeBag) }
3. 进阶Rx风格实现(可选)
如果想更贴合RxSwift的响应式思想,可以将网络请求包装为Observable并利用share(replay:1)缓存结果,确保仅执行一次:
class ListViewModel { let service: CurrencyService! private let currienciesRelay = BehaviorRelay<[Currency]>(value: []) var currienciesObservable: Observable<[Currency]> { return currienciesRelay.asObservable() } private let disposeBag = DisposeBag() // 懒加载请求Observable,share(replay:1)确保只执行一次并缓存结果 private lazy var fetchObservable: Observable<[Currency]> = { Observable.create { [weak self] observer in guard let self = self else { observer.onCompleted() return Disposables.create() } self.service.fetchAllSymbols { result in switch result { case .success(let currencies): guard let symbols = currencies["symbols"] as? [String: Any] else { observer.onError(NSError(domain: "CurrencyError", code: -1, userInfo: [NSLocalizedDescriptionKey: "数据格式错误"])) return } let currencyArray = symbols.compactMap { symbol, name in guard let fullName = name as? String else { return nil } return Currency(symbol: symbol, fullName: fullName) }.sorted { $0.fullName < $1.fullName } observer.onNext(currencyArray) observer.onCompleted() case .failure(let error): observer.onError(error) } } return Disposables.create() }.share(replay: 1, scope: .forever) }() init(service: CurrencyService) { self.service = service } func fetchIfNeeded() { guard currienciesRelay.value.isEmpty else { return } fetchObservable.subscribe(onNext: { [weak self] currencies in self?.currienciesRelay.accept(currencies) }, onError: { error in print("请求失败: \(error.localizedDescription)") }).disposed(by: disposeBag) } }
效果说明
- 无论哪个页面先加载,都会触发一次网络请求,后续页面调用
fetchIfNeeded时会直接跳过请求 - BehaviorRelay会保存最新的货币数据,新订阅的页面会立即收到当前的最新值,无需等待请求
- 优化了原代码中的强制解包问题,提升了稳定性
内容的提问来源于stack exchange,提问作者malczan
相关产品推荐
相关产品推荐

