当前位置: 首页 > article >正文

Swift Combine 使用 dataTaskPublisher 发起网络请求 从入门到精通十

Combine 系列

  1. Swift Combine 从入门到精通一
  2. Swift Combine 发布者订阅者操作者 从入门到精通二
  3. Swift Combine 管道 从入门到精通三
  4. Swift Combine 发布者publisher的生命周期 从入门到精通四
  5. Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五
  6. Swift Combine 订阅者Subscriber的生命周期 从入门到精通六
  7. Swift 使用 Combine 进行开发 从入门到精通七
  8. Swift 使用 Combine 管道和线程进行开发 从入门到精通八
  9. 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)")
    })
  1. 通常,你将有一个结构体的定义,至少遵循 Decodable 协议(即使没有完全遵循 Codable protocol)。此结构体可以只定义从网络拉取到的 JSON 中你感兴趣的字段。 不需要定义完整的 JSON 结构。
  2. dataTaskPublisher 是从 URLSession 实例化的。 你可以配置你自己的 URLSession,或者使用 shared session.
  3. 返回的数据是一个元组:(data: Data, response: URLResponse)。 map 操作符用来获取数据并丢弃 URLResponse,只把 Data 沿管道向下传递。
  4. decode 用于加载数据并尝试解析它。 如果解码失败,它会抛出一个错误。 如果它成功,通过管道传递的对象将是来自 JSON 数据的结构体。
  5. 如果解码完成且没有错误,则将触发完成操作,并将值传递给 receiveValue 闭包。
  6. 如果发生失败(无论是网络请求还是解码),则错误将被传递到 failure 闭包。
  7. 只有当数据请求并解码成功时,才会调用此闭包,并且收到的数据格式将是结构体 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,这使我们能够根据返回的内容识别并在管道中抛出错误。

  1. tryMap 仍旧获得元组 (data: Data, response: URLResponse),并且在这里定义仅返回管道中的 Data 类型。
  2. tryMap 的闭包内,我们将响应转换为 HTTPURLResponse 并深入进去,包括查看特定的状态码。
  3. 在这个例子中,我们希望将 200 状态码以外的任何响应视为失败。HTTPURLResponse.statusCode 是一种 Int 类型,因此你也可以使用 httpResponse.statusCode > 300 等逻辑。
  4. 如果判断条件未满足,则会抛出我们选择的错误实例:在这个例子中,是 invalidServerResponse
  5. 如果没有出现错误,则我们只需传递 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
}
  1. APIError 是一个错误类型的枚举,我们在此示例中使用该枚举来列举可能发生的所有错误。
  2. .networkError 是 APIError 的一个特定情况,当 URLSession.dataTaskPublisher 返回错误时我们将把错误转换为该类型。
  3. 我们使用标准 dataTaskPublisher 开始生成此发布者。
  4. 然后,我们将路由到 tryMap 操作符来检查响应,根据服务器响应创建特定的错误。
  5. 最后,我们使用 mapError 将任何其他不可忽视的错误类型转换为通用的错误类型 APIError

参考

https://heckj.github.io/swiftui-notes/index_zh-CN.html

代码

https://github.com/heckj/swiftui-notes


http://www.kler.cn/a/233947.html

相关文章:

  • 单例模式懒汉式、饿汉式(线程安全)
  • 安全合规遇 AI 强援:深度驱动行业发展新引擎 | 倍孜网络CEO聂子尧出席ICT深度观察报告会!
  • Xshell 和 Xftp 更新提示问题的解决方法及分析
  • 美国站群服务器如何帮助实现有效的多域名管理?
  • nvidia docker, nvidia docker2, nvidia container toolkits区别
  • Ubuntu20.04 交叉编译Qt5.15.15 for rk3588
  • 2024年腾讯云4核8G12M服务器性能测评,适合哪些使用场景?
  • 【Make编译控制 08】CMake动静态库
  • MySQL进阶查询篇(7)-触发器的创建和使用
  • 【leetcode热题100】 格雷编码
  • 从信息隐藏到功能隐藏
  • 动态水印怎么加 怎么去除动态水印 视频剪辑软件 会声会影安激活序列号 会声会影怎么剪辑视频
  • spring boot(2.4.x之前版本)和spring cloud项目中配置文件的作用
  • Movie review —— The Imitation Game
  • 生成树技术华为ICT网络赛道
  • 滑块识别验证
  • 2、卷积和ReLU激活函数
  • Rust基础拾遗--进阶
  • Linux 命令行的世界 :3.探索操作系统
  • 【心得】关于STM32中RTC的校准方法
  • 【数学建模】【2024年】【第40届】【MCM/ICM】【B题 搜寻潜水器】【解题思路】
  • 离线数仓(一)【数仓概念、需求架构】
  • 回归预测模型:MATLAB多项式回归
  • 中国判决生效,诺基亚全面与中国手机签署授权协议,降低专利费
  • STM32的ADC电压采集
  • Python和Java的区别(不断更新)