preface

class Light {
  funcPlug-in electric(a) {}
  funcOpen the(a) {}
  funcIncrease the brightness(a) {}
  funcReduce the brightness(a){}}class LEDLight: Light {}
class DeskLamp: Light {}

funcOpen the(Object: Light){object. Plug () object. Open ()}func main(a){open (object:DeskLampOpen (object:LEDLight()}Copy the code

The open methods in the above object-oriented implementation seem to be limited to the Light class and its derived classes. Protocol comes in handy if we want to describe opening this operation and abstract opening this operation beyond the Light class and its derived classes (cabinets, tables, etc., can be opened, after all).

protocol Openable {
  funcThe preparatory work(a)
  funcOpen the(a)
}

extension Openable {
  funcThe preparatory work(a) {}
  funcOpen the(a){}}class LEDLight: Openable {}
class DeskLamp: Openable {}
class Desk: Openable {}

funcOpen the < T: Openable >(Object: T){object. Preparation () object. Open ()}func main(a){open (object:DeskOpen (object:LEDLight()}Copy the code

Normal network request

  // 1. Prepare request body
  let urlString = "https://www.baidu.com/user"
  guard let url = URL(string: urlString) else {
    return
  }
  let body = prepareBody()
  let headers = ["token": "thisisatesttokenvalue"]
  var request = URLRequest(url: url)
  request.httpBody = body
  request.allHTTPHeaderFields = headers
  request.httpMethod = "GET"

  // 2. Create a network task using URLSeesion
  URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3. Deserialize data
    }
  }.resume()
Copy the code

We can see that making a web request generally involves three steps

  • Prepare the request body (URL, parameters, body, headers…)
  • Create network tasks using frameworks (URLSession, Alamofire, AFN…)
  • Deserialize data (Codable, Protobuf, SwiftyJSON, YYModel…)

We can abstract these three steps and standardize them with three protocols. After the specification is well established, the three protocols can be used in combination with each other.

Abstract network request steps

Parsable

First we define the Parsable protocol to abstract the deserialization process

protocol Parsable {
  // the Result type is declared below, assuming that the function returns' Self '
  static func parse(data: Data) -> Result<Self>}Copy the code

The Parsable protocol defines a static method that can be converted from Data -> Self to User. For example, following the Parsable protocol, the parse(:) method is implemented to convert Data to User

struct User {
  var name: String
}
extension User: Parsable {
  static func parse(data: Data) -> Result<User> {
    / /... Implement Data to User}}Copy the code

Codable

We can use the swift protocol extension to add a default implementation for Codable types

extension Parsable where Self: Decodable {
  static func parse(data: Data) -> Result<Self> {
    do {
      let model = try decoder.decode(self, from: data)
      return .success(model)
    } catch let error {
      return .failure(error)
    }
  }
}
Copy the code

This eliminates the need to implement the parse(:) method for Codable users and makes deserialization a simple matter

extension User: Codable.Parsable {}

URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3. Deserialize data
        let user = User.parse(data: data)
    }
Copy the code

So here’s the question: What if data is an array of models? Should I add another method to the Parsable protocol that returns an array of models? And then implement it again?

public protocol Parsable {
  static func parse(data: Data) -> Result<Self>
// Return an array
  static func parse(data: Data) -> Result"[Self]>
}
Copy the code

This is not impossible, but there is a more swift approach, which Swift calls conditional compliance

// When elements in an Array follow the Parsable and Decodable protocols, the Array also follows the Parsable protocol
extension Array: Parsable where Array.Element: (Parsable & Decodable) {}
Copy the code
URLSession.shared.dataTask(with: request) { (data, response, error) in
    if let data = data {
      // 3. Deserialize data
        let users = [User].parse(data: data)
    }
Copy the code

From this you can see that the SWIFT protocol is very powerful and can reduce a lot of duplicate code with good use. There are many examples of this in the SWIFT standard library.

protobuf

Of course, if you use SwiftProtobuf, you can also provide the default implementation of SwiftProtobuf

extension Parsable where Self: SwiftProtobuf.Message {
  static func parse(data: Data) -> Result<Self> {
    do {
      let model = try self.init(serializedData: data)
      return .success(model)
    } catch let error {
      return .failure(error)
    }
  }
}
Copy the code

The deserialization process is the same as in the previous example by calling the parse(:) method

Request

Now we define the Request protocol to abstract the process of preparing the Request body

protocol Request {
  var url: String { get }
  var method: HTTPMethod { get }
  var parameters: [String: Any]? { get }
  var headers: HTTPHeaders? { get }
  var httpBody: Data? { get }

  /// Request return type (subject to Parsable protocol)
  associatedtype Response: Parsable
}
Copy the code

We define an association type: Response that follows Parsable so that the type that implements the protocol specifies the type returned by the request. Response must follow Parsable because we will use the parse(:) method for deserialization.

Let’s implement a generic request body

struct NormalRequest<T: Parsable> :Request {
  var url: String
  var method: HTTPMethod
  var parameters: [String: Any]?
  var headers: HTTPHeaders?
  var httpBody: Data?

  typealias Response = T

  init(_ responseType: Response.Type,
       urlString: String,
       method: HTTPMethod=.get,
       parameters: [String: Any]? = nil,
       headers: HTTPHeaders? = nil,
       httpBody: Data? = nil) {
    self.url = urlString
    self.method = method
    self.parameters = parameters
    self.headers = headers
    self.httpBody = httpBody
  }
}
Copy the code

Here’s how it works

let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")
Copy the code

If the server has a set of interface https://www.baidu.com/user https://www.baidu.com/manager https://www.baidu.com/driver we can define a BaiduRequest, Add URL or public headers and body to BaiduRequest

// BaiduRequest.swift
private let host = "https://www.baidu.com"

enum BaiduPath: String {
  case user = "/user"
  case manager = "/manager"
  case driver = "/driver"
}

struct BaiduRequest<T: Parsable> :Request {
  var url: String
  var method: HTTPMethod
  var parameters: [String: Any]?
  var headers: HTTPHeaders?
  var httpBody: Data?

  typealias Response = T

  init(_ responseType: Response.Type,
       path: BaiduPath,
       method: HTTPMethod=.get,
       parameters: [String: Any]? = nil,
       headers: HTTPHeaders? = nil,
       httpBody: Data? = nil) {
    self.url = host + path.rawValue
    self.method = method
    self.parameters = parameters
    self.httpBody = httpBody
    self.headers = headers
  }
}
Copy the code

Creation is also simple

let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)
Copy the code

Client

Finally, we define Client protocol to abstract the process of initiating network request

enum Result<T> {
  case success(T)
  case failure(Error)}typealias Handler<T> = (Result<T>) - > ()protocol Client {
// Take a T that follows Parsable, and the last parameter in the closure for the callback is Response in T, which is the Response defined by the Request protocol
  func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>)
}
Copy the code

URLSession

Let’s implement a Client that uses URLSession

struct URLSessionClient: Client {
  static let shared = URLSessionClient(a)private init() {}

  func send<T: Request>(request: T, completionHandler: @escaping (Result<T.Response>)- > ()) {var urlString = request.url
    if let param = request.parameters {
      var i = 0
      param.forEach {
        urlString += i == 0 ? "?\ [$0.key)=\ [$0.value)" : "&\ [$0.key)=\ [$0.value)"
        i += 1}}guard let url = URL(string: urlString) else {
      return
    }
    var req = URLRequest(url: url)
    req.httpMethod = request.method.rawValue
    req.httpBody = request.httpBody
    req.allHTTPHeaderFields = request.headers

    URLSession.shared.dataTask(with: req) { (data, respose, error) in
      if let data = data {
        // Use the parse method to deserialize
        let result = T.Response.parse(data: data)
        switch result {
        case .success(let model):
          completionHandler(.success(model))
        case .failure(let error):
          completionHandler(.failure(error))
        }
      } else{ completionHandler(.failure(error!) )}}}}Copy the code

With all three protocols implemented, the network request at the beginning of the example can be written like this

let request = NormalRequest(User.self, urlString: "https://www.baidu.com/user")
URLSessionClient.shared.send(request) { (result) in
  switch result {
     case .success(let user):
       // Now you have the User instance
       print("user: \(user)")
     case .failure(let error):
       printLog("get user failure: \(error)")}}Copy the code

Alamofire

Of course, you can also implement the Client with Alamofire

struct NetworkClient: Client {
  static let shared = NetworkClient(a)func send<T: Request>(request: T, completionHandler: @escaping Handler<T.Response>) {
    let method = Alamofire.HTTPMethod(rawValue: request.method.rawValue) ?? .get
    var dataRequest: Alamofire.DataRequest

    if let body = request.httpBody {
      var urlString = request.url
      if let param = request.parameters {
        var i = 0
        param.forEach {
          urlString += i == 0 ? "?\ [$0.key)=\ [$0.value)" : "&\ [$0.key)=\ [$0.value)"
          i += 1}}guard let url = URL(string: urlString) else {
        print("URL format error")
        return
      }
      var urlRequest = URLRequest(url: url) urlRequest.httpMethod = method.rawValue urlRequest.httpBody = body urlRequest.allHTTPHeaderFields = request.headers  dataRequest =Alamofire.request(urlRequest)
    } else {
      dataRequest = Alamofire.request(request.url,
                                      method: method,
                                      parameters: request.parameters,
                                      headers: request.headers)
    }

    dataRequest.responseData { (response) in
      switch response.result {
      case .success(let data):
        // deserialize using the parse(:) method
        let parseResult = T.Response.parse(data: data)
        switch parseResult {
        case .success(let model):
          completionHandler(.success(model))
        case .failure(let error):
          completionHandler(.failure(error))
        }
      case .failure(let error):
        completionHandler(.failure(error))
      }
    }
  }

  private init() {}}Copy the code

We try to initiate a set of network requests

let userRequest = BaiduRequest(User.self, path: .user)
let managerRequest = BaiduRequest(Manager.self, path: .manager, method: .post)

NetworkClient.shared.send(managerRequest) { result in
    switch result {
     case .success(let manager):
       // Now you have the Manager instance
       print("manager: \(manager)")
     case .failure(let error):
       printLog("get manager failure: \(error)")}}Copy the code

conclusion

We abstract the network request process with three protocols, making the network request very flexible, you can combine various implementations, different request body with different serialization method or different network framework. URLSession + Codable, Alamofire + Protobuf, and more are available for daily development.

reference

This article by Meow God was the beginning of my study of protocol-oriented and gave me a great inspiration: protocol-oriented programming and Cocoa encounter