4

obviously I am new to RxSwift and though I consumed a lot of documentations and speeches, I think I am missing some fundamental concepts.

In my app I have a RESTful web service to load various resources but the base url of the web service is unknown at build/start time. Instead I have a "URL resolver" web service which I can call with my apps bundle, version and possible environment ("production", "debug" or any custom string entered in the apps debug settings) to obtain the base url I then use for the actual service.

My thinking was that I would create 2 services, one for the URL resolver and one for the actual web service which gives me my resources. The URL resolver would have a Variable and a Observable. I use the variable to signal the need to refresh the base url via a web service call to the URL resolver. I do this by observing the variable and filter only for true values. A function in the service class set the variables value to true (initially it is false) and inside an observer of the filtered variable, I make the web service call in another Observable (this example uses a dummy JSON web service):

import Foundation
import RxSwift
import Alamofire

struct BaseURL: Codable {
    let title: String
}

struct URLService {
    private static var counter = 0
    private static let urlVariable: Variable<Bool> = Variable(false)
    static let urlObservable: Observable<BaseURL> = urlVariable.asObservable()
        .filter { counter += 1; return $0 }
        .flatMap { _ in
            return Observable.create { observer in
                let url = counter < 5 ? "https://jsonplaceholder.typicode.com/posts" : ""
                let requestReference = Alamofire.request(url).responseJSON { response in
                    do {
                        let items = try JSONDecoder().decode([BaseURL].self, from: response.data!)
                        observer.onNext(items[0])
                    } catch {
                        observer.onError(error)
                    }
                }

                return Disposables.create() {
                    requestReference.cancel()
                }
            }
    }

    static func getBaseUrl() {
        urlVariable.value = true;
    }

    static func reset() {
        counter = 0;
    }
}

Now the problem is that sometimes it can happen that a web service call fails and I would need to show the error to the user so a retry can be made. I thought that the onError was useful for this but it seems to kills all the subscribers forever.

I could put the subscribing in its own function and inside the error handler of the Observer, I could show a alert and then call the subscribe function again like so:

func subscribe() {
        URLService.urlObservable.subscribe(onNext: { (baseURL) in
            let alert = UIAlertController(title: "Success in Web Service", message: "Base URL is \(baseURL.title)", preferredStyle: .alert)
            let actionYes = UIAlertAction(title: "Try again!", style: .default, handler: { action in
                URLService.getBaseUrl()
            })
            alert.addAction(actionYes)
            DispatchQueue.main.async {
                let alertWindow = UIWindow(frame: UIScreen.main.bounds)
                alertWindow.rootViewController = UIViewController()
                alertWindow.windowLevel = UIWindowLevelAlert + 1;
                alertWindow.makeKeyAndVisible()
                alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
            }
        }, onError: { error in
            let alert = UIAlertController(title: "Error in Web Service", message: "Something went wrong: \(error.localizedDescription)", preferredStyle: .alert)
            let actionYes = UIAlertAction(title: "Yes", style: .default, handler: { action in
                URLService.reset()
                self.subscribe()
            })
            alert.addAction(actionYes)
            DispatchQueue.main.async {
                VesselService.reset()
                let alertWindow = UIWindow(frame: UIScreen.main.bounds)
                alertWindow.rootViewController = UIViewController()
                alertWindow.windowLevel = UIWindowLevelAlert + 1;
                alertWindow.makeKeyAndVisible()
                alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
            }
        }).disposed(by: disposeBag)
    }

Then in my AppDelegate I would call

subscribe()
URLService.getBaseUrl()

The problem is that all other observers get killed on an error as well but since the the only other observer on the URLService.urlObservable is my other web service class, I guess I could implement the same style subscribe function in there as well.

I read that some people suggest to return a Result enum which has 2 cases: the actual result (.success(result: T)) or an error (.error(error: Error)).

So what is the better way of handling errors web service errors in Rx? I cant wrap my head around this problem and I'm trying for 2 days to understand it. Any ideas or suggestions?

Update

It just came to my mind that I could ignore errors from the web service calls completely and instead post any error to a global "error" variable which my app delegate could observe to show alerts. The "error" could reference the function which initially caused it so a retry could be made. I'm still confused and not sure what I should do. :/

Update 2

I think I might found a working solution. As I am still a beginner to Rx and RxSwift, I'm happy to take improvement suggestions. As I was writing the actual code, I splitted my call chain in two parts:

  • The part where I make the web service calls
  • The part where I click a button and process the result of the web service, whether it is an error or a success

In the part where I click the button and process the result, I use catchError and retry as suggested in the comments. The code looks like this:

let userObservable = URLService
    .getBaseUrl(environment: UserDefaults.standard.environment) //Get base url from web service 1
    .flatMap({ [unowned self] baseURL -> Observable<User> in
        UserService.getUser(baseURL: baseURL,
                            email: self.usernameTextField.text!,
                            password: self.passwordTextField.text!) //Get user from web service 2 using the base url from webservice 1
    })


signInButton
    .rx
    .tap
    .throttle(0.5, scheduler: MainScheduler.instance)
    .flatMap({ [unowned self] () -> Observable<()> in
        Observable.create { observable in
            let hud = MBProgressHUD.present(withTitle: "Signing in...");
            self.hud = hud
            observable.onNext(())
            return Disposables.create {
                hud?.dismiss()
            }
        }
    })
    .flatMap({ () -> Observable<User> in
        return userObservable
    })
    .catchError({ [unowned self] error -> Observable<User> in
        self.hud?.dismiss()
        self.handleError(error)
        return userObservable
    })
    .retry()
    .subscribe(onNext: { [unowned self] (user) in
        UserDefaults.standard.accessToken = user.accessToken
        UserDefaults.standard.tokenType = user.tokenType
        self.hud?.dismiss()
    })
    .disposed(by: disposeBag)

The trick was to move the call to the two web services out of the cain into their own variable so I can re-call it at any time. When I now return the "userObservable" and an error happens during the web service call, I can show the error in the catchError and return the same "userObservable" for the next retry.

At the moment this only properly handles errors when they occur in the web service call chain so I think I should make the button tap a driver.

xxtesaxx
  • 6,175
  • 2
  • 31
  • 50
  • "The problem is that all other observers get killed on an error" - that might be a problem, but it is the way Rx is designed. An observable **may have zero or more OnNext, and may be followed by only one of either an OnError or a OnCompleted, at which point the observable is completed and cannot return any more values**. – Enigmativity Oct 29 '17 at 01:15
  • You would normally use the **catch** operator with **retry** to catch errors and retry observables. – Enigmativity Oct 29 '17 at 01:16
  • Ah, I will have a look at catch and retry. Thanks. I understand that this is the design so I was wondering about this: I could create 1 observer inside my web service. Then I could subscribe to the observer, transform its data and signal it to my variable only when it was successful. Then I could create a second, similar transformation which I return. The variable would only get updated when the transofrmation was successful while the return would escalate the error up to my UI so the user can handle the error. Is that a valid approach? – xxtesaxx Oct 29 '17 at 01:24
  • There's almost always a way to do it cleanly in a single observable. Don't introduce subjects and side-effects unless you've exhausted all other approaches. It's just like good coding - try to encapsulate your error handling within blocks of code so that the observable never has to respond to errors. – Enigmativity Oct 29 '17 at 03:00
  • @xxtesaxx perhaps you can use *.switchLatest()* as described here - https://stackoverflow.com/a/47040856/856588 – Maxim Volgin Nov 01 '17 at 14:14

1 Answers1

3

Okay so for everyone who comes here, you probably have a lack of understanding or a misconception of how the Rx world is supposed to work. I still find it sometimes confusing but I found a way better solution than what I posted in my original question.

In Rx, a error "kills" or rather completes all observers in the chain and that is actually a good thing. If there are expected errors like API error in web service calls, you should either try to handle them where they occur or treat them like expected values.

For example, your observer could return a optional type and subscribers could filter for the existence of values. If an error in the API call occurs, return nil. Other "error handlers" could filter for nil values to display error messages to the user.

Also viable is to return a Result enum with two cases: .success(value: T) and .error(error: Error). You treat the error as a acceptable result and the observer is responsible for checking if it should display a error message or the success result value.

Yet another option, which surely is not the best as well but works it to simply nest the call which you expect to fail inside the subscriber of the call which must not be affected. In my case that is a button tap which causes a call to a web service.

The "Update 2" of my original post would become:

    signInButton.rx.tap.throttle(0.5, scheduler: MainScheduler.instance)
        .subscribe(onNext: { [unowned self] () in
            log.debug("Trying to sign user in. Presenting HUD")
            self.hud = MBProgressHUD.present(withTitle: "Signing in...");
            self.viewModel.signIn()
                .subscribe(onNext: { [unowned self] user in
                    log.debug("User signed in successfully. Dismissing HUD")
                    self.hud?.dismiss()
                }, onError: { [unowned self] error in
                    log.error("Failed to sign user in. Dismissing HUD and presenting error: \(error)")
                    self.hud?.dismiss()
                    self.handleError(error)
            }).disposed(by: self.disposeBag)
        }).disposed(by: self.disposeBag)

The MVVM view model makes the calls to the web serivces like so:

func signIn() -> Observable<User> {
    log.debug("HUD presented. Loading BaseURL to sign in User")
    return URLService.getBaseUrl(environment: UserDefaults.standard.environment)
        .flatMap { [unowned self] baseURL -> Observable<BaseURL> in
            log.debug("BaseURL loaded. Checking if special env is used.")
            if let specialEnv = baseURL.users[self.username.value] {
                log.debug("Special env is used. Reloading BaseURL")
                UserDefaults.standard.environment = specialEnv
                return URLService.getBaseUrl(environment: specialEnv)
            } else {
                log.debug("Current env is used. Returning BaseURL")
                return Observable.just(baseURL)
            }
        }
        .flatMap { [unowned self] baseURL -> Observable<User> in
            log.debug("BaseURL to use is: \(baseURL.url). Now signing in User.")
            let getUser = UserService.getUser(baseURL: baseURL.url, email: self.username.value, password: self.password.value).share()
            getUser.subscribe(onError: { error in
                UserDefaults.standard.environment = nil
            }).disposed(by: self.disposeBag)
            return getUser
        }
        .map{ user in
            UserDefaults.standard.accessToken = user.accessToken
            UserDefaults.standard.tokenType = user.tokenType
            return user
        }
}

First I was thinking to only call the view models signIn() function when pressing the button but since there should be no UI code in the view model, I figured that presenting and dismissing the HUD is the responsibility of the ViewController.

I think this design is now pretty solid. The button observer never completes and can continue to send events forever. Earlier, if there was a second error, it might happen that the button observer died and my logs showed that the userObservable was executed twice, which must also not be happen.

I just wonder if there is a better way then nesting the subscribers.

xxtesaxx
  • 6,175
  • 2
  • 31
  • 50
  • Thank you so much, I was looking how to connect VC->VM->API layers. Do you have any new suggestions ? – Darko Feb 20 '19 at 19:54