Networking

网络

Language: Swift 5 Platform: iOS 13+ SPM compatible License: MIT Build Status codebeat badge Release version

网络将 URLSessionasync-await(或 Combine)、DecodableGenerics 结合在一起,以简化与 JSON API 的连接。

演示时间 🍿

网络将这个

let config = URLSessionConfiguration.default
let urlSession = URLSession(configuration: config, delegate: nil, delegateQueue: nil)
var urlRequest = URLRequest(url: URL(string: "https://jsonplaceholder.typicode.com/users")!)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
urlRequest.httpBody = "firstname=Alan&lastname=Turing".data(using: .utf8)
let (data, _) = try await urlSession.data(for: urlRequest)
let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)

变成

let network = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com")
let user: User = try await network.post("/users", params: ["firstname" : "Alan", "lastname" : "Turing"])

视频教程

Rebeloper 的 Alex 制作了一个很棒的视频教程,点击这里观看!

如何实现

通过提供一个轻量级的客户端,**自动化每个人都必须编写的样板代码**。
通过暴露一个**非常简单**的 API,从而简单、清晰、快速地完成工作。
从 JSON API 获取 Swift 模型现在已成为过去的问题

URLSession + Combine + Generics + 协议 = 网络。

优点

开始使用

安装它

Networking 通过官方 Swift Package Manager 安装。

选择 Xcode > File > Swift Packages > Add Package Dependency...
并添加 https://github.com/freshOS/Networking

创建一个客户端

let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com")

进行你的第一次调用

在客户端上使用 getpostputpatchdelete 方法进行调用。

let data: Data = try await client.get("/posts/1")

所有 API 也都可以在 Combine 中使用

client.get("/posts/1").sink(receiveCompletion: { _ in }) { (data:Data) in
    // data
}.store(in: &cancellables)

获取你想要的返回类型

Networking 通过类型推断识别你想要的返回类型。支持的类型有 VoidDataAny(JSON)、Decodable(你的模型) 和 NetworkingJSONDecodable

这使得在支持多种类型的同时,保持一个简单的 API 成为可能

let voidPublisher: AnyPublisher<Void, Error> = client.get("")
let dataPublisher: AnyPublisher<Data, Error> = client.get("")
let jsonPublisher: AnyPublisher<Any, Error> = client.get("")
let postPublisher: AnyPublisher<Post, Error> = client.get("")
let postsPublisher: AnyPublisher<[Post], Error> = client.get("")

传递参数

只需将一个 [String: CustomStringConvertible] 字典传递给 params 参数。

let response: Data = try await client.posts("/posts/1", params: ["optin" : true ])

默认情况下,参数是 .urlEncoded 的 (Content-Type: application/x-www-form-urlencoded),要将它们编码为 json (Content-Type: application/json),你需要将客户端的 parameterEncoding 设置为 .json,如下所示

client.parameterEncoding = .json

上传 multipart 数据

对于 multipart 调用(post/put),只需将一个 MultipartData 结构体传递给 multipartData 参数。

let params: [String: CustomStringConvertible] = [ "type_resource_id": 1, "title": photo.title]
let multipartData = MultipartData(name: "file",
                                  fileData: photo.data,
                                  fileName: "photo.jpg",
                                   mimeType: "image/jpeg")
client.post("/photos/upload",
            params: params,
            multipartData: multipartData).sink(receiveCompletion: { _ in }) { (data:Data?, progress: Progress) in
                if let data = data {
                    print("upload is complete : \(data)")
                } else {
                    print("progress: \(progress)")
                }
}.store(in: &cancellables)

添加 Headers

Headers 通过客户端的 headers 属性添加。

client.headers["Authorization"] = "[mytoken]"

添加超时

超时(单位为秒的 TimeInterval)通过客户端上的可选 timeout 属性添加。

let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com", timeout: 15)

或者,

client.timeout = 15 

取消请求

由于 Networking 使用 Combine 框架。你只需要取消由 sink 调用返回的 AnyCancellable

var cancellable = client.get("/posts/1").sink(receiveCompletion: { _ in }) { (json:Any) in
  print(json)
}

稍后 ...

cancellable.cancel()

记录网络调用

支持 3 个日志级别:offinfodebug

client.logLevel = .debug

处理错误

错误可以在 Publisher 上处理,例如

client.get("/posts/1").sink(receiveCompletion: { completion in
switch completion {
case .finished:
    break
case .failure(let error):
    switch error {
    case let decodingError DecodingError:
        // handle JSON decoding errors
    case let networkingError NetworkingError:
        // handle NetworkingError
        // print(networkingError.status)
        // print(networkingError.code)
    default:
        // handle other error types
        print("\(error.localizedDescription)")
    }
}   
}) { (response: Post) in
    // handle the response
}.store(in: &cancellables)

支持 JSON 到模型解析。

对于一个可以被 Networking 解析的模型,它只需要符合 Decodable 协议。

如果你正在使用自定义 JSON 解析器,那么你必须符合 NetworkingJSONDecodable
例如,如果你正在使用 Arrow 进行 JSON 解析。支持一个 Post 模型看起来像这样

extension Post: NetworkingJSONDecodable {
    static func decode(_ json: Any) throws -> Post {
        var t = Post()
        if let arrowJSON = JSON(json) {
            t.deserialize(arrowJSON)
        }
        return t
    }
}

与其为每个模型都这样做,不如使用一个巧妙的扩展 🤓 一次性完成所有操作。

extension ArrowParsable where Self: NetworkingJSONDecodable {

    public static func decode(_ json: Any) throws -> Self {
        var t: Self = Self()
        if let arrowJSON = JSON(json) {
            t.deserialize(arrowJSON)
        }
        return t
    }
}

extension User: NetworkingJSONDecodable { }
extension Photo: NetworkingJSONDecodable { }
extension Video: NetworkingJSONDecodable { }
// etc.

设计一个干净的 API

为了编写一个简洁的 API,Networking 提供了 NetworkingService 协议。这会将你的调用转发到底层客户端,以便你只需要编写 get("/route") 而不是 network.get("/route"),虽然这对于小型 API 来说有点多余,但当使用大型 API 时,它绝对可以保持代码简洁。

给定一个 Article 模型

struct Article: DeCodable {
    let id: String
    let title: String
    let content: String
}

这是一个典型的 CRUD API 的样子

struct CRUDApi: NetworkingService {

    var network = NetworkingClient(baseURL: "https://my-api.com")

    // Create
    func create(article a: Article) async throws -> Article {
        try await post("/articles", params: ["title" : a.title, "content" : a.content])
    }

    // Read
    func fetch(article a: Article) async throws -> Article {
        try await get("/articles/\(a.id)")
    }

    // Update
    func update(article a: Article) async throws -> Article {
        try await put("/articles/\(a.id)", params: ["title" : a.title, "content" : a.content])
    }

    // Delete
    func delete(article a: Article) async throws {
        return try await delete("/articles/\(a.id)")
    }

    // List
    func articles() async throws -> [Article] {
        try await get("/articles")
    }
}

Combine 的等效代码如下所示

struct CRUDApi: NetworkingService {

    var network = NetworkingClient(baseURL: "https://my-api.com")

    // Create
    func create(article a: Article) -> AnyPublisher<Article, Error> {
        post("/articles", params: ["title" : a.title, "content" : a.content])
    }

    // Read
    func fetch(article a: Article) -> AnyPublisher<Article, Error> {
        get("/articles/\(a.id)")
    }

    // Update
    func update(article a: Article) -> AnyPublisher<Article, Error> {
        put("/articles/\(a.id)", params: ["title" : a.title, "content" : a.content])
    }

    // Delete
    func delete(article a: Article) -> AnyPublisher<Void, Error> {
        delete("/articles/\(a.id)")
    }

    // List
    func articles() -> AnyPublisher<[Article], Error> {
        get("/articles")
    }
}