alt [version] alt spm available

alt text

Snowdrop

认识 Snowdrop - 一个类型安全、易于使用的框架,由 Swift 宏驱动,旨在让您轻松构建和维护复杂的网络请求。

导航

安装

Snowdrop 可通过 SPM 获取。它适用于 iOS 部署目标 14.0 或更高版本以及 macOS 部署目标 11 或更高版本。

主要功能

基本用法

服务声明

使用 Snowdrop 创建网络服务非常容易。只需声明一个协议及其函数即可。

@Service
protocol MyEndpoint {

    @GET(url: "/posts")
    @Headers(["X-DeviceID": "testSim001"])
    func getAllPosts() async throws -> [Post]
}

如果您的请求包含一些动态值,例如 id,您可以将其添加到路径中,并用 {} 包裹。Snowdrop 将自动将您的函数声明的参数与您在请求路径中包含的参数绑定。

@GET(url: "/posts/{id}")
func getPost(id: Int) async throws -> Post

请求执行

在扩展宏时,Snowdrop 会创建一个类 MyEndpointService,该类实现了 MyEndpoint 协议并生成了您声明的所有函数。

class MyEndpointService: MyEndpoint {
    func getAllPosts() async throws -> [Post] {
        // auto-generated body
    }
    
    func getPost(id: Int) async throws -> Post {
        // auto-generated body
    }
}

请注意,如果您的服务协议已经包含 “Service” 关键字,例如 MyEndpointService,宏将生成名为 MyEndpointServiceImpl 的类。

要发送请求,只需初始化 MyEndpointService 实例并调用与您要执行的请求相对应的函数。

let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!)
let post = try await service.getPost(id: 7)

高级用法

默认 JSON 解码器

如果您需要更改默认 JSON 解码器,您可以在创建服务实例时设置您自己的解码器。

let decoder = CustomJSONDecoder()
let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!, decoder: decoder)

SSL/证书锁定

Snowdrop 在执行网络请求时提供 SSL/证书锁定功能。您可以在创建服务实例时打开/关闭它。您还可以确定应从锁定中排除的 URL。

let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!, pinningMode: .ssl, urlsExcludedFromPinning: ["https://my-endpoint.com/about"])

请求体参数

如果您想将一些可编码对象作为请求体,您可以将其放在声明中作为 “body” 参数,或者 - 如果您想使用另一个名称 - 使用 @Body 宏,例如

@POST(url: "/posts")
@Body("model")
func addPost(model: Post) async throws -> Data

使用 form-data 上传文件

如果您想声明将文件以 multipart/form-data 形式发送到服务器的服务函数,请使用 @FileUpload 宏。它将自动将 Content-Type: multipart/form-data 添加到请求头,并使用 _payloadDescription: PayloadDescription 扩展您的函数参数列表,您应该使用它来提供诸如 namefileNamemimeType 等信息。对于诸如 jpeg、png、gif、tiff、pdf、vnd、plain、octetStream 等 MIME 类型,您不必提供 PayloadDescription。Snowdrop 可以自动识别它们并为您创建 PayloadDescription

@Service
protocol MyEndpoint {

    @FileUpload
    @Body("image")
    @POST(url: "/uploadAvatar/")
    func uploadImage(_ image: UIImage) async throws -> Data
}

let payload = PayloadDescription(name: "avatar", fileName: "filename.jpeg", mimeType: "image/jpeg")
let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!)
_ = try await service.uploadImage(someImage, _payloadDescription: payload)

查询参数

使用 Snowdrop,您可以通过两种方式传递查询参数。

第一种方式是使用 @QueryParams 宏。要告知函数的哪些参数应该是查询参数,请将它们放在数组中,如下所示

@Service
protocol MyEndpoint {
    @GET(url: "/posts/{id}")
    @QueryParams(["author"])
    func getPost(id: Int, author: String) async throws -> Post
}

let authorName = "John Smith"
let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!)
let post = try await service.getPost(id: 7, author: authorName)

或者,在扩展宏时,Snowdrop 会为每个服务函数添加参数 _queryItems: [QueryItem]。像这样使用它

@Service
protocol MyEndpoint {
    @GET(url: "/posts/{id}")
    func getPost(id: Int) async throws -> Post
}

let authorName = "John Smith"
let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!)
let post = try await service.getPost(id: 7, _queryItems: [.init(key: "author", value: authorName)])

参数的默认值

Snowdrop 允许您为参数定义自定义值。假设您的路径包含 {id} 参数。正如您现在已经知道的,Snowdrop 会自动将其与您的 func 声明的 id 参数关联。如果您希望它的默认值等于 “3”,请这样做:{id=3}。但请注意,Snowdrop 不会检查您的默认值的类型是否符合声明。
当插入 String 类型的默认值(例如 {name=“Some name”})时,强烈建议使用 Raw String,例如 @GET(url: #"/authors/{name="John Smith"}"#)

拦截器

每个服务都提供了两种方法来添加拦截块 - addBeforeSendingBlockaddOnResponseBlock。两者都接受诸如 String 类型的 path 和作为闭包的 block 等参数。

要为与特定路径匹配的请求添加 addBeforeSendingBlockaddOnResponseBlock,请使用正则表达式,例如

service.addBeforeSendingBlock(for: "my/path/[0-9]{1,}/content") { urlRequest in
    // some operations
    return urlRequest
}

要为所有请求添加 addBeforeSendingBlockaddOnResponseBlock,请这样做

service.addOnResponseBlock { data, httpUrlResponse in
    // some operations
    return data
}

请注意,如果您为特定请求路径添加了拦截块,则通用拦截器将被忽略。

可 Mock

如果您想创建服务的可 Mock 版本,Snowdrop 可以满足您的需求。只需将 @Mockable 宏添加到您的服务声明中,例如

@Service
@Mockable
protocol Endpoint {
    @Get("/path")
    func getPosts() async throws -> [Posts]
}

Snowdrop 将自动创建一个 EndpointServiceMock 类,其中包含 Service 应具有的所有属性以及诸如 getPostsResult 之类的附加属性,您可以为这些属性分配应返回的值。

用法示例

func testEmptyArrayResult() async throws {
    let mock = EndpointServiceMock(baseUrl: URL(string: "https://some.url")!
    mock.getPostsResult = .success([])

    let result = try await mock.getPosts()
    XCTAssertTrue(result.isEmpty)
}

请注意,Mock 方法将直接返回桩结果,而无需访问 Snowdrop.Core,因此您的 beforeSend 和 onResponse 块将不会被调用。

JSON 注入

如果您想针对 Mock JSON 测试您的服务,您可以轻松做到这一点。只需确保您的 JSON Mock 位于项目文件中的某个位置,然后实例化您的服务并确定应为哪个请求注入 Mock,如下例所示。

func testJSONMockInjectsion() async throws {
    let service = MyEndpointService(baseUrl: someBaseURL)
    service.testJSONDictionary = ["users/123/info": "MyJSONMock"]
    
    let result = try await service.getUserInfo(id: 123)
    XCTAssertTrue(result.firstName, "JSON")
    XCTAssertTrue(result.lastName, "Bourne")
}

详细日志

如果您想查看 Snowdrop 的日志,请在创建服务新实例时使用 verbose 标志。

let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!, verbose: true)

致谢

Retrofit 是 Snowdrop 的灵感来源。