Combine之Operator(Error Handing 错误处理)

765 阅读3分钟

github.com/agelessman/…

本文主要讲解如何处理pipline中的错误。

catch

image.png

上图已经非常明确的表达了catch的核心用法,从宏观方向来看,它捕获publisher发送的异常后返回另一个publisher,这个新的publisher的目的就是替换掉旧的publisher,pipline会继续执行。

接下来,我们用3种不同的使用场景来更加深入的了解catch

第一种:

enum MyError: Error {
    case custom
}

let publisher = PassthroughSubject<String, Error>()

publisher
    .catch { err in
        Just("!!!")
    }
    .sink(receiveCompletion: { completion in
        print(completion)
    }, receiveValue: { value in
        print(value)
    })
    .store(in: &cancellables)

publisher.send("你")
publisher.send("好")
publisher.send(completion: Subscribers.Completion.failure(MyError.custom))
publisher.send("吗")

正是上边的代码生成了本小节的示意图,我们选用PassthroughSubject作为publisher的目的是方便我们发送数据,可以看出,当我们发送了一个.failure事件后,会触发catch,catch接收一个闭包,该闭包返回一个新的publisher,在上边的代码中,我们返回的是一个Just("!!!")。所以最后的结果就是旧的publisher取消,新的Just执行,但Just只发送一次数据就结束了。

因此,打印结果如下:

你
好
!!!
finished

第二种,我们使用官方的例子:

[1, 2, 3, 0, 5]
    .publisher
    .tryLast {
        guard $0 != 0 else { throw MyError.custom }
        return true
    }
    .catch { err in
        Just(-1)
    }
    .sink(receiveCompletion: { completion in
        print(completion)
    }, receiveValue: { value in
        print(value)
    })
    .store(in: &cancellables)

我们使用tryLast来获取pipline中的最后一个数据,上边代码有一点令人疑惑的地方:

.tryLast {
    guard $0 != 0 else { throw MyError.custom }
    return true
}

表面上看最后一个值是5,不会触发throw,其实不然,.tryLast在计算结果的时候,首先等待publisher发送完数据,然后从头到尾遍历数据,最终返回闭包结果为true的最后一个值。

每遍历一个数据,都会调用一次闭包,当便利到0的时候,就触发了异常。上边代码的打印结果为:

-1
finished

第三种,我们将使用.catch返回publisher的这一特性,在Combine中,flatMap用于做数据映射,它的闭包要求返回一个pulisher,下边的代码将组合使用.catchflatMap

let testURL = URL(string: "https://xxxx.com")!
Just(testURL)
    .flatMap {
        URLSession.shared.dataTaskPublisher(for: $0)
            .tryMap { data, response -> Data in
                guard let httpResp = response as? HTTPURLResponse, httpResp.statusCode == 200 else {
                    throw NetworkError.invalidResponse
                }
                return data
            }
            .decode(type: Student.self, decoder: JSONDecoder())
            .catch { err in
                Just(Student(name: "", age: 0))
            }
    }
    .eraseToAnyPublisher()

上边的代码是比较常用的,网络请求出错的情况下,我们返回默认的一份数据。

catch有一个扩展,名为tryCatch,它允许我们主动throw异常,官方的一个例子是如果网络受限,就替换弱网URL,代码如下:

func adaptiveLoader(regularURL: URL, lowDataURL: URL) -> AnyPublisher<Data, Error> {
    var request = URLRequest(url: regularURL) 
    request.allowsConstrainedNetworkAccess = false 
    return URLSession.shared.dataTaskPublisher(for: request) 
        .tryCatch { error -> URLSession.DataTaskPublisher in 
            guard error.networkUnavailableReason == .constrained else {
               throw error
            }
            return URLSession.shared.dataTaskPublisher(for: lowDataURL) 
        .tryMap { data, response -> Data in
            guard let httpResponse = response as? HTTPUrlResponse, 
                   httpResponse.statusCode == 200 else {
                       throw MyNetworkingError.invalidServerResponse
            }
            return data
}
.eraseToAnyPublisher() 

可以看出,如果网络错误的原因不是网络限制,直接抛出error结束pipline,如果是网络受限,则使用弱网请求。

assertNoFailure

vvv.jpg

assertNoFailure主要用于test目的,一旦test中的pipline发送了.failure,就会触发错误,这个可讲的不多。

retry

image.png

如上图所示,retry实现了重试的功能,当上游的publisher发送了错误后,retry可以实现重新对publisher进行订阅。

它的主要使用场景为对某个网络请求进行重试操作,代码如下:

func loadData() {
    let myURL = URL(string: "https://www.example.com")

    URLSession.shared.dataTaskPublisher(for: myURL!)
        .retry(3)
        .map({ (page) -> WebSiteData in
            return WebSiteData(rawHTML: String(decoding: page.data, as: UTF8.self))
        })
        .catch { error in
            return Just(WebSiteData(rawHTML: "<HTML>Unable to load page - timed out.</HTML>"))
        }
        .assign(to: \.data, on: self)
        .store(in: &cancellables)
}

mapError

image.png

上图只是演示mapError的核心思想,一般情况下,pipline遇到错误就会立刻终止。

在真实的开发中,会存在各种各样的错误,这些错误可能来源于网络,也可能来源于其他的错误,mapError的核心功能就是能够对pipline中的错误进行映射。

举个简单的例子,如果我们自己封装一个像Alamofire这样的网络框架,我们就需要识别网络请求过程中的各种各样的Error,就像Alamofire专门有一个AFError.swift的文件用于解析错误。那么在pipline中,我们的目的就是把其他的错误映射到我们自定义的错误类型,代码如下:

.mapError{ error -> MyApiError in
    if let err = error as? MyApiError {
        return err
    }
    if let urlErr = error as? URLError {
        return MyApiError.invalidURL(urlError: urlErr)
    }
    return MyApiError.unknown
}