1. Association Protocols: Protocols with Associated Types (PATs).

  2. Type erasure: Unwrap classes that implement the generic protocol using an implemented generic class (see AnySequence in the system library). To solve problems where variables cannot be defined directly using generic protocols.

Two concepts are thrown up above, which are relatively easy to understand for association protocols, such as the following definition of Request:

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case patch = "PATCH"
    case delete = "DELETE"
}

protocol Request {
    associatedtype Output
    
    var url: URL {get}
    var method: HTTPMethod {get}
    func decode(_ data: Data) throws -> Output
}
Copy the code

Request is also a simpler association protocol. Output is an association type. Decode is intended to refer broadly to the conversion of binary to Output after a network request is completed. If we want to load an image, we can define ImageRequest:

enum Error: Swift.Error {
    case invalidData
}

struct ImageRequest: Request { 
    let url: URL
    var method: HTTPMethod { .get }
    
    func decode(_ data: Data) throws -> UIImage {
        guard let image = UIImage(data: data) else {
            throw Error.invalidData
        }
        return image
    }
}
Copy the code

To make the problem easier to understand, we also show the network request layer of the case:

protocol Networking {
    func fetch<R: Request> (_ request: R) -> AnyPublisher<R.Output.Swift.Error>}class Networker: Networking {    
    func fetch<R: Request> (_ request: R) -> AnyPublisher<R.Output.Swift.Error> {
        var urlRequest = URLRequest(url: request.url)
        urlRequest.httpMethod = request.method.rawValue

        var publisher = URLSession.shared
            .dataTaskPublisher(for: urlRequest)
            .compactMap { $0.data }
            .eraseToAnyPublisher()
                
        return publisher.tryMap(request.decode)
            .eraseToAnyPublisher()
    }
}
Copy the code

Networker implements Networking protocol. In fetch method, a network request is constructed and the data is parsed after the request is successful.

Suppose we have a list of articles, and we want to load the covers of these articles. The request is roughly as follows:

let request: Request = ImageRequest(url: article.image)
let networker: Networking = Networker()
networker.fetch(request)
  .sink(receiveCompletion: { completion in
    switch completion {
    case .failure(let error): print(error)
    default: break
    }
  }, receiveValue: { [weak self] image in
    self?.articles[articleIndex].downloadedImage = image
  })
  .store(in: &cancellables)
Copy the code

Suppose we want to cache loaded images so that we can reuse them while rolling the list without reloading them.

class RequestCache<Value> {
    / / error:
    // 1.Protocol 'Request' can only be used as a generic constraint because it has Self or associated type requirements
    // 2.Type 'Request' does not conform to protocol 'Hashable'
    private var store: [Request: Value] =[:]}Copy the code

The purpose of RequestCache is to use the Request as the key and the downloaded data as the value. When loading the image, the value corresponding to the request is first retrieved from store. If the value does not exist, the image is loaded. However, when we defined RequestCache, a compilation error occurred.

To solve this problem, we need to use type erasure.

Using a generic generic class that implements the generic protocol (see AnySequence in the system library), unwrap the class that implements the generic protocol

So how do you do that?

// AnyRequest can be converted to AnyRequest to get rid of the association type
AnyRequest is a normal structure
struct AnyRequest: Hashable {
    let url: URL
    let method: HTTPMethod
}
Copy the code

We define RequestCache as follows:

class RequestCache<Value> {
    private var store: [AnyRequest: Value] =[:]}Copy the code

So you don’t get an error. So how to convert the Request concrete class into AnyRequest wrapper class next? We have further improved RequestCache:

class RequestCache<Value> {
    private var store: [AnyRequest: Value] = [:]
    
    / / / from the store
    func response<R: Request> (for request: R)-> Value? where R.Output = = Value {
        / / packaging
        let erasedRequest = AnyRequest(url: request.url, method: request.method)
        return store[erasedRequest]
    }
    
    // Save to store
    func saveResponse<R: Request> (_ response: Value.for request: R) where R.Output = = Value {
        / / packaging
        let erasedRequest = AnyRequest(url: request.url, method: request.method)
        store[erasedRequest] = response
    }
}
Copy the code

So the transformation of our Networker is as follows:


class Networker: Networking {
    
    private let imageCache = RequestCache<UIImage> ()func fetch<R: Request> (_ request: R) -> AnyPublisher<R.Output.Swift.Error> {
        var urlRequest = URLRequest(url: request.url)
        urlRequest.httpMethod = request.method.rawValue
        var publisher = URLSession.shared
            .dataTaskPublisher(for: urlRequest)
            .compactMap { $0.data }
            .eraseToAnyPublisher()        
        return publisher.tryMap(request.decode)
            .eraseToAnyPublisher()
    }
    
    func fetchWithCache<R: Request> (_ request: R) -> AnyPublisher<R.Output.Swift.Error> where R.Output = = UIImage {
        if let response = imageCache.response(for: request) {
            return Just<R.Output>(response)
                .setFailureType(to: Swift.Error.self)
                .eraseToAnyPublisher()
        }
        return fetch(request)
            .handleEvents(receiveOutput: {
                self.imageCache.saveResponse($0, for: request)
            }).eraseToAnyPublisher()
    }
}
Copy the code

Request code for the image of the final list:

let request = ImageRequest(url: article.image)
let networker: Networking = Networker()
networker.fetchWithCache(request)
    .sink(receiveCompletion: { error in
        print(error)
    }, receiveValue: { image in
        self.articles[articleIndex].downloadedImage = image
    })
    .store(in: &cancellables)
Copy the code

Conclusion:

When we cannot use the generic protocol directly for variable definition, we can use an implementation of the generic generic class to wrap the implementation of the generic protocol class.