swift version: 4.1
Xcode version 9.3 (9E145)
Code Github address: MoyaDemo
I. Introduction
I recently entered a new company to carry out a new project. I found that the network layer of the company's project is very OC. The most unbearable thing is that the data parsing is outside the network layer, and each data model needs to write parsing code separately. While the project is just starting, I propose that I write a network layer gadget to replace the previous network layer, and by the way, the loading chrysanthemum and the cache are also encapsulated in it.
2. Introduction to Moya tools and Codable protocol
Here is just to show Moya
the basic usage of and Codable协议
the basic knowledge of . If you are interested in these two, readers can search and study by themselves.
2.1 Moya Tools
Moya
Use because the author thinks it is very convenient, if the reader does not want to use it, it will Moya
not affect your reading of the content of this article.
Alamofire
I won't introduce it here. If you haven't touched it, you can regard it as a Swift
version AFNetworking
. Moya
It is Alamofire
a tool library that repackages . If only used Alamofire
, your network request might look like this:
let url = URL(string: "your url")!
Alamofire.request(url).response { (response) in
// handle response
}
Of course, readers will also perform secondary encapsulation based on it, not just the above code.
If you use Moya
it, the first thing you do is not to directly request, but to establish a file definition interface according to the project module. For example, I like to name the module according to the function 模块名 + API
, 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
}
}
// 这里只是带参数的网络请求
var task: Task {
var parameters: [String: Any] = [:]
switch self {
case let .yourAPI1:
parameters = [:]
case let .yourAPI2(parameter):
parameters = ["字段":parameter]
}
return .requestParameters(parameters: parameters,
encoding: URLEncoding.default)
}
// 单元测试使用
var sampleData : Data {
return Data()
}
}
After defining the above file, you can use the following methods to make network requests:
MoyaProvider<YourModuleAPI>().request(YourModuleAPI.yourAPI1) { (result) in
// handle result
}
2.2 Codable protocol
Codable协议
It is Swift4
only updated and used to parse and encode data. It consists of an encoding protocol and a decoding protocol.
public typealias Codable = Decodable & Encodable
Before the Swift
update Codable协议
, the author has been using SwiftyJSON to parse the data returned by the network request. Codable协议
After using it recently , I found it to be quite easy to use, so I used it directly.
However, there are Codable协议
still some pitfalls, such as described in this article:
When JSONDecoder meets the real world, things get ugly…
The following Person
model class stores a simple personal information, here only uses the decoding, so only complies with Decodable协议
:
struct Person: Decodable {
var name: String
var age: Int
}
String
And Int
is the default codec type of the system, so we don't need to write other code, the compiler will implement it 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
}
}
Just pass the Person
type to the JSONDecoder
object, and it can directly convert the JSON
data into a Person
data model object. In actual use, due to various strict limitations of parsing rules, it is far less simple than it looks above.
3. Analysis and Solutions
3.1.1 Repeated parsing data to model
For example, there are two interfaces here, one is to request the product list, and the other is to request the homepage of the mall. I wrote this before:
enum MallAPI {
case getMallHome
case getGoodsList
}
extension MallAPI: TargetType {
// 略
}
let mallProvider = MoyaProvider<MallAPI>()
mallProvider.request(MallAPI.getGoodsList) { (response) in
// 将 response 解析成 Goods 模型数组用 success 闭包传出去
}
mallProvider.request(MallAPI.getMallHome) { (response) in
// 将 response 解析成 Home 模型用 success 闭包传出去
}
The above is a simplified practical scenario. Each network request will be written once to parse the returned data into a data model or data model array. Even encapsulating the function of data parsing into a singleton tool class is only slightly better.
What the author wants is that after specifying the data model type, the network layer directly returns the parsed data model for us to use.
3.1.2 Use generics to solve
Generics are used to solve the above problem, use generics to create a network tool class, and give generic constraints: follow the Codable
protocol .
struct NetworkManager<T> where T: Codable {
}
In this way, when we use it, we can specify the type of data model that needs to be parsed.
NetworkManager<Home>().reqest...
NetworkManager<Goods>().reqest...
The attentive reader will notice that this is the same way as the Moya
Initializer MoyaProvider
class is used.
3.2.1 After using Moya, how to encapsulate the load controller and cache to the network layer
Due to the use Moya
of re-encapsulation, the cost of each encapsulation of the code is the sacrifice of degrees of freedom. How to Moya
fit ?
A very simple way is to add whether to display the controller and whether to cache boolean parameters in the request method. Looking at my request method parameters already 5 or 6, this solution was immediately ruled out. Looking at Moya
the TargetType
protocol gave me inspiration.
3.2.2 Use the protocol to solve
Since it MallAPI
can comply with TargetType
the configuration network request information, of course, we can also comply with our own protocol to perform some configuration.
Customize Moya
a Supplemental Protocol
protocol MoyaAddable {
var cacheKey: String? { get }
var isShowHud: Bool { get }
}
In this way MallAPI
, two protocols need to be followed.
extension MallAPI: TargetType, MoyaAddable {
// 略
}
Fourth, part of the code display and analysis
The complete code, readers can Github
go to download.
4.1 Encapsulated network request
By giving the data type to be returned, the returned response
can directly call the dataList
property to obtain the parsed Goods
data model array. In the error closure, the error information can also be error.message
obtained , and then choose whether to use a pop-up box to prompt the user according to business requirements.
NetworkManager<Goods>().requestListModel(MallAPI.getOrderList,
completion: { (response) in
let list = response?.dataList
let page = response?.page
}) { (error) in
if let msg = error.message else {
print(msg)
}
}
4.2 Encapsulation of returned data
The data structure returned by the author's server is roughly as follows:
{
"code": 0,
"msg": "成功",
"data": {
"hasMore": false,
"list": []
}
}
For the consideration of the current business and parsing data, the author encapsulates the returned data types into two categories, and also puts the parsing operation in it.
The following request methods are also divided into two, which is not necessary, readers can choose according to their own business and preferences.
- Request the data returned by the list interface
- Request the data returned by the common interface
class BaseResponse {
var code: Int { ... } // 解析
var message: String? { ... } // 解析
var jsonData: Any? { ... } // 解析
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]? { ... } // 解析
var page: PageModel? { ... } // 解析
}
class ModelResponse<T>: BaseResponse where T: Codable {
var data: T? { ... } // 解析
}
In this way, we can directly return the corresponding encapsulated class object to obtain the parsed data.
4.3 Wrong encapsulation
In the process of network request, there must be various errors, and the error mechanism of the Swift
language .
// 网络错误处理枚举
public enum NetworkError: 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
}
}
}
The extension here is important, it helps us get the wrong message
and code
.
4.4 Request network method
method of 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?
{}
The R
generic type is used to obtain Moya
the defined interface, specifying that the TargetType
and MoyaAddable
protocol must be followed at the same time, and the rest are normal operations. Like the encapsulated return data, there are also common interfaces and list interfaces.
@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)
}
I considered the pit points of the current project and Codable
protocol , and wrote it a bit rigidly, in case there is a list and other data, it is not applicable. However, a method similar to this can be added at that time to transmit the data for processing.
// Demo里没有这个方法
func requestCustom<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
completion: (Response) -> ()) -> Cancellable?
{
// 略
}
4.5 Cache and Load Controllers
After thinking of adding a MoyaAddable
protocol , there is no difficulty in other things, just type
make corresponding operations directly according to the configuration in the interface definition file.
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
}
}
This adds two functions in the getGoodsList
interface request
- After the request returns data, it will
Key
be cache - Automatically show and hide loading controllers during network requests.
If the reader's loading controller has a different style, you can also add a property for the loading controller's style. Even whether the cache is synchronous or asynchronous can be MoyaAddable
added .
// 缓存
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
}
if modelComletion != nil, let temp = model.0 {
// 缓存
}
if modelListComletion != nil, let temp = model.1 {
// 缓存
}
}
The loading controller is shown and hidden using the Moya
built -in plugin tool.
// 创建moya请求类
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
}
4.6 Avoid Duplicate Requests
Define an array to hold information about network requests, and a parallel queue using barrier
functions to ensure thread-safe addition and removal of array elements.
// 用来处理只请求一次的栅栏队列
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// 用来处理只请求一次的数组,保存请求的信息 唯一
private var fetchRequestKeys = [String]()
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:
// 不会调用
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:
// 不会调用
()
}
}
There is a small problem with this implementation method. If multiple interfaces use the same interface and the parameters are the same, they will only be requested once. However, this situation is still very rare, and it will not be dealt with if it is not encountered for the time being.
5. Postscript
The currently encapsulated network layer code is a bit of a strong business type. After all, my original intention was to rewrite a network layer for my company's project, so it may not be suitable for some situations. However, the method of using generics and protocols here is general, and readers can use the same method to implement the network layer that matches their own projects. If readers have better suggestions, they also hope to comment and discuss together.
Reprint comments and leave the reprint address to reprint.