Swift version: 4.1
Xcode Version 9.3 (9E145)
Repackaging based on Alamofire and Moya
Code Github address: MoyaDemo
One, foreword
Recently, I entered a new company to carry out a new project. I found that the network layer of the company’s project is very difficult. The most unbearable thing is that data parsing is outside the network layer, and each data model needs to write parsing code separately. While the project was just getting started, I proposed that I write a network-layer gadget to replace the old network-layer, with loading chrysanthemums and caching wrapped in it.
Moya tools and Codable Protocols
This is a guide to Moya and cidr protocols for which you may be interested. Check them out on your own for more information.
2.1 Moya tools
I used Moya because I found it convenient, and if the reader doesn’t want to use Moya, it won’t stop you from reading this article.
Alamofire won’t be covered here, but if you haven’t already, you can think of it as AFNetworking for Swift. Moya is a repackaged library of Alamofire tools. If you use Alamofire alone, your web request might look like this:
let url = URL(string: "your url")!
Alamofire.request(url).response { (response) in
// handle response
}
Copy the code
Of course, the reader will also be based on the secondary encapsulation, not just the above code as simple.
With Moya, the first thing you do is not ask directly, but create a file definition interface based on the project module. For example, I like to name the module + API according to its function, and then define the interface we need to use in it, for example:
import Foundation
import Moya
enum YourModuleAPI {
case yourAPI1
case yourAPI2(parameter: String)}extension YourModuleAPI: TargetType {
var baseURL : URL {
return URL(string: "your base url")!
}
var headers : [String : String]? {
return "your header"
}
var path: String {
switch self {
case .yourAPI1:
return "yourAPI1 path"
case .yourAPI2:
return "yourAPI2 path"}}var method: Moya.Method {
switch self {
case .yourAPI1:
return .post
default:
return .get}}// This is just a network request with parameters
var task: Task {
var parameters: [String: Any] = [:]
switch self {
case let .yourAPI1:
parameters = [:]
case let .yourAPI2(parameter):
parameters = ["Field":parameter]
}
return .requestParameters(parameters: parameters,
encoding: URLEncoding.default)}// unit test use
var sampleData : Data {
return Data()}}Copy the code
Once the above files are defined, you can make network requests as follows:
MoyaProvider<YourModuleAPI>().request(YourModuleAPI.yourAPI1) { (result) in
// handle result
}
Copy the code
2.2 Codable agreement
The Codable protocol is a Swift4 update for interpreting and encoding data. It is made up of an encoding protocol and a decoding protocol.
public typealias Codable = Decodable & Encodable
Copy the code
Before Swift updated the Codable protocol, I used SwiftyJSON to parse data returned from network requests. After using Codable, I started using it directly.
There are some holes in Codable protocols, though, as this article describes:
When JSONDecoder meets the real world, things Get ugly…
The following Person model class stores a simple personal information, which is only decoded, so only the Decodable protocol is obeyed:
struct Person: Decodable {
var name: String
var age: Int
}
Copy the code
String and Int are the system’s default codec types, so we don’t need to write any more code, and the compiler will implement them for us by default.
let jsonString = """{"name":"swordjoy","age": 99}"""
if let data = jsonString.data(using: .utf8) {
let decoder = JSONDecoder()
if let person = try? decoder.decode(Person.self, from: data) {
print(person.age) // 99
print(person.name) // swordjoy
}
}
Copy the code
All you need to do is pass the Person type to the JSONDecoder object, which directly converts JSON data into a Person data model object. In practice, due to the strict restrictions of parsing rules, it is not as easy as it looks above.
Analysis and solutions
3.1.1 Repeatedly parses data into the model
For example, there are two interfaces, one is to request the list of goods, one is to request the mall home page. I used to write like this:
enum MallAPI {
case getMallHome
case getGoodsList
}
extension MallAPI: TargetType {
/ / a little
}
Copy the code
let mallProvider = MoyaProvider<MallAPI>()
mallProvider.request(MallAPI.getGoodsList) { (response) in
// Parse response into Goods model array with success closure
}
mallProvider.request(MallAPI.getMallHome) { (response) in
// Parse the response into a Home model and pass out the success closure
}
Copy the code
The above is a simplified practical scenario where each network request is written separately to parse the returned data into a data model or an array of data models. Even encapsulating the functionality of data parsing into a singleton utility class is only marginally better.
What the author wants is that after specifying the data model type, the network layer directly returns the parsed data model for our use.
3.1.2 Use generics to solve
Generics solve this problem by using generics to create a web utility class that is subject to the Codable protocol.
struct NetworkManager<T> where T: Codable {}Copy the code
This allows us to specify the data model type to be parsed when we use it.
NetworkManager<Home>().reqest...
NetworkManager<Goods>().reqest...
Copy the code
Careful readers will notice that this is used in the same way that Moya initializes the MoyaProvider class.
3.2.1 How to encapsulate the loading controller and cache at the network layer after using Moya
Since Moya is used for reencapsulation, the cost of each encapsulation of code is a sacrifice of degrees of freedom. How does loading controller & caching fit with Moya?
A simple way to do this is to add whether to display the controller and whether to cache Boolean values to the request method. Seeing that my request method has 5 or 6 parameters, this option is immediately ruled out. Looking at Moya’s TargetType protocol gave me inspiration.
3.2.2 Use the protocol to solve the problem
Since MallAPI can comply with TargetType to configure network request information, it can also comply with our own protocol to do some configuration.
Customize a supplementary protocol for Moya
protocol MoyaAddable {
var cacheKey: String? { get }
var isShowHud: Bool { get}}Copy the code
MallAPI then has to comply with two protocols
extension MallAPI: TargetType.MoyaAddable {
/ / a little
}
Copy the code
Part of the code display and analysis
The complete code can be downloaded from Github.
4.1 Encapsulated Network Request
Given the data type to be returned, the returned Response can directly call the dataList property to obtain the parsed Goods data model array. Error closures can also be used to retrieve error messages directly from error. Message, and then choose whether to use pop-up boxes to prompt users based on business requirements.
NetworkManager<Goods>().requestListModel(MallAPI.getOrderList,
completion: { (response) in
letlist = response? .dataListletpage = response? .page }) { (error)in
if let msg = error.message else {
print(msg)
}
}
Copy the code
4.2 Encapsulation of returned data
The data structure returned by the author’s server is roughly as follows:
{
"code": 0."msg": "Success"."data": {
"hasMore": false."list": []}}Copy the code
For current business and data parsing purposes, I have encapsulated the returned data types into two categories, and also included the parsed operations.
The latter request method is also divided into two, which are not necessary and can be chosen according to the reader’s business and preference.
- Data returned by the request list interface
- Request data returned by the normal interface
class BaseResponse {
var code: Int{... }/ / parsing
var message: String? {... }/ / parsing
var jsonData: Any? {... }/ / parsing
let json: [String : Any]
init? (data:Any) {
guard let temp = data as? [String : Any] else {
return nil
}
self.json = temp
}
func json2Data(_ object: Any) -> Data? {
return try? JSONSerialization.data(
withJSONObject: object,
options: [])
}
}
class ListResponse<T> :BaseResponse where T: Codable {
var dataList: [T]? {... }/ / parsing
var page: PageModel? {... }/ / parsing
}
class ModelResponse<T> :BaseResponse where T: Codable {
var data: T? {... }/ / parsing
}
Copy the code
So we can just return the corresponding wrapper object and get the parsed data.
4.3 Incorrect encapsulation
In the process of network request, there must be all kinds of errors, here using the Swift language error mechanism.
// Network error handling enumeration
public enum NetworkError: Error {
/ / a little...
// The server returned an error
case serverResponse(message: String? , code:Int)}extension NetworkError {
var message: String? {
switch self {
case let .serverResponse(msg, _) :return msg
default: return nil}}var code: Int {
switch self {
case let .serverResponse(_, code): return code
default: return -1}}}Copy the code
The extension here is important to help us get the wrong message and code when handling errors.
4.4 Request network method
The method of the final request
private func request<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
progressBlock: ((Double)- > ())? =nil,
modelCompletion: ((ModelResponse<T>?) -> ())? = nil,
modelListCompletion: ((ListResponse<T>? - > ())? =nil,
error: @escaping (NetworkError) -> ()) ->Cancellable?
{}
Copy the code
The R generic here is used to retrieve the interface defined by Moya, specifying that both TargetType and MoyaAddable protocols must be followed, and the rest is routine. As with encapsulated return data, there is a plain interface and a list interface.
@discardableResult
func requestModel<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
progressBlock: ((Double)- > ())? =nil,
completion: @escaping ((ModelResponse<T>? -> ()), error: @escaping (NetworkError) -> ()) ->Cancellable?
{
return request(type,
test: test,
progressBlock: progressBlock,
modelCompletion: completion,
error: error)
}
@discardableResult
func requestListModel<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
completion: @escaping ((ListResponse<T>?) -> ()),
error: @escaping (NetworkError) -> ()) ->Cancellable?
{
return request(type,
test: test,
modelListCompletion: completion,
error: error)
}
Copy the code
I made this list a bit too rigid for one that is both a list and some other data based on what I already know about projects and Codable agreements. In time, however, you can add a method like this to send the data out for processing.
// There is no such method in Demo
func requestCustom<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
completion: (Response)- > () - >Cancellable?
{
/ / a little
}
Copy the code
4.5 Caching and Loading controllers
After adding the MoyaAddable protocol, there is nothing more difficult to do. You can simply get the configuration from the interface definition file according to type.
var cacheKey: String? {
switch self {
case .getGoodsList:
return "cache goods key"
default:
return nil}}var isShowHud: Bool {
switch self {
case .getGoodsList:
return true
default:
return false}}Copy the code
This adds two functions in the getGoodsList interface request
- The request returns data through the given cache
Key
caching - Automatically show and hide load controller during network request.
If the reader’s load controller has a different style, you can also add a load controller style property. Even whether the caching is synchronous or asynchronous can be added through this MoyaAddable.
/ / cache
private func cacheData<R: TargetType & MoyaAddable>(
_ type: R,
modelCompletion: ((Response<T>?)- > ())? =nil,
modelListCompletion: ( (ListResponse<T>? - > ())? =nil,
model: (Response<T>? .ListResponse<T>?))
{
guard let cacheKey = type.cacheKey else {
return
}
ifmodelComletion ! =nil.let temp = model.0 {
/ / cache
}
ifmodelListComletion ! =nil.let temp = model.1 {
/ / cache}}Copy the code
The loading controller is shown and hidden using Moya’s own plug-in tool.
// Create moya request class
private func createProvider<T: TargetType & MoyaAddable>( type: T, test: Bool)
-> MoyaProvider<T>
{
let activityPlugin = NetworkActivityPlugin { (state, targetType) in
switch state {
case .began:
DispatchQueue.main.async {
if type.isShowHud {
SVProgressHUD.showLoading()
}
self.startStatusNetworkActivity()
}
case .ended:
DispatchQueue.main.async {
if type.isShowHud {
SVProgressHUD.dismiss()
}
self.stopStatusNetworkActivity()
}
}
}
let provider = MoyaProvider<T>(
plugins: [activityPlugin,
NetworkLoggerPlugin(verbose: false)])
return provider
}
Copy the code
4.6 Avoiding Repeated Requests
An array is defined to hold information about network requests, and a parallel queue uses the Barrier function to keep array elements added and removed thread safe.
// It is used to process the fenced queue with only one request
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// An array is used to process only one request, holding unique information about the request
private var fetchRequestKeys = [String] ()Copy the code
private func isSameRequest<R: TargetType & MoyaAddable>(_ type: R) -> Bool {
switch type.task {
case let .requestParameters(parameters, _) :let key = type.path + parameters.description
var result: Bool!
barrierQueue.sync(flags: .barrier) {
result = fetchRequestKeys.contains(key)
if! result { fetchRequestKeys.append(key) } }return result
default:
// will not be called
return false}}private func cleanRequest<R: TargetType & MoyaAddable>(_ type: R) {
switch type.task {
case let .requestParameters(parameters, _) :let key = type.path + parameters.description
barrierQueue.sync(flags: .barrier) {
fetchRequestKeys.remove(key)
}
default:
// will not be called()}}Copy the code
There is a small problem with this implementation, multiple interfaces using the same interface, and the same parameters, will only be requested once, but this is still rare, not encountered for the moment there is no processing.
Five, the afterword.
The network layer code currently packaged is a bit of a business type, after all my original intention is to rewrite the network layer for my own company project, so it may not be suitable for some situations. However, the use of generics and protocols is common here, and readers can implement a network layer that matches their project in the same way. If readers have better suggestions, we hope to comment on them together.
Reprint comments leave reprint address can be reprinted.