RxSwift 处理错误例子 - 上传图片

在本节中,你可以根据这个上传图片的例子了解到在具体场景中处理错误的方式,在哪里抛出错误,在哪里处理错误,又该如何处理错误。

一段程序的执行往往伴随着多条分支,我们的代码逻辑会走向很多个分支,但一般我们只有一个分支是正常的情况。比如一个上传文件到静态对象储存服务中,并将上传成功返回的链接传到我们的服务器。在这一过程中,遇到的任何错误都不再进行下去,我们需要确定如何处理这些错误的分支。

这里我们以这个上传图片为例,完成一个相对全面的处理流程的代码。

流程图如下:

为了更简单地展示这个例子,我们在网络请求中做一些修改,上传图片和保存图片链接的请求均使用 httpbin 服务。

比如一个上传的请求是 https://httpbin.org/status/\$(statueCode)?type=upload ,我们直接使用状态码表示结果。暂时只处理 503 200 599 408 四个状态码。

创建枚举 StatusCodeError 和方法 statusCode ,方便后面调用,代码如下:

public enum StatusCodeError: Swift.Error {
    case code(Int)

    public var code: Int {
        switch self {
        case let .code(code):
            return code
        }
    }
}

extension Reactive where Base: URLSession {

    public func statusCode(url: URL) -> Observable<Int> {
        return response(request: URLRequest(url: url))
            .map { (response, data) -> Int in
                if 200 ..< 300 ~= response.statusCode {
                    return response.statusCode
                }
                else {
                    throw StatusCodeError.code(response.statusCode)
                }
        }
    }

}

我们先来完成一个只处理正常分支的代码:

uploadImageButton.rx.tap.asObservable()
    .map { [200, 503, 599].random! }
    .map { URL(string: "https://httpbin.org/status/\($0)?type=upload")! }
    .flatMap { URLSession.shared.rx.statusCode(url: $0) }
    .map { _ in [200, 503, 408].random! }
    .map { URL(string: "https://httpbin.org/status/\($0)?type=request")! }
    .flatMap { URLSession.shared.rx.statusCode(url: $0) }
    .subscribe(onNext: { _ in
        HUD.showMessage("上传成功")
    })
    .disposed(by: disposeBag)

当我们运行这段代码时,我们可能得到如下输出:

curl -X GET
"https://httpbin.org/status/200?type=upload" -i -v
Success (1823ms): Status 200
curl -X GET
"https://httpbin.org/status/408?type=request" -i -v
Failure (402ms): Status 408
Received unhandled error: /Users/Qing/Documents/GitHub/rx-sample-code/HandleError/UploadImageTestViewController.swift:84:viewDidLoad() -> code(408)

这说明我们有一些错误没有处理。

如果你没有得到上面的输出,你需要使用 Debug 版本的 RxSwift 。

为了避免忘记某些分支没有处理错误,我们这次用 Driver 完成这个需求。

当服务器不可用或者用户选择取消,我们需要展示相应的错误信息,我们可以直接将这个错误展示出来并返回一个 empty ,代码如下:

extension ObservableConvertibleType {

    public func asDriverJustShowErrorMessage() -> Driver<E> {
        return self.asObservable()
            .asDriver(onErrorRecover: { (error) -> Driver<E> in
                HUD.showMessage(error.localizedDescription)
                return Driver.empty()
            })
    }

}

现在我们可以将 Observable 转换成 Driver ,目前所有的错误均以提示的方式处理:

uploadImageButton.rx.tap.asDriver()
    .map { [200, 503, 599].random! }
    .map { URL(string: "https://httpbin.org/status/\($0)?type=upload")! }
    .flatMap { url -> Driver<()> in
        URLSession.shared.rx.statusCode(url: url)
            .map { _ in }
            .asDriverJustShowErrorMessage()
    }
    .map { _ in [200, 503, 408].random! }
    .map { URL(string: "https://httpbin.org/status/\($0)?type=request")! }
    .flatMap { url -> Driver<()> in
        URLSession.shared.rx.statusCode(url: url)
            .map { _ in }
            .asDriverJustShowErrorMessage()
    }
    .drive(onNext: {
        HUD.showMessage("上传成功")
    })
    .disposed(by: disposeBag)

此时的错误提示可能不大友好,我们可以为 StatusCodeError 增加一个扩展:

extension StatusCodeError: LocalizedError {

    public var errorDescription: String? {
        switch self {
        case let .code(code):
            return "错误的状态码\(code)"
        }
    }

}

我们还需要添加一个弹窗让用户决定是否需要重新尝试,使用 retryWhen 是个比较好的方式:

URLSession.shared.rx.statusCode(url: url)
    .map { _ in }
    .retryWhen { [unowned self] (errorObservable: Observable<StatusCodeError>) -> Observable<()> in
        errorObservable
            .flatMap { error -> Observable<()> in
                switch error {
                case let .code(code):
                    return showAlert(title: "上传图片时遇到了一个错误,是否重试?", message: "错误的状态码\(code)", for: self)
                        .map { isEnsure in
                            if isEnsure {
                                return ()
                            } else {
                                throw error
                            }
                    }
                }
        }
    }
    .asDriverJustShowErrorMessage()

然而这可能会和我们预期的结果有些偏差,当错误是 599 时,无论如何地重试,错误依然是 599 ,因为这里我们使用的是 httpbin ,请求的链接是 https://httpbin.org/status/599?type=upload ,每次返回的结果自然都是 599 。

为了让每次请求的 url 都是随机的,我们需要稍加改造:

Observable
    .deferred({ () -> Observable<Int> in
        let url = URL(string: "https://httpbin.org/status/\([200, 503, 599].random!)?type=upload")!
        return URLSession.shared.rx.statusCode(url: url)
    })
    .map { _ in }
    .retryWhen { (errorObservable: Observable<StatusCodeError>) -> Observable<()> in
        errorObservable
            .flatMap { error -> Observable<()> in
                switch error {
                case let .code(code):
                    return showAlert(title: "上传图片时遇到了一个错误,是否重试?", message: "错误的状态码\(code)", for: self)
                        .map { isEnsure in
                            if isEnsure {
                                return ()
                            } else {
                                throw error
                            }
                    }
                }
        }
    }
    .asDriverJustShowErrorMessage()

目前我们是对所有的 StatusCodeError 类型错误均又用户决定处理方案,当 code 为 503 时,应当直接结束本次流程,代码如下:

.retryWhen { (errorObservable: Observable<StatusCodeError>) -> Observable<()> in
    errorObservable
        .flatMap { error -> Observable<()> in
            switch error {
            case let .code(code):
                if code == 503 { // 503 时,直接返回错误,不进行处理
                    return Observable.error(error)
                }
                return showAlert(title: "上传图片时遇到了一个错误,是否重试?", message: "错误的状态码\(code)", for: self)
                    .map { isEnsure in
                        if isEnsure {
                            return ()
                        } else {
                            throw error
                        }
                }
            }
    }
}

到此我们完成了全部需要的处理过程,但应用到实际中可能还需要一些优化,请求状态和提示文案。

处理请求状态仍然使用 ActivityIndicator ,需要注意的是,应当添加 .trackActivity(activityIndicator)retryWhen 的前面:

Observable
    .deferred({ () -> Observable<Int> in
        let url = URL(string: "https://httpbin.org/status/\([200, 503, 599].random!)?type=upload")!
        return URLSession.shared.rx.statusCode(url: url)
    })
    .map { _ in }
    .trackActivity(activityIndicator)

否则你可能会在弹窗出现时,视图仍然表示请求在进行中。

为了让提示更清晰,我们可以加入用户取消操作的提示,而不是请求错误的提示。

我们需要创建一个可以自定义错误信息的 Error 类型:

public enum CustomMessageError: Swift.Error {
    case message(String)
}

extension CustomMessageError: LocalizedError {

    public var errorDescription: String? {
        switch self {
        case let .message(message):
            return message
        }
    }

}

当用户取消时,抛出该错误即可:

.retryWhen { (errorObservable: Observable<StatusCodeError>) -> Observable<()> in
    errorObservable
        .flatMap { error -> Observable<()> in
            switch error {
            case let .code(code):
                if code == 503 {
                    return Observable.error(error)
                }
                return showAlert(title: "上传图片时遇到了一个错误,是否重试?", message: "错误的状态码\(code)", for: self)
                    .map { isEnsure in
                        if isEnsure {
                            return ()
                        } else {
                            throw CustomMessageError.message("您取消了本次的上传操作")
                        }
                }
            }
    }
}

完整代码你可以在 HandleError 中 UploadImageTestViewController (opens new window) 找到。