网络将 URLSession
、async
-await
(或 Combine
)、Decodable
和 Generics
结合在一起,以简化与 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 + 协议 = 网络。
Decodable
和任何 JSON 解析器Networking
通过官方 Swift Package Manager 安装。
选择 Xcode
> File
> Swift Packages
> Add Package Dependency...
并添加 https://github.com/freshOS/Networking
。
let client = NetworkingClient(baseURL: "https://jsonplaceholder.typicode.com")
在客户端上使用 get
、post
、put
、patch
和 delete
方法进行调用。
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
通过类型推断识别你想要的返回类型。支持的类型有 Void
、Data
、Any
(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 调用(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
属性添加。
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 个日志级别:off
、info
、debug
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)
对于一个可以被 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,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")
}
}