WebServiceSwift 3.1.1

WebServiceSwift 3.1.1

ProVir维护。



  • ViR (Vitaliy Korotkiy)

WebServiceSwift

CocoaPods Compatible Carthage Compatible Platform License

网络层作为服务。服务作为与您的 Web 服务器交互的接口。支持 Swift 5。

在项目中使用 WebServiceSwift 的通用方案。

Scheme

特性

  • 使用方便的接口
  • 所有网络操作都在内部端点和存储中进行,对接口隐藏。
  • 支持 Dispatch Queue。
  • 使用一个类处理多种类型的请求。
  • 使用一个实例处理多个端点和存储。
  • 包中包含简单的存储(硬盘、数据库或内存中)。容易 - 只需添加自己的 API 端点即可完成!
  • 支持 iOS 上的 NetworkActivityIndicator。
  • 线程安全。
  • 在完成处理器的闭包中带有具体类型的响应。
  • RequestProvider 专门用于处理具体请求。用于指示更明确的依赖关系(DIP)。
  • 模拟端点,用于提供临时或测试响应数据,而不使用实际的 API 端点。
  • 完全支持 Alamofire(包括基本端点)。
  • 简单的 HTTP 端点(NSURLSession 或 Alamofire)。
  • 支持作为静态框架

需求

  • iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 9.0 和以上
  • Swift 4.0 和以上

通信

  • 如果您需要帮助,请访问 provir.ru
  • 如果您发现了bug,请提交问题。
  • 如果您有功能请求,请提交问题。
  • 如果您想要贡献,请提交拉取请求。

安装

CocoaPods

CocoaPods 是 Cocoa 项目的依赖关系管理器。您可以使用以下命令安装它

$ gem install cocoapods

要构建 WebServiceSwift 3.1.0+,需要 CocoaPods 1.1.0+。

要使用 CocoaPods 将 WebServiceSwift 集成到您的 Xcode 项目中,请指定它到您的 Podfile

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'

target '<Your Target Name>' do
    pod 'WebServiceSwift', '~> 3.1'
end

您也可以使用 Alamofire 端点

pod 'WebServiceSwift/Alamofire', '~> 3.1'

或者只包含核心库,不含简单端点和存储

pod 'WebServiceSwift/Core', '~> 3.1'

然后,运行以下命令

$ pod install

Carthage

Carthage 是一个去中心化依赖关系管理器,构建您的依赖并提供您二进制框架。

您可以使用以下命令使用 Homebrew 安装 Carthage

$ brew update
$ brew install carthage

要将 WebServiceSwift 集成到您的 Xcode 项目中使用 Carthage,请将其指定到您的 Cartfile

github "ProVir/WebServiceSwift" ~> 3.1

运行 carthage update 构建 framework 并将构建的 WebServiceSwift.framework 拖入您的 Xcode 项目。

Swift 包管理器

Swift 包管理器 是自动化 Swift 代码分发的一个工具,并且整合到了 swift 编译器中。它目前处于早期开发阶段,但 WebServiceSwift 支持在受支持的平台上使用它。

一旦设置好Swift包,将WebServiceSwift作为依赖添加就如同在Package.swift中的dependencies值里添加它一样简单。

dependencies: [
    .package(url: "https://github.com/ProVir/WebServiceSwift.git", from: "3.1.0")
]

手动

如果您不希望使用上述任何依赖管理器,可以将WebServiceSwift手动集成到项目中。

从项目中的Source目录复制文件。


用法(英文 / 俄语)

使用该库,您需要

  1. 创建至少一种实现WebServiceRequesting协议的请求类型。
  2. 创建至少一个用于网络操作(端点)的类,实现WebServiceEndpoint协议。它应该提供自己的协议,通过扩展实现该协议将允许此端点处理请求(见示例)。
  3. 如果您需要,可以创建一个用于存储上次响应的类的类,或者使用现有的类 - WebServiceFileStorageWebServiceDataBaseStorageWebServiceMemoryStorage
  4. 如果您需要,可以创建一个用于API部分不实现时的模拟请求的类 - WebServiceMockEndpoint或它的子类WebServiceMockRequestEndpoint用于单种类型的请求。建议首先将其在endpoints数组中。
  5. 编写生成WebService对象的方法。例如,可以使用工厂来生成WebService对象或为它编写一个扩展,其中包括便利构造函数。

注意:使用该库时,请务必在每个文件中使用它:import WebServiceSwift

该项目有一个使用该库的更具体示例,可以单独下载。研究WebServiceSimpleEndpointWebServiceAlamofireSimpleEndpoint类 - 它们是端点的好例子。

要创建具有相同端点和存储的服务不同实现的可不兼容副本,您可以调用WebService.clone()。每个副本独立管理其请求,当一个服务实例被销毁时,会自动取消这些请求。

要使用该库,您需要

  1. 创建至少一个实现WebServiceRequesting协议的请求类型。
  2. 创建至少一个实现WebServiceEndpoint协议的用于网络操作的类。它应该提供自己的协议,通过扩展实现该协议将允许该类处理请求(参见示例)。
  3. 根据需要,可以创建一个用于存储最后响应的类的类,或者使用现有的类 - WebServiceFileStorageWebServiceDataBaseStorageWebServiceMemoryStorage
  4. 根据需要,可以创建一个用于处理API部分未实现时的模拟请求的类 - WebServiceMockEndpoint或从它派生的类,WebServiceMockRequestEndpoint用于单一类型的请求。建议在endpoints列表中首先使用它。
  5. 编写方法以获得已准备好的WebService对象。例如,可以使用工厂或为它编写一个扩展,其中包括便利构造函数。

注意:使用库时,请务必在每个文件中导入它:import WebServiceSwift

项目中有一个更具体使用库的示例,可以单独下载。研究一下 WebServiceSimpleEndpointWebServiceAlamofireSimpleEndpoint 类,它们是端点处理器的良好示例。

若要创建具有相同处理器和存储配置的独立服务副本,可以调用 WebService.clone()。每个副本独立管理自己的请求,并且当服务实例被删除时自动取消请求。

端点

Swift 4.1+ 示例请求结构

struct ExampleRequest: WebServiceRequesting, Hashable {
    let param1: String  
    let param2: Int 
    
    typealias ResultType = String
}

可以实现任意数量的此类请求,并使用不同端点进行处理,这些端点会自动从列表中选取。存储版本也可以有多个。通常,您应该为自己创建端点以与API交互,但有时基本功能的现成解决方案可能已经足够—— WebServiceSimpleEndpointWebServiceAlamofireSimpleEndpoint

这样的请求类型可以有任意多个,并且可以使用不同的端点进行批量处理,这些端点会自动从列表中选取。存储的种类也可以是多个。一般而言,应为API创建自己的端点,但有时现成解决方案的基本功能,如 WebServiceSimpleEndpointWebServiceAlamofireSimpleEndpoint,就足够了。

使用库 Alamofire 搭建的端点示例

protocol WebServiceHtmlRequesting: WebServiceBaseRequesting {
    var url: URL { get }
}

class WebServiceHtmlEndpoint: WebServiceEndpoint {
    let queueForRequest: DispatchQueue? = DispatchQueue.global(qos: .background)
    let queueForDataProcessing: DispatchQueue? = nil
    let queueForDataProcessingFromStorage: DispatchQueue? = DispatchQueue.global(qos: .background)
    let useNetworkActivityIndicator = true

    func isSupportedRequest(_ request: WebServiceBaseRequesting, rawDataTypeForRestoreFromStorage: Any.Type?) -> Bool {
        return request is WebServiceHtmlRequesting
    }

    func performRequest(requestId: UInt64, request: WebServiceBaseRequesting,
                        completionWithRawData: @escaping (_ data: Any) -> Void,
                        completionWithError: @escaping (_ error: Error) -> Void) {

        guard let url = (request as? WebServiceHtmlRequesting)?.url else {
            completionWithError(WebServiceRequestError.notSupportRequest)
            return
        }

        Alamofire.request(url).responseData { response in
            switch response.result {
            case .success(let data):
                completionWithRawData(data)

            case .failure(let error):
                completionWithError(error)
            }
        }
    }

    func canceledRequest(requestId: UInt64) { /* Don't support in example */ }

    func dataProcessing(request: WebServiceBaseRequesting, rawData: Any, fromStorage: Bool) throws -> Any {
        guard request is WebServiceHtmlRequesting, let binary = rawData as? Data else {
            throw WebServiceRequestError.notSupportDataProcessing
        }
    
        if let result = String(data: binary, encoding: .utf8) ?? String(data: binary, encoding: .windowsCP1251) {
            return result
        } else {
            throw WebServiceResponseError.invalidData
        }
    }
}

使用 Alamofire 基端点的端点示例

class WebServiceHtmlV2Endpoint: WebServiceAlamofireBaseEndpoint {
    init() {
        super.init(queueForRequest: DispatchQueue.global(qos: .background), useNetworkActivityIndicator: true)
    }

    override func isSupportedRequest(_ request: WebServiceBaseRequesting, rawDataTypeForRestoreFromStorage: Any.Type?) -> Bool {
        return request is WebServiceHtmlRequesting
    }

    override func performRequest(requestId: UInt64, data: RequestData) throws -> Alamofire.DataRequest? {
        guard let url = (data.request as? WebServiceHtmlRequesting)?.url else {
            throw WebServiceRequestError.notSupportRequest
        }

        return Alamofire.request(url)
    }

    override func dataProcessing(request: WebServiceBaseRequesting, rawData: Any, fromStorage: Bool) throws -> Any {
        guard request is WebServiceHtmlRequesting, let binary = rawData as? Data else {
            throw WebServiceRequestError.notSupportDataProcessing
        }

        if let result = String(data: binary, encoding: .utf8) ?? String(data: binary, encoding: .windowsCP1251) {
            return result
        } else {
            throw WebServiceResponseError.invalidData
        }
    }
}

重要——传递给 completionWithRawData 的数据不一定始终是 Data 类型,在这种情况下,建议该类型实现 WebServiceRawDataSource 协议。需要二进制数据以便能够将在存储中作为原始数据保存。值得注意的是,从 Storage 到 Handler,读取的数据形式为 Data,应考虑到这一点。

重要点:传入 completionWithRawData 的数据不一定总是 Data 类型,在这种情况下,建议这个类型实现 WebServiceRawDataSource 协议。二进制数据需要存储为缓存中的 Raw 数据。需要注意的是,从存储器到处理程序读取的数据以 Data 的形式传入,因此应考虑到这一点。

示例

/// Data from server as raw, used only as example
struct ServerData: WebServiceRawDataSource {
    let statusCode: Int
    let binary: Data

    var binaryRawData: Data? { return binary }
}

func dataProcessing(request: WebServiceBaseRequesting, rawData: Any, fromStorage: Bool) throws -> Any {
    guard request is WebServiceHtmlRequesting else {
        throw WebServiceRequestError.notSupportDataProcessing
    }

    let binary: Data
    if let data = rawData as? Data {
        //Data from Storage
        binary = data
    } else if let data = rawData as? ServerData {
        //Data from server
        binary = data.binary
    } else {
        throw WebServiceRequestError.notSupportDataProcessing
    }

    return String(data: binary, encoding: .utf8) ?? String(data: binary, encoding: .windowsCP1251) ?? ""
}

端点(endpoint)与网络的交互不是必需的。在此层后面,你可以隐藏数据库的操作,或者至少暂时设置为空端点。从服务接口这一侧来看,这并不重要,并且在对端点进行开发时,可以不经意间替换,而无需更改请求自身代码。

端点(endpoint)并不一定需要网络处理。在此层后面,你可以隐藏数据库操作或临时设置一个占位符。从服务接口的角度看,这并不重要,在开发端点时,可以不引人注意地进行替换,而无需修改请求的代码。

简单请求支持具体端点示例

extension ExampleRequest: WebServiceHtmlRequesting {
    var url: URL {
        /* .... Logic create URL from param1 and param2 ... */
    }
    
    func decodeResponse(data: Data) throws -> String {
        /* ... Create concrete decoder data from server and decoding ... */
    }
}

每个请求必须实现每个可以处理它的端点支持协议。如果单个请求支持多个端点,则从 endpoints 数组中选择支持的第一个端点进行处理。

每个请求必须实现它所能处理的每个端点(endpoint)的支持协议。如果单个请求支持多个端点,则从 endpoints 列表中选择第一个支持端点进行处理。

创建 WebService 服务 - 使用工厂构造函数示例

extension WebService {
    convenience init() {
        let endpoint = WebServiceHtmlV2Endpoint()
        
        var storages: [WebServiceStoraging] = []
        if let storage = WebServiceDataBaseStorage() {
            storages.append(storage)
        }
        
        self.init(engines: [WebServiceMockEndpoint(), endpoint], storages: storages)
    }
}

你也可以制作单例(singleton)的支持——源代码示例中有这种方法的示例。

同样可以创建单例(singleton)支持——源代码示例中有这种方式的示例。

闭包的使用示例

let webService = WebService()

webService.performRequest(ExampleRequest(param1: val1, param2: val2)) { [weak self] response in
    switch response {
    case .canceledRequest: 
        break

    case .data(let dataCustomType):
        self?.dataFromServer = dataCustomType
        
    case .error(let error):
        self?.showError(error)
    }
}

我们将在请求数据中传递,在输出中我们获得用于显示的已准备对象。

Передаем данные в запросе, на выходе получаем готовый объект для отображения。

使用委托的示例

let webService = WebService()

webService.performRequest(ExampleRequest(param1: val1, param2: val2), responseDelegate: self)

func webServiceResponse(request: WebServiceRequesting, key: AnyHashable?, isStorageRequest: Bool, response: WebServiceAnyResponse) {
    if let request = request as? ExampleRequest {
        let response = response.convert(request: request)

        switch response {
        case .canceledRequest: 
            break

        case .data(let dataCustomType):
            if isStorageRequest {
                dataFromStorage = dataCustomType
            } else {
                dataFromServer = dataCustomType
            }
                
        case .error(let error):
            showError(error)
        }
    }
}

管理请求

可执行请求可以受到控制 - 检查执行(包含)和取消。在endpoint中,您可以实现取消查询执行的方法 - 这对于优化是必要的,无论您是否在endpoint中取消请求,请求都将被取消,并且其结果将被忽略。

请求可以通过多种方式进行管理

  • 全部: containsManyRequests()cancelAllRequests();
  • 对于请求实例,如果它是哈希的(Request: WebServiceBaseRequesting, Hashable):containsRequest(Request)cancelRequests(Request);
  • 通过请求类型: containsRequest(type: WebServiceBaseRequesting.Type)cancelRequests(type: WebServiceBaseRequesting.Type);
  • 通过键(更多关于此): containsRequest(key:)cancelRequests(key:);
  • 通过键类型: containsRequest(keyType:)cancelRequests(keyType:).

所有已取消的请求将结束于响应 WebServiceResponse.canceledRequest(duplicate: false).

可执行请求可以受到控制 - 检查执行(包含)和取消。在endpoint中,您可以实现取消查询执行的方法 - 这对于优化是必要的,无论您是否在endpoint中取消请求,请求都将被取消,并且其结果将被忽略。

请求可以通过多种方式来进行管理

  • 全部: containsManyRequests()cancelAllRequests();
  • 对于请求实例,如果它是哈希的(Request: WebServiceBaseRequesting, Hashable):containsRequest(Request)cancelRequests(Request);
  • 按请求类型: containsRequest(type: WebServiceBaseRequesting.Type)cancelRequests(type: WebServiceBaseRequesting.Type);
  • 按键(关于此的更多信息): containsRequest(key:)cancelRequests(key:);
  • 按键类型: containsRequest(keyType:)cancelRequests(keyType:).

所有已取消的请求将结束于响应 WebServiceResponse.canceledRequest(duplicate: false).

示例包含和取消请求

struct ExampleKey: Hashable {
    let value: String
}

let isContains1 = webService.containsRequest(ExampleRequest(param1: val1, param2: val2))
let isContains2 = webService.containsRequest(type: ExampleRequest.self)
let isContains3 = webService.containsRequest(key: ExampleKey(value: val1))
let isContains3 = webService.containsRequest(keyType: ExampleKey.self)

webService.cancelRequests(ExampleRequest(param1: val1, param2: val2))
webService.cancelRequests(type: ExampleRequest.self)
webService.cancelRequests(key: ExampleKey(value: val1))
webService.cancelRequests(keyType: ExampleKey.self)

您还可以排除重复请求。为此,请求必须实现协议 Hashable 或在使用请求时(任何实现协议Hashable的类型)使用键。如果请求被判定为重复,将立即以响应 WebServiceResponse.canceledRequest(duplicate: true) 完成。

也可以排除重复请求。为此,请求必须实现协议 Hashable 或在使用请求时(任何实现协议 Hashable 的类型)使用键。那些被判定为重复的请求将立即以响应 WebServiceResponse.canceledRequest(duplicate: true) 完成。

测试重复请求的示例

webService.performRequest(ExampleRequest(param1: val1, param2: val2), excludeDuplicate: true) { [weak self] response in
    switch response {
    case .canceledRequest(duplicate: let duplicate): 
        if duplicate {
            print("Request is duplicate!")
        }

    case .data(let dataCustomType):
        self?.dataFromServer = dataCustomType

    case .error(let error):
        self?.showError(error)
    }
}

webService.performRequest(ExampleRequest(param1: val1, param2: val2), key: ExampleKey(value: val1), excludeDuplicate: true) { [weak self] response in
    switch response {
    case .canceledRequest(duplicate: let duplicate): 
        if duplicate {
            print("Key is duplicate!")
        }

    case .data(let dataCustomType):
        self?.dataFromServer = dataCustomType

    case .error(let error):
        self?.showError(error)
    }
}

服务提供商

为了在项目中添加更明确的依赖项,以及防止某些错误,您可以使用服务提供商。服务提供商是覆盖 WebService 的包装器,并通过私有访问来隐藏它。它们提供有限类型的请求访问,这些请求通过方便的接口提供,排除代码中一定类别的错误。服务提供商的主要目的是在代码的正确位置提供对 WebService 功能允许部分的访问。

要向您的项目添加更明确的依赖项,并防止某些错误,请使用服务提供商。服务提供商是覆盖 WebService 的包装器,并使用私有访问隐藏它。它们只提供有限类型的请求访问,这些请求通过方便的接口提供,排除代码中一定类别的错误。服务提供商的主要目的是在代码的正确位置提供对可允许的 WebService 功能部分的访问。

自定义服务提供商示例

enum SiteWebServiceRequests {
    struct Example: WebServiceRequesting, Hashable {
        let site: String  
        let domainRu: Bool
    
        typealias ResultType = String
    }
    
    struct GetList: WebServiceEmptyRequesting, Hashable {
        typealias ResultType = [String]
    }
}


class SiteWebProvider: WebServiceProvider {
    private let webService: WebService

    required init(webService: WebService) {
        self.webService = webService
    }
    
    enum Site: String {
    case google
    case yandex
    }

    func requestExampleData(site: Site, domainRu: Bool = true, completionHandler: @escaping (_ response: WebServiceResponse<String>) -> Void) {
        webService.performRequest(SiteWebServiceRequests.Example(site: site.rawValue, domainRu: domainRu), completionHandler: completionHandler)
    }
}

您可以使用为一种类型请求准备的通用提供商类 - WebServiceRequestProvider

您可以使用已经预定义的提供者类模板 - WebServiceRequestProvider,用于一种请求类型。

示例提供者

let getListSiteWebProvider: WebServiceRequestProvider<SiteWebServiceRequests.GetList>
let exampleSiteWebProvider: WebServiceRequestProvider<SiteWebServiceRequests.Example>

init(webService: WebService) {
    getListSiteWebProvider = webService.createProvider()
    exampleSiteWebProvider = webService.createProvider()
}

func performRequests() {
    // RequestProvider for WebServiceEmptyRequesting
    getListSiteWebProvider.performRequest() { [weak self] response in
        switch response {
        case .canceledRequest(duplicate: let duplicate): 
            break
    
        case .data(let list):
            self?.sites = list
    
        case .error(let error):
            self?.showError(error)
        }
    }
    
    // RequestProvider for request with params
    exampleSiteWebProvider.performRequest(.init(site: "google", domainRu: false)) { _ in }
}

存储

为了使应用程序在没有互联网的情况下也能工作,将接收到的数据保存在永久存储中非常有用,因为用户设备上存储的数据通常读取速度要快得多,并且不受互联网连接状态的依赖。在大多数情况下,这足以存储最后一次从服务器接收到的响应,并在需要时提供它。服务器的存储就是为了这种情况而提供的。

您可以通过实现WebServiceStorage协议来创建自己的存储类。但在大多数情况下,这并不需要,因为已经存在可用于所有必要使用场景的预定义类 - WebServiceFileStorageWebServiceDataBaseStorageWebServiceMemoryStorage。在大多数情况下,您只需从中选择一个,但在更复杂的逻辑情况下,可以通过不同的设置组合和重复使用它们,按数据的分类进行分隔(更多内容见下文)。

并非所有请求都保存在存储中,只有满足以下任一条件的请求才会保存:通用存储协议(推荐)或特定存储类型的协议。数据通常以两种版本进行存储

  • 原始:来自服务器的原始数据(通常是二进制)。这种类型的数据在读取后会发送到适当的终点进行处理;
  • 值:已处理的数据。这种类型的数据在读取后直接作为结果发送。

原始数据很方便,因为它不需要写入二进制转换程序,因为来自服务器的数据通常已经是以这种形式。值更优化,因为数据已经处理,不需要重新处理,但需要提供转换到二进制类型和相反的转换器(通常使用Codable)。如果从服务器到值的转换器的处理复杂性相同,那么使用原始数据更好,否则,如果可能的话,使用值 - 取决于数据和处理请求。

通常,存储除了数据(时间戳)外还可以提供数据保存的时间 - 这可以使用来评估数据是否已经过时。

存储中的数据可以按以下任一特性进行删除

  • 对于具体请求:WebService.deleteInStorage(request:);
  • 特定存储中的全部数据,仅针对特定分类的数据:WebService.deleteAllInStorages(withDataClassification:);
  • 特定存储中的全部数据,不限分类,排除具有特定数据分类列表的存储:WebService.deleteAllInStoragesWithAnyDataClassification();
  • 所有存储中的全部数据:WebService.deleteAllInStorages();

为了使应用程序能离线运行,将获取的数据保存在持久存储中非常有用,因为通常设备上的数据读取速度要比通过网络读取快得多,并且不受网络连接状态的影响。在大多数情况下,只需要保存最后从服务器获取的响应,并按照需求提供即可。为此,在服务中准备了一个存储器。

你可以创建自己的存储类——需要实现 WebServiceStorage 协议。但通常不需要这样做,因为已经存在可以使用的类,它们覆盖了所有必要的使用场景——WebServiceFileStorageWebServiceDataBaseStorageWebServiceMemoryStorage。在大多数情况下,你只会使用其中之一,但在更复杂的逻辑中,它们可以组合并重复使用,以不同的配置分离数据类别(下面会有更多介绍)。

并非所有请求都会保存到存储器中,只有符合总存储协议(推荐)或特定存储器协议的请求才会保存。数据可以以两种方式存储

  • 原始数据:来自服务器的未处理数据(通常是二进制)。此类型数据读取后将被发送到适当的处理器(endpoint)进行处理;
  • 值:处理过的数据。此类型数据读取后即作为结果发送。

原始数据方便之处在于不需要写入二进制数据的转换器,因为从服务器接收到的数据通常已经是这种格式。值更优化,因为数据已处理,无需重复处理,但需要提供向二进制类型和反二进制的转换器(通常使用 Codable)。如果从服务器到二进制类型的转换器与转换器在二进制类型之间的复杂性相同,则首选使用原始数据,否则在可能的情况下使用值——取决于数据和请求处理器。

通常,存储器可以提供与数据一起保存的时间戳(timeStamp)(这是评估数据是否过时的方法)。

可以通过某些特征从存储器中删除数据。

  • 针对特定请求: WebService.deleteInStorage(request:);
  • 指定存储器中的所有数据,只用于特定数据分类的: WebService.deleteAllInStorages(withDataClassification:);
  • 指定存储器中的所有数据,用于任何分类(具有特定分类列表的存储器将被忽略): WebService.deleteAllInStoragesWithAnyDataClassification();
  • 所有存储器中的所有数据: WebService.deleteAllInStorages();

示例支持请求存储

extension SiteWebServiceRequests.Example: WebServiceRequestRawGeneralStoring {
    var identificatorForStorage: String? {
        return "SiteWebServiceRequests_Example"
    }
}

extension SiteWebServiceRequests.GetList: WebServiceRequestValueGeneralStoring {
    var identificatorForStorage: String? {
        return "SiteWebServiceRequests_GetList"
    }
    
    func writeDataToStorage(value: [String]) -> Data? {
        return try? PropertyListEncoder().encode(value)
    }
    
    func readDataFromStorage(data: Data) throws -> [String]? {
        return try PropertyListDecoder().decode([String].self, from: data)
    }
}

为了分离存储方法,可以根据特定的存储类型对请求进行分类——这是你决定的。默认情况下,所有数据都被分类为 WebServiceDefaultDataClassification = "default"

例如,这种案例可能很受欢迎:普通的缓存、用户缓存(退出账户时删除)和应用运行时仅在内存中存储的临时缓存。每个数据类别都有自己的存储器。

为了方便地分离存储方式,可以将请求按特定存储类型进行分类——具体类型由你决定。默认情况下,所有数据都被分类为 WebServiceDefaultDataClassification = "default"

例如,以下情况可能很受欢迎:常见的缓存、用户缓存(在退出账户时删除)以及仅在应用运行时存储在内存中的临时缓存。每个数据类别都有自己的存储器。

示例数据分类

enum WebServiceDataClass: Hashable {
    case user
    case temporary
}

extension WebService {
    static func create() -> WebService {
        let endpoint = WebServiceHtmlV2Endpoint()

        var storages: [WebServiceStorage] = []
        
        // Support temporary in memory Data Classification
        storages.append(WebServiceMemoryStorage(supportDataClassification: [WebServiceDataClass.temporary]))
   
        // Support user Data Classification
        if let dbUserURL = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("userCaches.sqlite"),
           let storage = WebServiceDataBaseStorage(sqliteFileUrl: dbUserURL, supportDataClassification: [WebServiceDataClass.user]) {
            storages.append(storage)
        }
        
        // Support any Data Classification (also can use WebServiceDataBaseStorage())
        if let storage = WebServiceFileStorage() {
            storages.append(storage)
        }

        return .init(endpoints: [endpoint], storages: storages)
    }
    
    func clearUserCaches() {
        deleteAllInStorages(withDataClassification: WebServiceDataClass.user)
    }
}

extension UserWebServiceRequests.GetInformation: WebServiceRequestRawGeneralStoring {
    var dataClassificationForStorage: AnyHashable { 
        return WebServiceDataClass.user
    }

    var identificatorForStorage: String? {
        return "UserInformation"
    }
}

存储中的数据应该始终明确请求。此请求可以关联到两种版本的到服务器的请求

  • WebService.ReadStorageDependencyType.dependSuccessResult: 如果没有错误地先于服务器上的数据到达,存储的请求将被取消;
  • WebService.ReadStorageDependencyType.dependFull: 如果服务器上的数据先于错误或请求被取消或为重复的到达,存储的请求将被取消。

应将附加到存储请求的服务器请求立即在存储请求之后调用 - 无论其类型如何,此请求都将被绑定。通过与其关联的作为.dependFull的主服务器请求,可以取消存储的显式请求。

存储中的数据总是需要明确请求的。此请求可以通过两种方式关联到服务器的请求

  • WebService.ReadStorageDependencyType.dependSuccessResult: 如果服务器上的数据先于错误到达,存储的请求将被取消;
  • WebService.ReadStorageDependencyType.dependFull: 如果服务器上的数据先于错误到达或请求被取消或为重复的,存储的请求将被取消。

附加到存储请求的服务器请求必须在存储请求之后立即调用 - 无论是何种类型,这个请求都会被绑定。只能通过与其关联的作为.dependFull的主服务器请求来取消存储的显式请求。

示例读取存储中的数据

let request = ExampleRequest(param1: val1, param2: val2)

webService.readStorageData(request, dependencyNextRequest: .dependFull) { [weak self] timeStamp, response in
    if case .data(let data) = response {
        if let timeStamp = timeStamp, timeStamp.timeIntervalSinceNow > -3600 {   //no longer than 1 hour
            self?.dataFromStorage = data
        }
    }
}

webService.performRequest(request, excludeDuplicate: true) { [weak self] response in
    switch response {
    case .canceledRequest: 
        break

    case .data(let dataCustomType):
        self?.dataFromServer = dataCustomType

    case .error(let error):
        self?.showError(error)
    }
}

webService.readStorageData(TestRequest(), dependencyNextRequest: .notDepend) { [weak self] timeStamp, response in
    if case .data(let data) = response {
        self?.testData = data
    }
}

/// responseOnlyData - ignore errors and canceled read
webService.readStorage(ExampleRequest(param1: val1, param2: val2), dependencyNextRequest: .dependSuccessResult, responseOnlyData: true, responseDelegate: self)

模拟端点

如果API的一部分不可用或您只需生成临时测试数据,您可以使用WebServiceMockEndpoint。模拟端点模拟从实际服务器接收和处理数据,并返回您指定的确切数据。为了维持其请求,您需要将请求扩展到WebServiceMockRequesting协议。

如果API的一部分不可用或您只需提供临时测试数据,您可以使用WebServiceMockEndpoint。Mock端点模拟从实际服务器获取和处理数据,并返回您指定的数据。为了支持其请求,需要将请求扩展到WebServiceMockRequesting协议。

请求支持模拟引擎示例

extension ExampleRequest: WebServiceMockRequesting {
    var isSupportedRequestForMock: Bool { return true }
    var mockTimeDelay: TimeInterval? { return 3 }

    var mockHelperIdentifier: String? { return "template_html" }
    func mockCreateHelper() -> Any? {
        return "<html><body>%[BODY]%</body></html>"
    }

    func mockResponseHandler(helper: Any?) throws -> String {
        if let template = helper as? String {
            return template.replacingOccurrences(of: "%[BODY]%", with: "<b>Hello world!</b>")
        } else {
            throw WebServiceResponseError.invalidData
        }
    }
}

为了支持您的请求数据类型,而不是使用 WebServiceMockRequesting,您可以通过继承 WebServiceMockEndpoint 并重写函数 isSupportedRequest()convertToMockRequest 来创建自己的模拟类。后一个函数通过实现协议 WebServiceMockRequesting 将您的请求转换为适合的类型。它的类用作处理模拟数据,最初在单元测试中十分有用——这样可以避免在主代码中添加模拟数据的实现。

另一个适用于单元测试的端点是 WebServiceMockRequestEndpoint。每个实例仅针对一种请求类型,所有处理都在服务配置的地方进行。这样的端点可以有任意多个。

要支持自己的请求类型,而不是 WebServiceMockRequesting,您可以通过继承 WebServiceMockEndpoint 并重写函数 isSupportedRequest()convertToMockRequest 来创建自己的模拟类。最后一个函数通过实现协议 WebServiceMockRequesting 将您的请求转换为适合的类型。此类用于处理模拟数据,最初主要用于单元测试——以免在主代码中添加模拟数据。

另一种适合单元测试的选项是 WebServiceMockRequestEndpoint。每个实例仅用于一种请求类型,所有的处理都在服务配置中进行。这种处理器可以有任意多个。

用于 WebServiceMockRequestEndpoint 的示例

let template = "<html><body>%[BODY]%</body></html>"
let mockRequest = WebServiceMockRequestEndpoint.init(timeDelay: 3) { (request: ExampleRequest) -> String in
    return template.replacingOccurrences(of: "%[BODY]%", with: "<b>Hello world from MockRequestEndpoint!</b>")
}

let webService = WebService(endpoints: [mockRequest, mockRequest2, mockRequest3], storages: [])

作者

ViR(短名:维塔利)

许可协议

WebServiceSwift 采用 MIT 许可协议发布。有关详细信息,请参阅 LICENSE 文件。