认识 Snowdrop - 一个类型安全、易于使用的框架,由 Swift 宏驱动,旨在让您轻松构建和维护复杂的网络请求。
Snowdrop 可通过 SPM 获取。它适用于 iOS 部署目标 14.0 或更高版本以及 macOS 部署目标 11 或更高版本。
@Service
宏进行类型安全的服务创建@GET
@POST
@PUT
@DELETE
@PATCH
@CONNECT
@HEAD
@OPTIONS
@QUERY
@TRACE
使用 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 解码器,您可以在创建服务实例时设置您自己的解码器。
let decoder = CustomJSONDecoder()
let service = MyEndpointService(baseUrl: URL(string: "https://my-endpoint.com")!, decoder: decoder)
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
如果您想声明将文件以 multipart/form-data
形式发送到服务器的服务函数,请使用 @FileUpload
宏。它将自动将 Content-Type: multipart/form-data
添加到请求头,并使用 _payloadDescription: PayloadDescription
扩展您的函数参数列表,您应该使用它来提供诸如 name
、fileName
和 mimeType
等信息。对于诸如 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"}"#)
。
每个服务都提供了两种方法来添加拦截块 - addBeforeSendingBlock
和 addOnResponseBlock
。两者都接受诸如 String
类型的 path
和作为闭包的 block
等参数。
要为与特定路径匹配的请求添加 addBeforeSendingBlock
或 addOnResponseBlock
,请使用正则表达式,例如
service.addBeforeSendingBlock(for: "my/path/[0-9]{1,}/content") { urlRequest in
// some operations
return urlRequest
}
要为所有请求添加 addBeforeSendingBlock
或 addOnResponseBlock
,请这样做
service.addOnResponseBlock { data, httpUrlResponse in
// some operations
return data
}
请注意,如果您为特定请求路径添加了拦截块,则通用拦截器将被忽略。
如果您想创建服务的可 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 块将不会被调用。
如果您想针对 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 的灵感来源。