Swift Combine 使用 dataTaskPublisher 发起网络请求 从入门到精通十
Combine 系列
- Swift Combine 从入门到精通一
- Swift Combine 发布者订阅者操作者 从入门到精通二
- Swift Combine 管道 从入门到精通三
- Swift Combine 发布者publisher的生命周期 从入门到精通四
- Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五
- Swift Combine 订阅者Subscriber的生命周期 从入门到精通六
- Swift 使用 Combine 进行开发 从入门到精通七
- Swift 使用 Combine 管道和线程进行开发 从入门到精通八
- Swift Combine 使用 sink, assign 创建一个订阅者 从入门到精通九
1. 使用 dataTaskPublisher 发起网络请求
- 目的: 一个常见的用例是从 URL 请求 JSON 数据并解码。
这可以通过使用 Combine 的 URLSession.dataTaskPublisher 搭配一系列处理数据的操作符来轻松完成。
最简单的,调用 URLSession 的 dataTaskPublisher,然后在数据到达订阅者之前使用 map 和 decode。
使用此操作的最简单例子可能是:
let myURL = URL(string: "https://postman-echo.com/time/valid?timestamp=2016-10-10")
// checks the validity of a timestamp - this one returns {"valid":true}
// matching the data structure returned from https://postman-echo.com/time/valid
fileprivate struct PostmanEchoTimeStampCheckResponse: Decodable, Hashable { // 1
let valid: Bool
}
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!) // 2
// the dataTaskPublisher output combination is (data: Data, response: URLResponse)
.map { $0.data } // 3
.decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder()) // 4
let cancellableSink = remoteDataPublisher
.sink(receiveCompletion: { completion in
print(".sink() received the completion", String(describing: completion))
switch completion {
case .finished: // 5
break
case .failure(let anError): // 6
print("received error: ", anError)
}
}, receiveValue: { someValue in // 7
print(".sink() received \(someValue)")
})
- 通常,你将有一个结构体的定义,至少遵循 Decodable 协议(即使没有完全遵循 Codable protocol)。此结构体可以只定义从网络拉取到的 JSON 中你感兴趣的字段。 不需要定义完整的 JSON 结构。
dataTaskPublisher
是从URLSession
实例化的。 你可以配置你自己的URLSession
,或者使用shared session
.- 返回的数据是一个元组:
(data: Data, response: URLResponse)
。 map 操作符用来获取数据并丢弃 URLResponse,只把 Data 沿管道向下传递。 - decode 用于加载数据并尝试解析它。 如果解码失败,它会抛出一个错误。 如果它成功,通过管道传递的对象将是来自 JSON 数据的结构体。
- 如果解码完成且没有错误,则将触发完成操作,并将值传递给
receiveValue
闭包。 - 如果发生失败(无论是网络请求还是解码),则错误将被传递到
failure
闭包。 - 只有当数据请求并解码成功时,才会调用此闭包,并且收到的数据格式将是结构体
PostmanEchoTimeStampCheckResponse
的实例。
2. 使用 dataTaskPublisher 进行更严格的请求处理
- 目的: 当 URLSesion 进行连接时,它仅在远程服务器未响应时报告错误。 你可能需要根据状态码将各种响应视为不同的错误。 为此,你可以使用 tryMap 检查 http 响应并在管道中抛出错误。
要对 URL 响应中被认为是失败的操作进行更多控制,可以对 dataTaskPublisher
的元组响应使用 tryMap
操作符。 由于 dataTaskPublisher
将响应数据和 URLResponse
都返回到了管道中,你可以立即检查响应,并在需要时抛出自己的错误。
这方面的一个例子可能看起来像:
let myURL = URL(string: "https://postman-echo.com/time/valid?timestamp=2016-10-10")
// checks the validity of a timestamp - this one returns {"valid":true}
// matching the data structure returned from https://postman-echo.com/time/valid
fileprivate struct PostmanEchoTimeStampCheckResponse: Decodable, Hashable {
let valid: Bool
}
enum TestFailureCondition: Error {
case invalidServerResponse
}
let remoteDataPublisher = URLSession.shared.dataTaskPublisher(for: myURL!)
.tryMap { data, response -> Data in // 1
guard let httpResponse = response as? HTTPURLResponse, // 2
httpResponse.statusCode == 200 else { // 3
throw TestFailureCondition.invalidServerResponse // 4
}
return data // 5
}
.decode(type: PostmanEchoTimeStampCheckResponse.self, decoder: JSONDecoder())
let cancellableSink = remoteDataPublisher
.sink(receiveCompletion: { completion in
print(".sink() received the completion", String(describing: completion))
switch completion {
case .finished:
break
case .failure(let anError):
print("received error: ", anError)
}
}, receiveValue: { someValue in
print(".sink() received \(someValue)")
})
在 上个模式 中使用了 map 操作符, 这里我们使用 tryMap,这使我们能够根据返回的内容识别并在管道中抛出错误。
- tryMap 仍旧获得元组
(data: Data, response: URLResponse)
,并且在这里定义仅返回管道中的 Data 类型。 - 在
tryMap
的闭包内,我们将响应转换为HTTPURLResponse
并深入进去,包括查看特定的状态码。 - 在这个例子中,我们希望将 200 状态码以外的任何响应视为失败。
HTTPURLResponse.statusCode
是一种Int
类型,因此你也可以使用httpResponse.statusCode > 300
等逻辑。 - 如果判断条件未满足,则会抛出我们选择的错误实例:在这个例子中,是
invalidServerResponse
。 - 如果没有出现错误,则我们只需传递
Data
以进行进一步处理。
3. 标准化 dataTaskPublisher 返回的错误
当在管道上触发错误时,不管错误发生在管道中的什么位置,都会发送 .failure
完成回调,并把错误封装在其中。
此模式可以扩展来返回一个发布者,该发布者使用此通用模式可接受并处理任意数量的特定错误。 在许多示例中,我们用默认值替换错误条件。 如果我们想要返回一个发布者的函数,该发布者不会根据失败来选择将发生什么,则同样 tryMap 操作符可以与 mapError 一起使用来转换响应对象以及转换 URLError 错误类型。
enum APIError: Error, LocalizedError { // 1
case unknown, apiError(reason: String), parserError(reason: String), networkError(from: URLError)
var errorDescription: String? {
switch self {
case .unknown:
return "Unknown error"
case .apiError(let reason), .parserError(let reason):
return reason
case .networkError(let from): // 2
return from.localizedDescription
}
}
}
func fetch(url: URL) -> AnyPublisher<Data, APIError> {
let request = URLRequest(url: url)
return URLSession.DataTaskPublisher(request: request, session: .shared) // 3
.tryMap { data, response in // 4
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.unknown
}
if (httpResponse.statusCode == 401) {
throw APIError.apiError(reason: "Unauthorized");
}
if (httpResponse.statusCode == 403) {
throw APIError.apiError(reason: "Resource forbidden");
}
if (httpResponse.statusCode == 404) {
throw APIError.apiError(reason: "Resource not found");
}
if (405..<500 ~= httpResponse.statusCode) {
throw APIError.apiError(reason: "client error");
}
if (500..<600 ~= httpResponse.statusCode) {
throw APIError.apiError(reason: "server error");
}
return data
}
.mapError { error in // 5
// if it's our kind of error already, we can return it directly
if let error = error as? APIError {
return error
}
// if it is a TestExampleError, convert it into our new error type
if error is TestExampleError {
return APIError.parserError(reason: "Our example error")
}
// if it is a URLError, we can convert it into our more general error kind
if let urlerror = error as? URLError {
return APIError.networkError(from: urlerror)
}
// if all else fails, return the unknown error condition
return APIError.unknown
}
.eraseToAnyPublisher() // 6
}
APIError
是一个错误类型的枚举,我们在此示例中使用该枚举来列举可能发生的所有错误。.networkError
是 APIError 的一个特定情况,当 URLSession.dataTaskPublisher 返回错误时我们将把错误转换为该类型。- 我们使用标准
dataTaskPublisher
开始生成此发布者。 - 然后,我们将路由到 tryMap 操作符来检查响应,根据服务器响应创建特定的错误。
- 最后,我们使用 mapError 将任何其他不可忽视的错误类型转换为通用的错误类型
APIError
。
参考
https://heckj.github.io/swiftui-notes/index_zh-CN.html
代码
https://github.com/heckj/swiftui-notes