Apexy 1.7.2

Apexy 1.7.2

Aleksey TyurninAlexander IgnitionDaniil SubbotinAnton Glezman 维护。



Apexy 1.7.2

  • Alexander Ignatiev

Apexy

CocoaPods Compatible Platform SPM compatible Swift 5.3 GitHub license codebeat badge

在项目中组织网络层的库。

  • 将用于网络的对象分离到独立的模块、目标或库,使它们在 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 工作的基本协议之一。它是一组请求和响应处理。

不可变。

  1. 创建用于发送请求的 URLRequest
  2. 检查服务器响应以验证 API 错误。
  3. 将服务器响应转换为正确的类型(DataStringDecodable)。
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 中,无需为每个请求创建包装器。

ClientEndpoint 的分离允许你在 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
  • HouseholdAPITests
    • Endpoint
      • Book
        • BookListEndpointTests
        • BookEndpointTests
        • UpdateBookEndpointTests
        • DeleteBookEndpointTests

需求

  • iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+
  • Xcode 12+
  • Swift 5.3+

额外资源