One of the main features of using RxSwift and RxCocoa or FRP is the possibility to chain several asynchronous requests and avoid callback hell. It can be achieved by using FlatMap operator and creating your own Observables from your asynchronous jobs like network requests or heavy calculations. But sometimes it is not simple to understand, how to do it. In this post I would like to show an example from one of my projects. The same can be applied to Objective C + ReactiveCocoa, the idea is the same.
First of all we should create Observables from different asynchronous requests. Then we can chain them using FlatMap operator. My example concerns a classic case – login and profile fetching. In many apps after logging in a user you have to request his profile in a separate request.
Usually you use MVVM with RxSwift. In a ViewModel you can create observables. In my case login() method creates an Observable from Alamofire request. In loadProfile() method Observable is created from ProfileService.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
/// Logins user and saves credentials /// /// - Returns: true if login was successfull; error in other cases func login() -> Observable<Bool> { return Observable<Bool>.create { (observer) -> Disposable in let params = ["username": self.nameText.value, "password": self.passwordText.value] self.inProgress.value = true Alamofire.request(APPURL.Login, method: .post, parameters: params) .responseJSON(completionHandler: { (dataResponse) in self.inProgress.value = false switch dataResponse.result { case .success(let value): guard let jsonDictionary = value as? Dictionary<String, Any> else { let userInfo: [String : Any] = [NSLocalizedDescriptionKey : "Unable to login with the provided credentials"] let error = NSError(domain:"", code:0, userInfo:userInfo) return observer.onError(error) } guard let authToken = jsonDictionary["auth_token"] as? String else { let userInfo: [String : Any] = [NSLocalizedDescriptionKey : "Unable to login with the provided credentials"] let error = NSError(domain:"", code:0, userInfo:userInfo) return observer.onError(error) } self.saveAuthToken(authToken) observer.onNext(true) observer.onCompleted() case .failure(let error): print("Could not login due to \(error)") observer.onError(error) } }) return Disposables.create() } } /// Loads profile /// /// - Returns: profile object if success, otherwise error func loadProfile() -> Observable<Profile> { return Observable<Profile>.create({ (observer) -> Disposable in self.inProgress.value = true ProfileService.shared.getProfile { self.inProgress.value = false guard let profile = ProfileService.shared.profile else { let userInfo: [String : Any] = [NSLocalizedDescriptionKey : "Unable to load profile"] let error = NSError(domain:"", code:0, userInfo:userInfo) return observer.onError(error) } observer.onNext(profile) observer.onCompleted() } return Disposables.create() }) } |
This is how you chain login and loadProfile requests. When they finish, you can do some job with a result profile. In this case I check a trial period and navigate to one of the views depending on a result.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@IBAction func signMeInAction(_ sender: AnyObject) { self.viewModel.login() .flatMap {result in self.viewModel.loadProfile() } .subscribe(onNext: { profile in let trialNotExpired = self.viewModel.checkTrialNotExpired(for: profile) if trialNotExpired { let firstAppLaunch = self.viewModel.checkFirstAppLaunch() if firstAppLaunch { self.performSegue(withIdentifier: Segue.FromLoginToInstruction, sender: nil) } else { self.navigationController?.popToRootViewController(animated: true) } } else { self.showTrialExpirePopUp() } }, onError: { error in SCLAlertView().showError("Error", subTitle:error.localizedDescription) }) .disposed(by: disposeBag) } |
Note that error for both requests is handled in one place using SCLAlertView. FlatMap operator maps a result of a first request to a second request and also flattens Observables. If you use just Map instead of FlatMap, the result of chaining will be an Observable of Observables.