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