引言:为什么正确的 HTTP 客户端对您的 Swift 应用很重要
现代 iOS 和 macOS 应用很少能避免与远程服务器交互。无论是从 REST API 加载数据、发送分析数据,还是与云服务协作——Swift 中的 HTTP 客户端都是网络交互的基础。然而,许多开发者,尤其是初学者,会犯典型错误:忽略错误处理、不使用现代语言特性(async/await),或者将网络调用写成“意大利面条式代码”。在本文中,我们将按照最佳实践,讲解如何编写一个可靠且高效的 Swift HTTP 客户端。
1. URLSession 基础:网络请求的核心
在 Swift 标准库中,负责网络交互的是 Foundation 框架和 URLSession 类。这是一个强大的工具,支持会话配置、缓存、后台请求以及更多功能。
1.1. 创建基础 HTTP 客户端
我们从执行 GET 请求的简单结构开始。使用 enum 描述可能的错误,使用 protocol 实现灵活性:
import Foundation
enum HTTPClientError: Error {
case invalidURL
case noData
case decodingError(Error)
case networkError(Error)
case statusCode(Int)
}
protocol HTTPClientProtocol {
func get<T: Decodable>(from urlString: String, responseType: T.Type) async throws -> T
}
final class HTTPClient: HTTPClientProtocol {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .shared, decoder: JSONDecoder = JSONDecoder()) {
self.session = session
self.decoder = decoder
}
func get<T: Decodable>(from urlString: String, responseType: T.Type) async throws -> T {
guard let url = URL(string: urlString) else {
throw HTTPClientError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw HTTPClientError.networkError(NSError(domain: "Invalid response", code: -1))
}
guard (200...299).contains(httpResponse.statusCode) else {
throw HTTPClientError.statusCode(httpResponse.statusCode)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw HTTPClientError.decodingError(error)
}
}
}
1.2. 为什么使用 async/await?
在 Swift 5.5 出现之前,开发者使用闭包(completion handlers)或第三方库(Alamofire)。Async/await 使代码线性化、清晰易懂。错误通过 do-catch 处理,而不是通过嵌套闭包。
2. POST、PUT、DELETE:处理不同的 HTTP 方法
真正的 HTTP 客户端必须支持所有主要方法。我们在协议中添加用于发送数据和更新资源的方法。
2.1. 扩展协议以支持 POST 和 PUT
extension HTTPClientProtocol {
func post<T: Decodable, U: Encodable>(
to urlString: String,
body: U,
responseType: T.Type
) async throws -> T {
guard let url = URL(string: urlString) else {
throw HTTPClientError.invalidURL
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpBody = try JSONEncoder().encode(body)
return try a
```