SimpleNetworking

Twitter: @gonzalezreal

SimpleNetworking 是一个 Swift 包,可以帮助您创建可扩展的 API 客户端,简单而优雅。 它使用 Combine 来暴露 API 响应,使其易于组合和转换。

它还包含其他好用的功能,例如日志记录和响应桩。

让我们以 The Movie Database API 为例,探索所有功能。

配置 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",
        ]
    )
)

创建 API 请求

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 中,因此组合响应并转换它们以进行演示非常简单。

例如,假设我们必须展示一个热门电影列表,包括它们的标题、类型和封面。 为了构建该列表,我们需要发出三个不同的请求。

我们可以按如下方式建模列表中的一个项目。

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"
     },
 ...

为 API 请求打桩响应

当编写 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")

相关项目

帮助 & 反馈