现代应用程序与许多不同的 API 交互。Netable 通过提供一个简单的接口来使用这些 API 驱动高质量的 iOS 和 MacOS 应用程序,这些应用程序构建于 Swift Codable
之上,同时仍然在需要时支持非标准和不寻常的 API,从而简化了这一过程。
Netable 构建于我们认为网络库应遵循的许多核心原则之上
let netable = Netable(baseURL: URL(string: "https://api.thecatapi.com/v1/")!)
有关添加其他实例参数的信息,请参阅此处。
struct CatImage: Decodable {
let id: String
let url: String
}
struct GetCatImages: Request {
typealias Parameters = [String: String]
typealias RawResource = [CatImage]
public var method: HTTPMethod { return .get }
public var path: String {
return "images/search"
}
public var parameters: [String: String] {
return ["mime_type": "jpg,png", "limit": "2"]
}
}
Task {
do {
let catImages = try await netable.request(GetCatImages())
if let firstCat = catImages.first,
let url = URL(string: firstCat.url),
let imageData = try? Data(contentsOf: url) {
self.catsImageView1.image = UIImage(data: imageData)
}
if let lastCat = catImages.last,
let url = URL(string: lastCat.url),
let imageData = try? Data(contentsOf: url) {
self.catsImageView2.image = UIImage(data: imageData)
}
} catch {
let alert = UIAlertController(
title: "Uh oh!",
message: "Get cats request failed with error: \(error)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
self.present(alert, animated: true, completion: nil)
}
}
netable.request(GetCatImages())
.sink { result in
switch result {
case .success(let catImages):
if let firstCat = catImages.first,
let url = URL(string: firstCat.url),
let imageData = try? Data(contentsOf: url) {
self.catsImageView1.image = UIImage(data: imageData)
}
if let lastCat = catImages.last,
let url = URL(string: lastCat.url),
let imageData = try? Data(contentsOf: url) {
self.catsImageView2.image = UIImage(data: imageData)
}
case .failure(let error):
let alert = UIAlertController(
title: "Uh oh!",
message: "Get cats request failed with error: \(error)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
self.present(alert, animated: true, completion: nil)
}
}.store(in: &cancellables)
netable.request(GetCatImages()) { result in
switch result {
case .success(let catImages):
if let firstCat = catImages.first,
let url = URL(string: firstCat.url),
let imageData = try? Data(contentsOf: url) {
self.catsImageView1.image = UIImage(data: imageData)
}
if let lastCat = catImages.last,
let url = URL(string: lastCat.url),
let imageData = try? Data(contentsOf: url) {
self.catsImageView2.image = UIImage(data: imageData)
}
case .failure(let error):
let alert = UIAlertController(
title: "Uh oh!",
message: "Get cats request failed with error: \(error)",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .cancel))
self.present(alert, animated: true, completion: nil)
}
}
您可以使用 .cancel()
轻松取消请求,您可以在示例项目中的 AuthNetworkService 中看到它的实际应用。
要取消任务,我们首先需要确保保留对任务的引用,如下所示
let createRequest = Task {
let result = try await netable.request()
}
createRequest.cancel()
在您的 Netable 实例中,您可以提供 baseURL
之外的可选参数,以便与发出的每个请求一起发送其他信息。 这些包括
globalHeaders
、您首选的 encoding/decoding
策略、logRedecation
和/或 timeouts
之类的选项。logDestination
retryConfiguration
按需重试请求。requestFialureDelegate/Subject
。 let netable = Netable(baseURL: URL(string: "https://...")!,
config: Config(globalHeaders: ["Authentication" : "Bearer \(login.token)"]),
logDestination: EmptyLogDestination(),
retryConfiguration: RetryConfiguration(errors: .all, count: 3, delay: 5.0),
requestFailureDelegate: ErrorService.shared)
有关更详细的示例,请参阅示例项目中的 AuthNetworkService。
您还可以灵活地设置可选参数,以便与发送到实例的每个单独请求一起发送。 请注意,对于实例和单个请求之间的重复参数,实例的参数将被单个请求覆盖。 您可以在此处查看这些参数的列表。
在示例项目中,您可以在 LoginRequest 中看到添加 unredactedParameterKeys
的示例,并在 GetUserRequest 中看到 jsonKeyDecodingStrategy
的示例。
struct CatImage: Decodable {
let id: String
let url: String
}
struct GetCatImageURL: Request {
typealias Parameters = [String: String]
typealias RawResource = [CatImage]
typealias FinalResource = URL
// ...
func finalize(raw: RawResource) async throws -> FinalResource {
guard let catImage = raw.first else {
throw NetableError.resourceExtractionError("The CatImage array is empty")
}
guard let url = URL(string: catImage.url) else {
throw NetableError.resourceExtractionError("Could not build URL from CatImage url string")
}
return url
}
}
Task {
do {
let catUrl = try await netable.request(GetCatImages())
guard let imageData = try? Data(contentsOf: catUrl) else {
throw NetableError.noData
}
self.imageView.image = UIImage(data: imageData)
} catch {
// ...
}
}
有时,API 喜欢将您实际关心的对象返回在单层包装器内,Finalize
非常擅长处理这种情况,但这比我们希望的需要更多的样板代码。 这就是 SmartUnwrap<>
的用武之地!
像往常一样创建您的请求,但将您的 RawResource = SmartUnwrap<ObjectYouCareAbout>
和 FinalResource = ObjectYourCareAbout
设置为。 您还可以指定 Request.smartUnwrapKey
以避免从响应中解包对象时出现歧义。
之前
struct UserResponse {
let user: User
}
struct User {
let name: String
let email: String
}
struct GetUserRequest: Request {
typealias Parameters: GetUserParams
typealias RawResource: UserResponse
typealias FinalResource: User
// ...
func finalize(raw: RawResource) async throws -> FinalResource {
return raw.user
}
}
之后
struct User: {
let name: String
let email: String
}
struct GetUserRequest: Request {
typealias Parameters: GetUserParams
typealias RawResource: SmartUnwrap<User>
typealias FinalResource: User
}
有时,在解码对象数组时,如果其中一些对象解码失败,您可能不想使整个请求失败。 例如,以下 json
将无法使用标准解码进行解码,因为第二个帖子缺少内容。
{
posts: [
{
"title": "Super cool cat."
"content": "Info about a super cool cat."
},
{
"title": "Even cooler cat."
}
]
}
为此,您可以将请求的 arrayDecodeStrategy
设置为 .lossy
以返回任何成功解码的元素。
struct Post: {
let title: String
let content: String
}
struct GetPostsRequests: {
typealias RawResource: SmartUnwrap<[Post]>
typealias FinalResource: [Post]
var arrayDecodingStrategy: ArrayDecodingStrategy: { return .lossy }
}
请注意,这仅在您的 RawResource
是 RawResource: Sequence
或 RawResource: SmartUnwrap<Sequence>
时才有效。 为了更好地支持解码嵌套的、有损的数组,我们建议查看 Better Codable。 此外,目前,Netable 不支持 GraphQL 请求的部分解码。
使用 .lossy
作为我们的 arrayDecodingStrategy
非常适合作为数组解码的对象。 我们添加了支持,以允许对包含数组的对象进行部分解码。
struct User: Decodable {
let firstName: String
let lastName: String
let bio: String
let additionalInfo: LossyArray<AdditionalInfo>
}
struct UserLoginData: Decodable, Hashable {
let age: Int
let gender: String
let nickname: String
}
注意:要访问 LossyArray 的元素,您必须访问其中的 .element
,如下所示。
ForEach(user.additionalInfo.element, id: \.self) {
// ..
}
这对于管理缓存或数据管理器等位置的数据非常有用。 您可以在我们的 UserRequest 中更深入地了解这一点
要在请求内部使用 postProcess
,请添加您想在 return 语句之前运行的代码
struct GetUserRequest: Request {
// ...
func postProcess(result: FinalResource) -> FinalResource {
DataManager.shared.user = result
return result
}
}
除了处理本地抛出或通过 Result
对象返回的错误之外,我们还提供了两种全局处理错误的方法。 这些对于在 UI 中呈现跨多个请求的常见错误情况,或捕获诸如身份验证请求失败以清除存储的用户之类的事情非常有用。
有关更详细的示例,请参阅示例项目中的 GlobalRequestFailureDelegate。
extension GlobalRequestFailureDelegateExample: RequestFailureDelegate {
func requestDidFail<T>(_ request: T, error: NetableError) where T : Request {
let alert = UIAlertController(title: "Uh oh!", message: error.errorDescription, preferredStyle: .alert)
present(alert, animated: true)
}
}
拦截器是修改 Request
在执行前的一种强大而灵活的方式。 当您创建 Netable
实例时,您可以传入可选的 InterceptorList
,其中包含您想要应用于请求的任何 Interceptor
。
当您发出请求时,每个 Interceptor
将按顺序调用其 adapt
函数,顺序与传入 InterceptorList
的顺序相同。 adapt
应返回一个特殊的 AdaptedRequest
对象,该对象指示函数调用的结果。
您可能会附加一个新的标头,修改请求
func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest {
var newRequest = request
newRequest.addValue("1a2a3a4a", forHTTPHeaderField: "Authorization")
return .changed(newRequest)
}
或者,您可能会为特定端点替换整个请求为模拟文件,否则不执行任何操作
func adapt(_ request: URLRequest, instance: Netable) async throws -> AdaptedRequest {
if request.url.contains("/foo") {
return .mocked("./path/to/foo-mock.json")
} else if request.url.contains("/bar") {
return .mocked("./path/to/bar-mock.json")
}
return .notChanged
}
有关更详细的示例,请参阅示例项目中的 MockRequestInterceptor。
如果您更喜欢 Combine
,您可以订阅此发布者以从应用程序的其他位置接收 NetableErrors
。
有关更详细的示例,请参阅示例项目中的 GlobalRequestFailurePublisher。
netable.requestFailurePublisher.sink { error in
let alert = UIAlertController(title: "Uh oh!", message: error.errorDescription, preferredStyle: .alert)
self.present(alert, animated: true)
}.store(in: &cancellables)
有时,您可能想要指定一个备份类型,以便在初始解码失败时尝试将响应解码为该类型,例如
在创建请求时,Request
允许您选择性地声明 FallbackResource: Decodable
关联类型。 如果您这样做,并且您的请求无法解码 RawResource
,它将尝试解码您的回退资源,如果成功,则抛出带有成功解码的 NetableError.fallbackDecode
。
struct CoolCat {
let name: String
let breed: String
}
struct Cat {
let name: String
}
struct GetCatRequest: Request {
typealias RawResource: CoolCat
typealias FallbackResource: Cat
// ...
}
有关更详细的示例,请参阅示例项目中的 FallbackDecoderViewController。
虽然从技术上讲,您可以使用 Netable
来管理开箱即用的 GraphQL 查询,但我们添加了一个辅助协议来让您的生活更轻松一点,称为 GraphQLRequest
。
struct GetAllPostsQuery: GraphQLRequest {
typealias Parameters = Empty
typealias RawResource = SmartUnwrap<[Post]>
typealias FinalResource = [Post]
var source = GraphQLQuerySource.resource("GetAllPostsQuery")
}
有关更详细的示例,请参阅示例项目中的 UpdatePostsMutation。 请注意,默认情况下,您的 .graphql
文件的名称与您的请求完全匹配非常重要。
我们建议使用诸如 Postman 之类的工具来记录和测试您的查询。 另请注意,目前不支持共享片段。
深入文档通过 Jazzy 和 GitHub Pages 提供。
Netable 通过 Swift Package Manager 提供。 要安装它,请按照以下步骤操作
https://github.com/steamclock/netable.git
由于 Netable 2.0 在底层利用了 async
/await
,如果您想为 15.0 之前的 iOS 版本构建,则需要使用 v1.0
。
Netable 在 MIT 许可证下可用。 有关更多信息,请参阅 License.md。