如何在 MVVM 中使用 RxSwift 正确处理来自 api 请求的错误?

How to handle error from api request properly with RxSwift in MVVM?

所以,我有一个按钮,点击它会发出 API 请求。当API请求returns出错时,如果我的理解是正确的,序列将终止,不会记录后续动作。我该如何正确处理这个问题,以便在点击按钮时我仍然可以发出另一个 API 请求。

我的想法是有两个我可以在 ViewController 中订阅的 Observable,并且在按下按钮时,其中一个将打印成功响应,另一个将打印成功响应将打印错误。只是不太确定我该如何实现。

PS:在Post.swift中,我特意将id设置为String类型,使响应失败.它应该是 Int 类型。

Post.swift

import Foundation
struct Post: Codable {
    let id: String
    let title: String
    let body: String
    let userId: Int
}

APIClient.swift

class APIClient {

    static func request<T: Codable> (_ urlConvertible: URLRequestConvertible, decoder: JSONDecoder = JSONDecoder()) -> Observable<T> {

        return Observable<T>.create { observer in

            URLCache.shared.removeAllCachedResponses()

            let request = AF.request(urlConvertible)
                .responseDecodable (decoder: decoder) { (response: DataResponse<T>) in

                switch response.result {
                case .success(let value):
                    observer.onNext(value)
                    observer.onCompleted()
                case .failure(let error):
                    switch response.response?.statusCode {
                    default:
                        observer.onError(error)
                    }
                }
            }

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

}

PostService.swift

class PostService {
    static func getPosts(userId: Int) -> Observable<[Post]> {
        return APIClient.request(PostRouter.getPosts(userId: userId))
    } 
}

ViewModel.swift

class LoginLandingViewModel {

    struct Input {
        let username: AnyObserver<String>
        let nextButtonDidTap: AnyObserver<Void>
    }

    struct Output {
        let apiOutput: Observable<Post>
        let invalidUsername: Observable<String>
    }

    // MARK: - Public properties

    let input: Input
    let output: Output

    // Inputs

    private let usernameSubject = BehaviorSubject(value: "")
    private let nextButtonDidTapSubject = PublishSubject<Void>()

    // MARK: - Init

    init() {

        let minUsernameLength = 4

        let usernameEntered = nextButtonDidTapSubject
            .withLatestFrom(usernameSubject.asObservable())

        let apiOutput = usernameEntered
            .filter { text in
                text.count >= minUsernameLength
            }
            .flatMapLatest { _ -> Observable<Post> in
                PostService.getPosts(userId: 1)
                    .map({ posts -> Post in
                        return posts[0]
                    })

            }

        let invalidUsername = usernameEntered
            .filter { text in
                text.count < minUsernameLength
            }
            .map { _ in "Please enter a valid username" }


        input = Input(username: usernameSubject.asObserver(),
                      nextButtonDidTap: nextButtonDidTapSubject.asObserver())

        output = Output(apiOutput: apiOutput,
                        invalidUsername: invalidUsername)

    }

    deinit {
        print("\(self) dellocated")
    }


}

ViewController

private func configureBinding() {

        loginLandingView.usernameTextField.rx.text.orEmpty
            .bind(to: viewModel.input.username)
            .disposed(by: disposeBag)

        loginLandingView.nextButton.rx.tap
            .debounce(0.3, scheduler: MainScheduler.instance)
            .bind(to: viewModel.input.nextButtonDidTap)
            .disposed(by: disposeBag)

        viewModel.output.apiOutput
            .subscribe(onNext: { [unowned self] post in
                print("Valid username - Navigate with post: \(post)")
                })
            .disposed(by: disposeBag)


        viewModel.output.invalidUsername
            .subscribe(onNext: { [unowned self] message in
                self.showAlert(with: message)
            })
            .disposed(by: disposeBag)

    }

您可以通过具体化偶数序列来做到这一点:

第一步:在网络通话中使用 URLSession.shared 上的 .rx 分机

func networkCall(...) -> Observable<[Post]> {
    var request: URLRequest = URLRequest(url: ...)
    request.httpMethod = "..."
    request.httpBody = ...

    URLSession.shared.rx.response(request)
        .map { (response, data) -> [Post] in
            guard let json = try? JSONSerialization.jsonObject(with: data, options: []),
                let jsonDictionary = json as? [[String: Any]]
                else { throw ... }    // Throw some error here

            // Decode this dictionary and initialize your array of posts here
            ...
            return posts
        }
}

第二步,具体化您的可观察序列

viewModel.networkCall(...)
    .materialize()
    .subscribe(onNext: { event in
        switch event {
            case .error(let error):
                // Do something with error
                break
            case .next(let posts):
                // Do something with posts
                break
            default: break
        }
    })
    .disposed(by: disposeBag)

这样,即使您在网络调用中抛出错误,您的可观察序列也永远不会终止,因为 .error 事件被转换为 .next 事件但状态为 .error.

所以我也找到了实现我想要的方法,即将成功输出和错误输出分别分配给两个不同的可观察对象。通过使用 RxSwiftExt,有两个额外的运算符,elements()errors() 可用于物化以获取元素的可观察对象。

这是我的做法,

ViewModel.swift

let apiOutput = usernameEntered
            .filter { text in
                text.count >= minUsernameLength
            }
            .flatMapLatest { _ in
                PostService.getPosts(userId: 1)
                    .materialize()
            }
            .share()

let apiSuccess = apiOutput
    .elements()

let apiError = apiOutput
    .errors()
    .map { "\([=10=])" }

然后,只需订阅 ViewController 中的每个 observables。

作为参考:http://adamborek.com/how-to-handle-errors-in-rxswift/