Fetch

Swift Package Manager

Fetch 是一个基于资源的网络抽象层,基于 Alamofire

特性

基本用法

首先使用 Config 设置 APIClient。只需要一行代码。在 application(_:didFinishLaunchingWithOptions:) 函数中调用它。

APIClient.shared.setup(with: Config(baseURL: URL(string: "https://api.github.com")!))

让我们创建一个名为 Organization 的结构体,包含一些属性。模型将从网络响应中解析。

struct Organization: Decodable, Equatable {
    let id: Int
    let name: String
    let location: String
}

一个 Resource 包含创建网络请求和解析响应所需的所有必要信息。

let resource = Resource<Organization>(
    method: .get,
    path: "/orgs/allaboutapps")

发送请求并将响应解析为类型化的模型 Organisation

resource.request { (result) in
    switch result {
    case .success(let networkResponse):
        print("Status code:", networkResponse.urlResponse.statusCode)
        print("Model:", networkResponse.model)
    case .failure(let apiError):
        print("Error:", apiError)
    }
}

高级用法

内容解析

默认情况下,配置使用标准库提供的 JSONDecoder 和 JSONEncoder,但不限于此。这两种类型都已扩展为符合 ResourceDecoderProtocolResourceEncoderProtocol,允许您定义自己的自定义解码器/编码器。 Resource 提供了解码和编码闭包,这些闭包使用配置中定义的解码器和编码器。 如果要为资源实现不同的行为,可以在创建资源时提供一个闭包。

有效负载解包

有时内容会被打包在一个信封中,这使得解析变得困难。 在这种情况下,您可以定义所谓的“根键”。 根键定义了信封中要解析的内容的路径。 这意味着只会解析用根键定义的内容。

示例

这是一个应该被解析的响应。

{
  "data": {
    "people": [
        {
          "name": "Alex"
        },
        {
          "name": "Jeff"
        },
        {
          "name": "Tom"
        },
        {
          "name": "Xavier"
        }
    ]
  }
}

我们只想要 people,它是一个 Person 的数组。 而不是定义一个对层次结构建模的结构,我们在资源上定义 "root keys" 以仅获取数组。

struct Person: Decodable {
    let name: String
}

let resource = Resource<[Person]>(
    path: "/people",
    rootKeys: ["data", "people"]
)
resource.request { result in
...
}

模拟 (Stubbing)

Fetch 为您提供了一套通用的模拟方法。 要执行模拟,必须启用 APIClients Config 上的 shouldStub 并且必须为资源注册一个模拟。

使用 json 响应模拟成功的网络请求

let stub = StubResponse(statusCode: 200, fileName: "success.json", delay: 2.0)
        
let resource = Resource<Person>(path: "/test")
    
APIClient.shared.stubProvider.register(stub: stub, for: resource)
    

上面的模拟将返回一个 200 状态代码,内容来自从您应用程序的 bundle 加载的 success json 文件,并将延迟两秒。

模拟未经授权的错误

let stub = StubResponse(statusCode: 401, fileName: "unauthorized.json", delay: 2.0)
        
let resource = Resource<Person>(path: "/unauthorized")
    
APIClient.shared.stubProvider.register(stub: stub, for: resource)

模拟不限于 json,您还可以提供原始数据或提供符合 Encodable 协议的实例。

使用 Encodable 进行模拟

struct Person: Encodable {
    let name: String
    let age: Int
}
    
let peter = Person(name: "Peter", age: 18)

let stub = StubResponse(statusCode: 200, encodable: peter, delay: 2.0)
        
let resource = Resource<Person>(path: "/peter")
    
APIClient.shared.stubProvider.register(stub: stub, for: resource)

交替模拟

let successStub = StubResponse(statusCode: 200, fileName: "success.json", delay: 0.0)
let failureStub = StubResponse(statusCode: 404, fileName: "notFound.json", delay: 0.0)

let alternatingStub = AlternatingStub(stubs: [successStub, failureStub])
       
let resource = Resource<Person>(path: "/peter")
   
APIClient.shared.stubProvider.register(stub: alternatingStub, for: resource)

每次执行资源时,它将遍历给定的模拟,并且总是返回一个与之前不同的模拟。

随机模拟

RandomStub 的工作方式类似于 AlternatingStub,但总是从数组中返回一个随机的模拟。

条件模拟

基于特定条件模拟行为可以使用条件模拟来实现。

示例

模拟一个受用户授权保护的端点,并根据您应用程序的授权状态返回成功或错误

let conditionalStub = ClosureStub { () -> Stub in
let unauthorizedStub = StubResponse(statusCode: 401, data: Data(), delay: 2)
let okStub = StubResponse(statusCode: 200, data: Data(), delay: 2)
  return CredentialsController.shared.currentCredentials == nil ? unauthorizedStub : okStub
}

let resource = Resource(path: "/auth/secret")
    
APIClient.shared.stubProvider.register(stub: conditionalStub, for: resource)

自定义模拟

您可以通过符合 Stub 协议来创建自定义模拟。

struct CustomStub: Stub {
...
}

自定义 StubProvider

您可以通过符合 StubProvider 协议来创建自定义 StubProvider

struct CustomStubProvider: StubProvider {
...
}

使用自定义 stubProvider 初始化/设置 APIClient

let client = APIClient(config: Config(stubProvider: customStubProvider))
APIClient.shared.setup(with: Config(stubProvider: customStubProvider))

或者,替换 APIClient 上的默认 StubProvider

APIClient.shared.setStubProvider(customStubProvider)

缓存

实现了以下缓存类型

设置缓存

let cache = MemoryCache(defaultExpiration: .seconds(3600))
        
let config = Config(
    baseURL: URL(string: "https://example.com")!,
    cache: cache,
    cachePolicy: .networkOnlyUpdateCache)

let client = APIClient(config: config)

注意:要使用缓存,您从资源加载的模型必须符合 Cacheable 协议。

混合缓存

混合缓存允许您组合两个单独的缓存,使用的缓存类型不受限制。

自定义缓存实现

要实现自定义缓存,您必须创建一个符合 Cache 协议的类/结构体。

class SpecialCache: Cache {
    ...
}

缓存策略

缓存策略定义了资源的加载行为。 您可以在创建资源时直接在资源上设置策略,可以在 APIClient 的配置中设置策略,也可以将其作为参数传递给资源的 fetch 函数。

注意:资源中定义的策略始终优先于配置中定义的策略。

从缓存加载,否则从网络加载

这将首先尝试从缓存中读取请求的数据,如果数据不可用或已过期,则将从网络加载数据。

let resource: Resource<X> = ...
resource.fetch(cachePolicy: .cacheFirstNetworkIfNotFoundOrExpired) { (result, finishedLoading) in 
    ...
}

从网络加载并更新缓存

这将从网络加载数据并更新缓存。完成闭包只会使用来自网络的值调用。

let resource: Resource<Person> = ...
resource.fetch(cachePolicy: .networkOnlyUpdateCache) { (result, finishedLoading) in 
...
}

从缓存加载数据并始终从网络加载数据

这将从缓存加载数据并从网络加载数据。您将异步地在完成闭包中获得这两个值。

let resource: Resource<Person> = ...
resource.fetch(cachePolicy: .cacheFirstNetworkAlways) { (result, finishedLoading) in 
    ...
}

有关策略的概述,请查看 Cache.swift 中的实现

Swift Package Manager

使用 Xcode 11+:转到 Project > Swift Packages > + 并输入 git@github.com:allaboutapps/Fetch.git

或者手动更新您的 Package.swift 文件

dependencies: [
    .package(url: "git@github.com:allaboutapps/Fetch.git", from: "1.0.9"),
    ....
],
targets: [
    .target(name: "YourApp", dependencies: ["Fetch"]),
]

要求

贡献