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

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 getData request first, show a loading indicator while it runs
  • Save the getData response 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

  • BehaviorRelay for 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 ConcurrentDispatchQueueScheduler to 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 08:17:43