本文主要讲解如何处理pipline中的错误。
catch
上图已经非常明确的表达了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,下边的代码将组合使用.catch
和flatMap
。
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
assertNoFailure主要用于test目的,一旦test中的pipline发送了.failure
,就会触发错误,这个可讲的不多。
retry
如上图所示,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
上图只是演示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
}