Apexy
在项目中组织网络层的库。
- 将用于网络的对象分离到独立的模块、目标或库,使它们在
namespace
中隔离。 - 将请求分解成单独的结构。类不是禁止使用,但应使其不可变。如果不同的请求具有相同的响应,请使用
enum
。
安装
CocoaPods
要使用 CocoaPods 将 Apexy 集成到 Xcode 项目中,请在 Podfile 中指定它。
如果您想与 Alamofire 一起使用 Apexy
pod 'Apexy'
如果您想不使用 Alamofire 使用 Apexy
pod 'Apexy/URLSession'
如果您想使用 ApexyLoader
pod 'Apexy/Loader'
Swift Package Manager
如果您有 Xcode 项目,请打开它并选择 文件 → Swift Packages → 添加包依赖,然后粘贴 Apexy 仓库 URL
https://github.com/RedMadRobot/apexy-ios
有三个包产品:Apexy、ApexyAlamofire、ApexyLoader。
Apexy —— 在内部使用 URLSession
ApexyAlamofire —— 内部使用 Alamofire
ApexyLoader —— Apexy 的插件,用于存储获取到的数据到内存并监听加载状态。详见文档说明 ApexyLoader
如果你有自己的 Swift 包,将 Apexy 添加为依赖项到你的 Package.swift 的依赖项值中。
dependencies: [
.package(url: "https://github.com/RedMadRobot/apexy-ios.git")
]
端点
Endpoint
— 组织与 REST API 工作的基本协议之一。它是一组请求和响应处理。
不可变。
- 创建用于发送请求的
URLRequest
。 - 检查服务器响应以验证 API 错误。
- 将服务器响应转换为正确的类型(
Data
、String
、Decodable
)。
public struct Book: Codable, Identifiable {
public let id: String
public let name: String
}
public struct BookEndpoint: Endpoint {
public typealias Content = Book
public let id: Book.ID
public init(id: Book.ID) {
self.id = id
}
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(id)
return URLRequest(url: url)
}
public func validate(_ response: URLResponse?, with body: Data) throws {
// TODO: check API / HTTP error
}
public func content(from response: URLResponse?, with body: Data) throws -> Content {
return try JSONDecoder().decode(Content.self, from: body)
}
}
let client = Client ...
let endpoint = BookEndpoint(id: "1")
client.request(endpoint) { (result: Result<Book, Error>)
print(result)
}
客户端
Client
— 仅有一个执行 Endpoint
方法的对象。
- 因为它只有一个方法,所以容易模拟。
- 容易发送多个
Endpoint
。 - 容易组合装饰器或适配器。例如,你可以将其包裹在
Combine
中,无需为每个请求创建包装器。
Client
和 Endpoint
的分离允许你在 Client
中分离异步代码,在 Endpoint
中分离同步代码。因此,副作用被隔离在 Client
中,而纯函数则在不可变的 Endpoint
中。
入门
由于大多数请求将接收 JSON,因此有必要在模块级别创建基本协议。它们将包含特定 API 的常见请求逻辑。
JsonEndpoint
—— 响应体中等待 JSON 的请求的基本协议。
public protocol JsonEndpoint: Endpoint where Content: Decodable {}
extension JsonEndpoint {
public func validate(_ response: URLResponse?, with body: Data) throws {
// TODO: check API / HTTP error
}
public func content(from response: URLResponse?, with body: Data) throws -> Content {
return try JSONDecoder().decode(Content.self, from: body)
}
}
VoidEndpoint
—— 不等待响应体的请求的基本协议。
public protocol VoidEndpoint: Endpoint where Content == Void {}
extension VoidEndpoint {
public func validate(_ response: URLResponse?, with body: Data) throws {
// TODO: check API / HTTP error
}
public func content(from response: URLResponse?, with body: Data) throws {}
}
BookListEndpoint
—— 获取书籍列表。
public struct BookListEndpoint: JsonEndpoint, URLRequestBuildable {
public typealias Content = [Book]
public func makeRequest() throws -> URLRequest {
return get(URL(string: "books")!)
}
}
BookEndpoint
—— 通过 ID
获取书籍。
public struct BookEndpoint: JsonEndpoint, URLRequestBuildable {
public typealias Content = Book
public let id: Book.ID
public init(id: Book.ID) {
self.id = id
}
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(id)
return get(url)
}
}
UpdateBookEndpoint
—— 更新书籍。
public struct UpdateBookEndpoint: JsonEndpoint, URLRequestBuildable {
public typealias Content = Book
public let Book: Book
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(Book.id)
return put(url, body: .json(try JSONEncoder().encode("Book")))
}
}
为了方便创建
URLRequest
,你可以使用HTTP
中的函数。
DeleteBookEndpoint
—— 通过 ID
删除书籍。
public struct DeleteBookEndpoint: VoidEndpoint, URLRequestBuildable {
public let id: Book.ID
public init(id: Book.ID) {
self.id = id
}
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(id)
return delete(url)
}
}
向服务器发送大量数据
您可以使用UploadEndpoint
来发送文件或大量数据。在makeRequest()
方法中,您需要返回URLRequest
和您要上传的数据,可以是一个文件.file(URL)
,数据.data(Data)
或流.stream(InputStream)
。要执行请求,请调用Client.upload(endpoint: completionHandler:)
方法。使用Progress
对象来跟踪数据上传进度或取消请求。
public struct FileUploadEndpoint: UploadEndpoint {
public typealias Content = Void
private let fileUrl: URL
init(fileUrl: URL) {
self.fileUrl = fileUrl
}
public func content(from response: URLResponse?, with body: Data) throws {
// ...
}
public func makeRequest() throws -> (URLRequest, UploadEndpointBody) {
var request = URLRequest(url: URL(string: "upload")!)
request.httpMethod = "POST"
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
return (request, .file(fileUrl))
}
}
网络层组织
如果您的应用程序名为Household
,则网络模块将被命名为HouseholdAPI
。
将网络层分为文件夹
Model
文件夹包含网络级模型。这是我们发送到服务器的和从响应获得的内容。Endpoint
文件夹包含请求。Common
文件夹包含常用助手,例如APIError
。
最终文件和文件夹结构
- Household
- HouseholdAPI
- Model
- Book
- Endpoint
JsonEndpoint
VoidEndpoint
- Book
BookListEndpoint
BookEndpoint
UpdateBookEndpoint
DeleteBookEndpoint
- Common
APIError
- Model
- HouseholdAPITests
- Endpoint
Book
BookListEndpointTests
BookEndpointTests
UpdateBookEndpointTests
DeleteBookEndpointTests
- Endpoint
需求
- iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+
- Xcode 12+
- Swift 5.3+