2017-10-28 132 views
2

顯然我是RxSwift的新手,雖然我消耗了大量文檔和演講,但我想我缺少一些基本概念。RxSwift可觀察錯誤停止鏈 - 帶Rx的Web服務,如何恢復?

在我的應用程序中,我有一個REST風格的Web服務來加載各種資源,但Web服務的基礎URL在構建/開始時未知。相反,我有一個「URL解析器」Web服務,我可以使用我的應用程序包,版本和可能的環境(「生產」,「調試」或在應用程序調試設置中輸入的任何自定義字符串)調用以獲取基礎URL,然後使用爲實際的服務。

我的想法是,我會創建2個服務,一個用於URL解析器,另一個用於實際的Web服務,它爲我提供了資源。 URL解析器將有一個Variable和一個Observable。我使用該變量來表示需要通過對URL解析器的Web服務調用刷新基礎URL。我通過觀察變量並僅對真實值進行過濾來實現此目的。服務類中的函數將變量值設置爲true(最初爲false),並在已過濾變量的觀察者中,在另一個Observable中進行Web服務調用(此示例使用虛擬JSON Web服務):

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; 
    } 
} 

現在的問題是,有時可能會發生Web服務調用失敗,我需要向用戶顯示錯誤,以便重試。我認爲onError對此有用,但它似乎永遠殺死所有用戶。

我可以把訂閱在它自己的功能和觀察者的錯誤處理程序中,我可以顯示一個警告,然後再次調用訂閱功能,像這樣:

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) 
    } 

然後在我的AppDelegate我會調用

subscribe() 
URLService.getBaseUrl() 

的問題是,所有其他觀察員喪命上的錯誤,以及但是因爲在URLService.urlObservable的唯一的其他觀察者是我的其他網絡服務類,我想我可以實現同樣的風格認購功能也在那裏。

我讀過一些人建議返回一個Result枚舉,它有兩種情況:實際結果(.success(result:T))或錯誤(.error(error:Error))。

那麼,在Rx中處理錯誤Web服務錯誤的更好方法是什麼?我不能把這個問題包裹起來,我想了解2天。任何想法或建議?

更新

它只是來到我的腦海,我可以忽略來自Web服務的錯誤調用的任何錯誤完全,而是張貼到一個全球性的「錯誤」的變量,我的應用程序代理可以觀察到顯示警報。 「錯誤」可以引用最初導致它的功能,因此可以進行重試。我仍然感到困惑,不知道該怎麼做。 :/

更新2

我想我可能找到了一個工作方案。由於我還是Rx和RxSwift的初學者,我很樂意接受改進建議。當我寫實際的代碼,我分裂我的調用鏈分爲兩個部分:

  • ,我讓Web服務調用
  • ,我點擊一個按鈕,處理Web服務的結果的一部分,部分,無論是錯誤還是成功

在我單擊按鈕並處理結果的部分中,我使用catchError並按照註釋中的建議重試。代碼如下所示:

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) 

訣竅是移動調用兩個Web服務了該隱到自己的變量,所以我可以在任何時候重新調用它。當我現在返回「userObservable」並在Web服務調用期間發生錯誤時,我可以在catchError中顯示錯誤,併爲下一次重試返回相同的「userObservable」。

目前這隻能正確處理錯誤,當他們發生在Web服務調用鏈,所以我認爲我應該讓按鈕點擊驅動程序。

+0

「問題是所有其他觀察者都因爲錯誤而死亡」 - 這可能是一個問題,但它是Rx設計的方式。一個可觀察的**可能有零個或多個OnNext,並且可能只有一個OnError或OnCompleted,此時觀察結束,並且不能返回任何更多值**。 – Enigmativity

+0

您通常會使用帶**重試**的** catch **運算符來捕獲錯誤並重試observables。 – Enigmativity

+0

啊,我會看看並重試。謝謝。我知道這是設計,所以我想知道這個:我可以在我的Web服務中創建1個觀察者。然後,我可以訂閱觀察員,轉換其數據並僅在成功時將其發送給我的變量。然後,我可以創建第二個類似的轉換,我將返回。只有在轉換成功的情況下,變量纔會被更新,而返回會將錯誤升級到我的UI,因此用戶可以處理錯誤。這是一種有效的方法嗎? – xxtesaxx

回答

0

對於來到這裏的每個人來說,好吧,您可能對Rx世界應該如何工作缺乏理解或誤解。我仍然發現它有時會讓人困惑,但我發現比我在原始問題中發佈的更好的解決方案。

在Rx中,錯誤「殺死」或者完成鏈中的所有觀察者,這實際上是一件好事。如果Web服務調用中存在預期的錯誤(如API錯誤),則應嘗試在發生它們的位置處理它們,或將它們視爲期望值。

例如,您的觀察者可以返回一個可選類型,並且訂閱者可以篩選值的存在。如果發生API調用錯誤,則返回nil。其他「錯誤處理程序」可能會過濾零值以向用戶顯示錯誤消息。

另外可行的是返回一個Result枚舉與兩種情況:.success(value:T)和.error(error:Error)。您將錯誤視爲可接受的結果,觀察者負責檢查它是否應顯示錯誤消息或成功結果值。

另一個選項,它肯定不是最好的,但它的作用是簡單地將你期望失敗的呼叫嵌套在呼叫用戶內部,這一點不能被影響。在我的情況下,這是一個按鈕點擊,導致調用一個Web服務。

在我原來的職位的「更新2」將成爲:

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) 

的MVVM視圖模型進行調用到Web serivces像這樣:

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 
     } 
} 

首先,我想只在按下按鈕時調用視圖模型signIn()函數,但由於視圖模型中不應該有UI代碼,因此我認爲提交和解散HUD是ViewController的責任。

我認爲這個設計現在非常穩固。按鈕觀察者永遠不會完成並可以繼續發送事件。此前,如果出現第二個錯誤,可能會發生按鈕觀察者死亡,並且我的日誌顯示userObservable被執行了兩次,這也不會發生。

我只是想知道是否有更好的方法來嵌套用戶。