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

时间:2019-05-09 04:33:20

标签: swift mvvm error-handling rx-swift

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

我的想法是在 ViewController 中可以订阅两个可观察对象,并在按下按钮时,其中之一将显示成功响应,而其中之一将显示错误。只是不太确定如何实现。

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)

    }

2 个答案:

答案 0 :(得分:1)

您可以通过实现偶数序列来做到这一点:

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

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

答案 1 :(得分:0)

因此,我还找到了实现所需目标的方法,即将成功输出和错误输出分别分配给两个不同的可观察对象。通过使用 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 { "\($0)" }

然后,只需在ViewController中订阅每个这些可观察对象。

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