Swift Package Manager compatiblePRs WelcomeLicense: MIT Platform

现代应用程序与许多不同的 API 交互。Netable 通过提供一个简单的接口来使用这些 API 驱动高质量的 iOS 和 MacOS 应用程序,这些应用程序构建于 Swift Codable 之上,同时仍然在需要时支持非标准和不寻常的 API,从而简化了这一过程。

功能特性

Netable 构建于我们认为网络库应遵循的许多核心原则之上

使用方法

标准用法

创建一个新的 Netable 实例,并传入您的基本 URL

let netable = Netable(baseURL: URL(string: "https://api.thecatapi.com/v1/")!)

有关添加其他实例参数的信息,请参阅此处

扩展 Request

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"]
    }
}

使用 async/await 发出您的请求并处理结果

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)
    }
}

使用 Combine 发出请求

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 实例参数

在您的 Netable 实例中,您可以提供 baseURL 之外的可选参数,以便与发出的每个请求一起发送其他信息。 这些包括

  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 }
}

请注意,这仅在您的 RawResourceRawResource: SequenceRawResource: SmartUnwrap<Sequence> 时才有效。 为了更好地支持解码嵌套的、有损的数组,我们建议查看 Better Codable。 此外,目前,Netable 不支持 GraphQL 请求的部分解码。

直接在您的对象中创建 LossyArray

使用 .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) {
    // ..
    }

在使用 postProcess 返回结果之前执行可选过程

这对于管理缓存或数据管理器等位置的数据非常有用。 您可以在我们的 UserRequest 中更深入地了解这一点

要在请求内部使用 postProcess,请添加您想在 return 语句之前运行的代码

struct GetUserRequest: Request {
   // ...
   
    func postProcess(result: FinalResource) -> FinalResource {
        DataManager.shared.user = result
        return result
    }
}

错误处理

除了处理本地抛出或通过 Result 对象返回的错误之外,我们还提供了两种全局处理错误的方法。 这些对于在 UI 中呈现跨多个请求的常见错误情况,或捕获诸如身份验证请求失败以清除存储的用户之类的事情非常有用。

使用 requestFailureDelegate

有关更详细的示例,请参阅示例项目中的 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

使用 requestFailurePublisher

如果您更喜欢 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)

使用 FallbackResource

有时,您可能想要指定一个备份类型,以便在初始解码失败时尝试将响应解码为该类型,例如

在创建请求时,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

GraphQL 支持

虽然从技术上讲,您可以使用 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 提供。 要安装它,请按照以下步骤操作

  1. 在 Xcode 中,单击文件,然后单击Swift Package Manager,然后单击添加 Package Dependency
  2. 选择您的项目
  3. 在搜索栏中输入此 URL https://github.com/steamclock/netable.git

支持更早版本的 iOS

由于 Netable 2.0 在底层利用了 async/await,如果您想为 15.0 之前的 iOS 版本构建,则需要使用 v1.0

许可证

Netable 在 MIT 许可证下可用。 有关更多信息,请参阅 License.md