# introduction

If you have looked at NetworkService in the previous source, you will see that the use of Moya + RxSwift is very primitive. Now let’s try to encapsulate the following NetworkService, providing:

  • Caches network request results and displays local cached data at startup

  • Provide time-by-time caching for data that does not need to be requested every time

  • The unified RxSwift interface is provided externally. For new functions, only the invocation of corresponding functions need to be annotated, and the subsequent methods need not be modified

Unified network request interface

In this article we use the global variable kDynamicProvider to make network requests:

Let kDynamicProvider = MoyaProvider<XTNetworkService>()... . / / network request kDynamicProvider. Rx. Request (. List (param: param toJsonDict ()))Copy the code

The need to provide this form of global variables repeatedly for different interfaces (e.g., article related interfaces) makes it difficult to uniformly add Plugins and so on. Using the same MoyaProvider instance for all interfaces increases the amount of code in the enum, which is not conducive to code reading and maintenance. So, this is the part that we encapsulate first.

First create XTNetworkCacheExtension. Swift file add the following code:

import Foundation
import RxSwift
import Moya

/// The provider that actually sends the network request
private let xtProvider = MoyaProvider<MultiTarget> ()public extension TargetType {

    // make a network request directly
    func request(a) -> Single<Response> {
        return xtProvider.rx.request(.target(self))}}Copy the code

Now you can delete the kDynamicProvider and go back to DynamicListViewModel and replace the kDynamicProvider as follows

// Need to replace the code
kDynamicProvider.rx.request(.list(param: param.toJsonDict()))

// Final code
DynamicNetworkService.list(param: param.toJsonDict()).request()
Copy the code

This concludes the first step.

2. Add the function of caching by time

Start by defining the cacheTime cacheTime and TargetType as a meta-ancestor

public typealias CacheTimeTargetTuple = (cacheTime: TimeInterval, target: TargetType)
Copy the code

Add an interface for caching by time after the request method in extension TargetType:

// Use the time cache policy. If there is data in memory, do not request network
func memoryCacheIn(_ seconds: TimeInterval = 180) -> Single<CacheTimeTargetTuple> {
    return Single.just((seconds, self))}Copy the code

Note: Here’s a bonus point — something you already know if you’ve read the RxSwift source code:

public typealias Single<Element> = PrimitiveSequence<SingleTrait.Element>
Copy the code

PrimitiveSequence

is an alias for PrimitiveSequence. PrimitiveSequence

is extended to PrimitiveSequence

.
,>
,>

extension PrimitiveSequence where Trait= =SingleTrait.Element= =CacheTimeTargetTuple {

    public func request(a) -> Single<Response> {
        / / 1.
        flatMap { tuple -> Single<Response> in
            let target = tuple.target

            / / 2.
            if let response = target.cachedResponse() {
                return .just(response)
            }

            / / 3.
            let cacheKey = target.cacheKey
            let seconds = tuple.cacheTime
            / / 4.
            let result = target.request().cachedIn(seconds: seconds, cacheKey: cacheKey)
            return result
        }
    }
}
Copy the code
  • in1Only one of them is rightPrimitiveSequenceextensionBefore you can use it directly.flatMap(omitted herereturn)
  • in2We used incacheformemorydiskstorage
  • in3Is our extended cachekey, see the specific code at the end of the supplement, or refer togithubThe source code
  • in4In thecachedIn(seconds:, cacheKey:)That’s what we actually domemoryCached code

Implement func cachedIn(seconds:, cacheKey:)

extension PrimitiveSequence where Trait= =SingleTrait.Element= =Response {

    fileprivate func cachedIn(seconds: TimeInterval.cacheKey: String) -> Single<Response> {
        flatMap { response -> Single<Response> in
            kMemoryStroage.setObject(response, forKey: cacheKey, expiry: .seconds(seconds))
            return .just(response)
        }
    }
}	
Copy the code

Add code to read cache in TargetType:

/// Data cached in memory
fileprivate func cachedResponse(a) -> Response? {

    do {
        let cacheData = try kMemoryStroage.object(forKey: cacheKey)
        if let response = cacheData as? Response {
            return response
        } else {
            return nil}}catch {
        print(error)
        return nil}}Copy the code

With this function done, I ended up not being able to call the cache interface as follows:

DynamicNetworkService.topicListRecommend
.memoryCacheIn()
.request()
Copy the code

To not use caching, comment out.memoryCachein ().

Implement disk cache function

Struct OnDiskStorage

is an alternative encapsulation method for disk caching.

  1. The statementOnDiskStorage:
// MARK: - Cache on disk

public struct OnDiskStorage<Target: TargetType.T: Codable> {
    fileprivate let target: Target
    private var keyPath: String = ""

    fileprivate init(target: Target.keyPath: String) {
        self.target = target
        self.keyPath = keyPath
    }

    /// Each wrapped structure provides a request method for subsequent chain calls to remove unwanted functionality
    ///
    /// For example, 'provider.memoryCachein (3*50).request()'. MemoryCacheIn (3*50) can still be used normally
    public func request(a) -> Single<Response> {
        return target.request().flatMap { response -> Single<Response> in
            do {
                let model = try response.map(T.self)
                try target.writeToDiskStorage(model)
            } catch {
                // nothings to do
                print(error)
            }

            return .just(response)
        }
    }
}
Copy the code
  1. rightTargetTypeaddonStorage.writeToDiskStoragereadDiskStoragemethods
// read the disk cache, usually used to load data at startup, and then actually read the network data
func onStorage<T: Codable> (_ type: T.Type.atKeyPath keyPath: String = "".onDisk: ((T) - > ())?) -> OnDiskStorage<Self.T> {
    if let storage = readDiskStorage(type) { onDisk?(storage) }

    return OnDiskStorage(target: self, keyPath: keyPath)
}

/// Read from disk
fileprivate func readDiskStorage<T: Codable> (_ type: T.Type) -> T? {
    do {
        let config = DiskConfig(name: "\(type.self)")
        let transformer = TransformerFactory.forCodable(ofType: type.self)
        let storage = try DiskStorage<String.T>.init(config: config, transformer: transformer)
        let model = try storage.object(forKey: cacheKey)
        return model
    } catch {
        print(error)
        return nil}}fileprivate func writeToDiskStorage<T: Codable> (_ model: T) throws {
    let config = DiskConfig(name: "\(T.self)")
    let transformer = TransformerFactory.forCodable(ofType: T.self)
    let storage = try DiskStorage<String.T>.init(config: config, transformer: transformer)
    try storage.setObject(model, forKey: cacheKey)
}
Copy the code

Now you can use the interface as follows:

DynamicNetworkService.list(param: param.toJsonDict()).onStorage(XTListResultModel.self) {[weak self] diskModel in
    // Populate the UI with disk Model
    self?.diskCacheSubject.onNext(diskModel)
}.request()
Copy the code

At this point, the simple encapsulation of Moya is complete. Thank you for reading

Supplement:

# cache key code

There are two ways to generate the cached key, one is generated from the TargetType instance, and one is passed in from the external. Here, the TargetType is used to generate the cache key, the code is as follows:

  • The Swift development

    // MARK: - Swift.Collection
    
    private extension String {
    
        var sha256: String {
            guard let data = data(using: .utf8) else { return self }
    
            var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
    
            _ = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in
                return CC_SHA256(bytes.baseAddress, CC_LONG(data.count), &digest)
            }
    
            return digest.map { String(format: "%02x".$0) }.joined()
        }
    }
    
    // TODO:- Need to do XCTest
    
    private extension Optional {
        var stringValue: String {
            switch self {
            case .none:
                return ""
            case .some(let wrapped):
                return "\(wrapped)"}}}private extension Optional where Wrapped= =Dictionary<String.Any> {
        var stringValue: String {
            switch self {
            case .none:
                return ""
            case .some(let wrapped):
                let allKeys = wrapped.keys.sorted()
                return allKeys.map { $0 + ":" + wrapped[$0].stringValue }.joined(separator: ",")}}}private extension Optional where Wrapped: Collection.Wrapped.Element: Comparable {
        var stringValue: String {
            switch self {
            case .none:
                return ""
            case .some(let wrapped):
                return wrapped.sorted().reduce("") { $0 + "\ [The $1)"}}}}private extension Dictionary where Key= =String {
    
        var sortedDescription: String {
            let allKeys = self.keys.sorted()
            return allKeys.map { $0 + ":" + self[$0].stringValue }.joined(separator: ",")}}Copy the code
  • Extend the cache code for TargetType

    // MARK: - Cache related
    
    fileprivate extension TargetType {
    
        /// Cache key
        var cacheKey: String {
            let key = "\(method)\(URL(target: self).absoluteString)\ [self.path)?\(task.parameters)"
            return key.sha256
        }
    }
    
    fileprivate extension Task {
    
        var canCactch: Bool {
            switch self {
            case .requestPlain:
                fallthrough
            case .requestParameters(_._) :fallthrough
            case .requestCompositeData(_._) :fallthrough
            case .requestCompositeParameters(_ , _._) :return true
            default:
                return false}}var parameters: String {
            switch self {
            case .requestParameters(let parameters, _) :return parameters.sortedDescription
            case .requestCompositeData(_.let urlParameters):
                return urlParameters.sortedDescription
            case .requestCompositeParameters(let bodyParameters, _.let urlParameters):
                return bodyParameters.sortedDescription + urlParameters.sortedDescription
            default:
                return ""}}}Copy the code

# source

XTDemo SUN branch.