RxSwift异步请求设计:新手寻求登录后双请求流程最简实现
Hey Dan, welcome to the RxSwift world! Let's build your data flow step by step
First, let's break down your core requirements to make sure we cover everything:
- After login, fire the
getDatarequest first, show a loading indicator while it runs - Save the
getDataresponse to Core Data once received - Automatically start a second background request right after the first task finishes
- Save the second request's data to Core Data too
- If the user navigates to the view showing the second request's data before it's loaded, show a loading indicator there
Here's a minimal, RxSwift-native implementation that hits all these points:
1. Define Loading State & Core Services
First, let's set up a simple enum to track loading states, plus reusable services for API calls and Core Data operations.
Loading State Enum
enum LoadingState { case idle case loading case loaded }
API Service (Handles Network Requests)
class APIService { // Replace with your actual API call logic func getData() -> Observable<[PrimaryDataModel]> { // Simulate a 2-second network delay return Observable.just([PrimaryDataModel(id: "1", content: "Sample primary data")]) .delay(.seconds(2), scheduler: MainScheduler.instance) } // Secondary request, configured to run in background func getSecondaryData() -> Observable<[SecondaryDataModel]> { // Simulate a 3-second background request return Observable.just([SecondaryDataModel(id: "2", detail: "Sample secondary data")]) .delay(.seconds(3), scheduler: ConcurrentDispatchQueueScheduler(qos: .background)) } }
Core Data Manager (Handles Persistence)
class DataManager { private let persistentContainer: NSPersistentContainer init(container: NSPersistentContainer = NSPersistentContainer(name: "YourAppModel")) { self.persistentContainer = container persistentContainer.loadPersistentStores { _, error in if let error = error { fatalError("Core Data load failed: \(error.localizedDescription)") } } } // Save primary data to Core Data func savePrimaryData(_ data: [PrimaryDataModel]) -> Completable { return Completable.create { [weak self] completable in guard let self = self else { completable(.error(NSError(domain: "DataManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Manager deallocated"]))) return Disposables.create() } let context = self.persistentContainer.viewContext // Map your model to Core Data entity data.forEach { model in let entity = PrimaryDataEntity(context: context) entity.id = model.id entity.content = model.content } do { try context.save() completable(.completed) } catch { completable(.error(error)) context.rollback() } return Disposables.create { context.rollback() } } .subscribeOn(SerialDispatchQueueScheduler(qos: .background)) } // Save secondary data to Core Data func saveSecondaryData(_ data: [SecondaryDataModel]) -> Completable { return Completable.create { [weak self] completable in guard let self = self else { completable(.error(NSError(domain: "DataManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Manager deallocated"]))) return Disposables.create() } let context = self.persistentContainer.viewContext data.forEach { model in let entity = SecondaryDataEntity(context: context) entity.id = model.id entity.detail = model.detail } do { try context.save() completable(.completed) } catch { completable(.error(error)) context.rollback() } return Disposables.create { context.rollback() } } .subscribeOn(SerialDispatchQueueScheduler(qos: .background)) } }
2. ViewModel (Orchestrates the Data Flow)
The ViewModel will handle the sequence of requests, manage loading states, and coordinate with the services.
class MainViewModel { private let apiService = APIService() private let dataManager = DataManager() // Track loading state for the primary request (login flow) let primaryLoadingState = BehaviorRelay<LoadingState>(value: .idle) // Track loading state for the secondary request (for its target view) let secondaryLoadingState = BehaviorRelay<LoadingState>(value: .idle) private let disposeBag = DisposeBag() func startDataFlowAfterLogin() { // Show loading indicator for primary request primaryLoadingState.accept(.loading) apiService.getData() // Chain the network request to Core Data save .flatMap { [weak self] primaryData -> Completable in self?.dataManager.savePrimaryData(primaryData) ?? Completable.error(NSError(domain: "ViewModel", code: -1, userInfo: nil)) } // Switch back to main thread to update UI state .observeOn(MainScheduler.instance) .subscribe( onCompleted: { [weak self] in // Primary task done, hide loading and start secondary request self?.primaryLoadingState.accept(.loaded) self?.startSecondaryDataFlow() }, onError: { [weak self] error in print("Primary data flow failed: \(error)") self?.primaryLoadingState.accept(.idle) // Add error handling here (e.g., alert user) } ) .disposed(by: disposeBag) } private func startSecondaryDataFlow() { secondaryLoadingState.accept(.loading) apiService.getSecondaryData() .flatMap { [weak self] secondaryData -> Completable in self?.dataManager.saveSecondaryData(secondaryData) ?? Completable.error(NSError(domain: "ViewModel", code: -1, userInfo: nil)) } .observeOn(MainScheduler.instance) .subscribe( onCompleted: { [weak self] in self?.secondaryLoadingState.accept(.loaded) }, onError: { [weak self] error in print("Secondary data flow failed: \(error)") self?.secondaryLoadingState.accept(.idle) } ) .disposed(by: disposeBag) } }
3. View Controllers (Bind to Loading States)
Main View Controller (After Login)
This will show the loading indicator while the primary request runs:
class PostLoginViewController: UIViewController { private let viewModel = MainViewModel() private let loadingIndicator = UIActivityIndicatorView(style: .large) private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setupLoadingIndicator() bindViewModel() // Trigger the data flow once login is confirmed viewModel.startDataFlowAfterLogin() } private func setupLoadingIndicator() { loadingIndicator.center = view.center loadingIndicator.hidesWhenStopped = true view.addSubview(loadingIndicator) } private func bindViewModel() { viewModel.primaryLoadingState .subscribe(onNext: { [weak self] state in switch state { case .loading: self?.loadingIndicator.startAnimating() case .loaded, .idle: self?.loadingIndicator.stopAnimating() } }) .disposed(by: disposeBag) } }
Secondary Data View Controller
This will show a loading indicator if the secondary data isn't ready yet:
class SecondaryDataViewController: UIViewController { // Use the same ViewModel instance (use dependency injection in real apps) private let viewModel = MainViewModel() private let loadingIndicator = UIActivityIndicatorView(style: .large) private let disposeBag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() setupLoadingIndicator() bindViewModel() } private func setupLoadingIndicator() { loadingIndicator.center = view.center loadingIndicator.hidesWhenStopped = true view.addSubview(loadingIndicator) } private func bindViewModel() { viewModel.secondaryLoadingState .subscribe(onNext: { [weak self] state in switch state { case .loading: self?.loadingIndicator.startAnimating() case .loaded: self?.loadingIndicator.stopAnimating() self?.fetchAndDisplayData() case .idle: self?.loadingIndicator.stopAnimating() // Handle error state if needed } }) .disposed(by: disposeBag) } private func fetchAndDisplayData() { // Fetch saved data from Core Data and update your UI let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext do { let secondaryData = try context.fetch(SecondaryDataEntity.fetchRequest()) // Update your table/collection view with the data } catch { print("Failed to fetch secondary data: \(error)") } } }
Key Things to Note
BehaviorRelayfor Loading States: This is perfect here because it retains the current state, so when the secondary view loads, it immediately gets the latest loading status (no need to re-trigger requests).- Sequential Flow with
flatMap: We chain the network request to the Core Data save operation to ensure the first task finishes before starting the second. - Thread Safety: All Core Data operations run on a background thread, and we switch back to the main thread only to update UI states.
- Background Secondary Request: The secondary API call uses a
ConcurrentDispatchQueueSchedulerto run in the background, so it doesn't block the main thread.
Quick Optimizations for Production
- Use a private Core Data context for background operations instead of the view context (safer for concurrent writes).
- Add retry logic for failed network requests using RxSwift's
retry()operator. - Inject dependencies (APIService, DataManager) into the ViewModel instead of initializing them directly (easier to test).
- Share the ViewModel instance between view controllers (e.g., via a coordinator or dependency injection) instead of creating new ones.
内容的提问来源于stack exchange,提问作者Dan
相关产品推荐
相关产品推荐

