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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 21:39:17