Как правильно обработать ошибку из API-запроса с помощью RxSwift в MVVM? - PullRequest
0 голосов
/ 09 мая 2019

Итак, у меня есть кнопка, и после нажатия я сделаю запрос API. Когда запрос API возвращает ошибку, если мое понимание верно, последовательность будет прервана, и никакие последующие действия не будут записаны. Как мне обработать это правильно, чтобы я мог сделать еще один запрос API при нажатии кнопки.

Мои мысли состоят в том, чтобы иметь две наблюдаемые, на которые я могу подписаться в ViewController и при нажатии кнопки, одна из них напечатает успешный ответ, а другая - ошибку. Просто не совсем уверен, как мне этого добиться.

PS: в Post.swift я специально установил id как String type, чтобы не дать ответ. Это должен быть тип 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 ]

1 голос
/ 09 мая 2019

Вы можете сделать это материализовав четную последовательность:

Первый шаг: используйте .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.

0 голосов
/ 10 мая 2019

Таким образом, я также нашел способ достичь того, что хотел, а именно назначить выходные данные об успехе и об ошибке на две разные наблюдаемые соответственно.Используя 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/

...