SimpleNetworking 是一个 Swift 包,可以帮助您创建可扩展的 API 客户端,简单而优雅。 它使用 Combine 来暴露 API 响应,使其易于组合和转换。
它还包含其他好用的功能,例如日志记录和响应桩。
让我们以 The Movie Database API 为例,探索所有功能。
APIClient
负责向 API 发出请求并处理其响应。 要创建 API 客户端,您需要提供基本 URL,并可以选择提供任何其他参数或标头,以便附加到所有请求,例如 API 密钥或授权标头。
let tmdbClient = APIClient(
baseURL: URL(string: "https://api.themoviedb.org/3")!,
configuration: APIClientConfiguration(
additionalParameters: [
"api_key": "20495f041a8caac8752afc86",
"language": "es",
]
)
)
APIRequest
类型包含发出 API 请求所需的所有数据,以及解码来自请求端点的有效和错误响应的逻辑。
在创建 API 请求之前,我们需要对它的有效和错误响应进行建模,最好是符合 Decodable
的类型。
通常,API 会根据请求定义不同的有效响应模型,但对于所有请求,通常只有一个错误响应模型。 对于 The Movie Database API,错误响应采用 Status
值的形式。
struct Status: Decodable {
var code: Int
var message: String
enum CodingKeys: String, CodingKey {
case code = "status_code"
case message = "status_message"
}
}
现在,考虑 GET /genre/movie/list
API 请求。 此请求返回电影的官方类型列表。 我们可以为它的响应实现一个 GenreList
类型。
struct Genre: Decodable {
var id: Int
var name: String
}
struct GenreList: Decodable {
var genres: [Genre]
}
有了这些响应模型,我们就可以创建 API 请求了。
let movieGenresRequest = APIRequest<GenreList, Status>.get("/genre/movie/list")
但我们可以做得更好,并扩展 APIClient
以提供一种获取电影类型的方法。
extension APIClient {
func movieGenres() -> AnyPublisher<GenreList, APIClientError<Status>> {
response(for: .get("/genre/movie/list"))
}
}
response(for:)
方法接受一个 APIRequest
并返回一个发布者,它包装了发送请求和解码其响应的过程。 我们可以通过依赖它来实现所有 API 方法。
extension APIClient {
func createSession(with token: Token) -> AnyPublisher<Session, APIClientError<Status>> {
response(for: .post("/authentication/session/new", body: token))
}
func deleteSession(_ session: Session) -> AnyPublisher<Void, APIClientError<Status>> {
response(for: .delete("/authentication/session", body: session))
}
...
func popularMovies(page: Int) -> AnyPublisher<Page<Movie>, APIClientError<Status>> {
response(for: .get("/movie/popular", parameters: ["page": page]))
}
func topRatedMovies(page: Int) -> AnyPublisher<Page<Movie>, APIClientError<Status>> {
response(for: .get("/movie/top_rated", parameters: ["page": page]))
}
...
}
当使用 API 客户端时,您的应用程序必须准备好处理错误。 SimpleNetworking 提供了 APIClientError
,它将 URL 加载错误、JSON 解码错误和特定的 API 错误响应统一为一个通用类型。
let cancellable = tmdbClient.movieGenres()
.catch { error in
switch error {
case .loadingError(let loadingError):
// Handle URL loading errors
...
case .decodingError(let decodingError):
// Handle JSON decoding errors
...
case .apiError(let apiError):
// Handle specific API errors
...
}
}
.sink { movieGenres in
// handle response
}
通用的 APIError
类型提供了对 HTTP 状态代码和 API 错误响应的访问。
由于我们的 API 客户端将响应包装在 Publisher
中,因此组合响应并转换它们以进行演示非常简单。
例如,假设我们必须展示一个热门电影列表,包括它们的标题、类型和封面。 为了构建该列表,我们需要发出三个不同的请求。
GET /configuration
,获取图像的基本 URL。GET /genre/movie/list
,获取电影的官方类型列表。GET /movie/popular
,获取当前热门电影列表。我们可以按如下方式建模列表中的一个项目。
struct MovieItem {
var title: String
var posterURL: URL?
var genres: String
init(movie: Movie, imageBaseURL: URL, movieGenres: GenreList) {
self.title = movie.title
self.posterURL = imageBaseURL
.appendingPathComponent("w300")
.appendingPathComponent(movie.posterPath)
self.genres = ...
}
}
要构建列表,我们可以将 zip
运算符与 API 客户端返回的发布者一起使用。
func popularItems(page: Int) -> AnyPublisher<[MovieItem], APIClientError<Status>> {
return Publishers.Zip3(
tmdbClient.configuration(),
tmdbClient.movieGenres(),
tmdbClient.popularMovies(page: page)
)
.map { (config, genres, page) -> [MovieItem] in
let url = config.images.secureBaseURL
return page.results.map {
MovieItem(movie: $0, imageBaseURL: url, movieGenres: genres)
}
}
.eraseToAnyPublisher()
}
每个 APIClient
实例都使用 SwiftLog 记录器记录请求和响应。
要查看请求和响应日志的实时输出,您需要在构造 APIClient 时指定 .debug
日志级别。
let tmdbClient = APIClient(
baseURL: URL(string: "https://api.themoviedb.org/3")!,
configuration: APIClientConfiguration(
...
),
logLevel: .debug
)
SimpleNetworking 格式化标头和 JSON 响应,生成结构化且可读的日志。 这是一个 GET /genre/movie/list
请求产生的输出示例
2019-12-15T17:18:47+0100 debug: [REQUEST] GET https://api.themoviedb.org/3/genre/movie/list?language=en
├─ Headers
│ Accept: application/json
2019-12-15T17:18:47+0100 debug: [RESPONSE] 200 https://api.themoviedb.org/3/genre/movie/list?language=en
├─ Headers
│ access-control-expose-headers: ETag, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After, Content-Length, Content-Range
│ Content-Type: application/json;charset=utf-8
│ x-ratelimit-reset: 1576426582
│ Server: openresty
│ Etag: "df2617d2ab5d0c85ceff5098b8ab70c4"
│ Cache-Control: public, max-age=28800
│ access-control-allow-methods: GET, HEAD, POST, PUT, DELETE, OPTIONS
│ Access-Control-Allow-Origin: *
│ Date: Sun, 15 Dec 2019 16:16:14 GMT
│ x-ratelimit-remaining: 39
│ Content-Length: 547
│ x-ratelimit-limit: 40
├─ Content
{
"genres" : [
{
"id" : 28,
"name" : "Action"
},
{
"id" : 12,
"name" : "Adventure"
},
...
当编写 UI 或集成测试以避免依赖网络可达性时,打桩响应非常有用。
为此,SimpleNetworking 提供了 HTTPStubProtocol
,这是一个 URLProtocol
子类,允许为特定的 API 或 URL 请求打桩响应。
您可以将任何 Encodable
值打桩为 API 请求的有效响应
try HTTPStubProtocol.stub(
User(name: "gonzalezreal"),
statusCode: 200,
for: APIRequest<User, Error>.get(
"/user",
headers: [.authorization: "Bearer 3xpo"],
parameters: ["api_key": "a9a5aac8752afc86"]
),
baseURL: URL(string: "https://example.com/api")!
)
或者作为同一 API 请求的错误响应
try HTTPStubProtocol.stub(
Error(message: "The resource you requested could not be found."),
statusCode: 404,
for: APIRequest<User, Error>.get(
"/user",
headers: [.authorization: "Bearer 3xpo"],
parameters: ["api_key": "a9a5aac8752afc86"]
),
baseURL: URL(string: "https://example.com/api")!
)
要使用桩响应,您需要在创建 APIClient
实例时将 URLSession.stubbed
作为参数传递。
let apiClient = APIClient(
baseURL: URL(string: "https://example.com/api")!,
configuration: configuration,
session: .stubbed
)
使用 Swift Package Manager
将 SimpleNetworking 作为依赖项添加到您的 Package.swift
文件中。 有关更多信息,请参阅 Swift Package Manager 文档。
.package(url: "https://github.com/gonzalezreal/SimpleNetworking", from: "2.0.0")
URLCache
,提供持久性和内存缓存。SimpleNetworking
进行一些更改,请打开一个 PR。