Swift之Moya使用和封装

Moya是什么?

Moya是对请求库Alamofire的抽象封装,相当于OC中YTKNetwork和AFNetworking的关系。

为什么用Moya?

我们用Moya在Github上的一张图来解释。

image.png

TargetType

TargetType这个是使用moya必须要实现的一个协议,先看一下它有哪些定义

/// The protocol used to define the specifications necessary for a `MoyaProvider`.
public protocol TargetType {

    /// The target's base `URL`.
    var baseURL: URL { get }

    /// The path to be appended to `baseURL` to form the full `URL`.
    var path: String { get }

    /// The HTTP method used in the request.
    var method: Moya.Method { get }

    /// Provides stub data for use in testing. Default is `Data()`.
    var sampleData: Data { get }

    /// The type of HTTP task to be performed.
    var task: Task { get }

    /// The type of validation to perform on the request. Default is `.none`.
    var validationType: ValidationType { get }

    /// The headers to be used in the request.
    var headers: [String: String]? { get }
}

看完之后我们来定义一个结构体,遵守TargetType协议

import Foundation
import Moya

// NetworkAPI就是一个遵循TargetType协议的枚举
enum NetworkAPI {
    //测试天气
    case realtimeWeather(cityId:String)
    
}

extension NetworkAPI: TargetType{
    var baseURL: URL {
        return URL(string: "https://go.apipost.cn/")!
    }
    
    var path: String {
        switch self {
        case .realtimeWeather:
            return "?Query=test"
            
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .realtimeWeather:
            return .post
        }
    }
    
    var task: Moya.Task {
        // 公共参数
        var params: [String: Any] = [:]
//        params["token"] = "Gz1qYLXeBW8MZuUfDlr9wsAYuVS1cZFMJY9BbaF842L2gRps747o4w=="
        
        switch self {
        case .realtimeWeather(let cityId):
            params["cityId"] = cityId
        }
        return .requestParameters(parameters: params, encoding: JSONEncoding.default)
    }
    
    var headers: [String : String]? {
        var headers: [String: String] = [:]
        
        switch self {
        case .realtimeWeather:
            headers["Content-type"] = "application/json;charset=utf-8"
        }
        
        return headers
    }
    
}

这里只说一下task,它是一个枚举,这里看一下moya内部定义,我们可以根据需要进行创建

/// Represents an HTTP task.
public enum Task {

    /// A request with no additional data.
    case requestPlain

    /// A requests body set with data.
    case requestData(Data)

    /// A request body set with `Encodable` type
    case requestJSONEncodable(Encodable)

    /// A request body set with `Encodable` type and custom encoder
    case requestCustomJSONEncodable(Encodable, encoder: JSONEncoder)

    /// A requests body set with encoded parameters.
    case requestParameters(parameters: [String: Any], encoding: ParameterEncoding)

    /// A requests body set with data, combined with url parameters.
    case requestCompositeData(bodyData: Data, urlParameters: [String: Any])

    /// A requests body set with encoded parameters combined with url parameters.
    case requestCompositeParameters(bodyParameters: [String: Any], bodyEncoding: ParameterEncoding, urlParameters: [String: Any])

    /// A file upload task.
    case uploadFile(URL)

    /// A "multipart/form-data" upload task.
    case uploadMultipart([MultipartFormData])

    /// A "multipart/form-data" upload task  combined with url parameters.
    case uploadCompositeMultipart([MultipartFormData], urlParameters: [String: Any])

    /// A file download task to a destination.
    case downloadDestination(DownloadDestination)

    /// A file download task to a destination with extra parameters using the given encoding.
    case downloadParameters(parameters: [String: Any], encoding: ParameterEncoding, destination: DownloadDestination)
}

MoyaProvider

我们对网络的请求基本上就是MoyaProvider来调用了,我们先看一下它的定义,具体的参数什么意思我们都可以在这个类中找到对应的参数说明。

    /// Initializes a provider.
    public init(endpointClosure: @escaping EndpointClosure = MoyaProvider.defaultEndpointMapping,
                requestClosure: @escaping RequestClosure = MoyaProvider.defaultRequestMapping,
                stubClosure: @escaping StubClosure = MoyaProvider.neverStub,
                callbackQueue: DispatchQueue? = nil,
                session: Session = MoyaProvider<Target>.defaultAlamofireSession(),
                plugins: [PluginType] = [],
                trackInflights: Bool = false) {

        self.endpointClosure = endpointClosure
        self.requestClosure = requestClosure
        self.stubClosure = stubClosure
        self.session = session
        self.plugins = plugins
        self.trackInflights = trackInflights
        self.callbackQueue = callbackQueue
    }

Moya的使用

接下来我们就可以使用最简单的调用方法:

        let networkProvider = MoyaProvider<NetworkAPI>()
        let target = NetworkAPI.realtimeWeather(cityId: "123")
        networkProvider.request(target) { result in
            
        }

当然这些都只是简单的case帮助理解,当我们使用的时候还需要进行二次封装,以及进行一些配置信息等,下面我就把我封装好的代码贴出来一起研究下
NetworkAPI.swift文件如下:
这里主要做一些接口的配置,也是我们经常使用的

import Foundation
import Moya

// NetworkAPI就是一个遵循TargetType协议的枚举
enum NetworkAPI {
    //测试天气
    case realtimeWeather(cityId:String)
    
}

extension NetworkAPI: TargetType{
    var baseURL: URL {
        return URL(string: "https://go.apipost.cn/")!
    }
    
    var path: String {
        switch self {
        case .realtimeWeather:
            return "?Query=test"
            
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .realtimeWeather:
            return .post
        }
    }
    
    var task: Moya.Task {
        // 公共参数
        var params: [String: Any] = [:]
//        params["token"] = "Gz1qYLXeBW8MZuUfDlr9wsAYuVS1cZFMJY9BbaF842L2gRps747o4w=="
        
        switch self {
        case .realtimeWeather(let cityId):
            params["cityId"] = cityId
        }
        return .requestParameters(parameters: params, encoding: JSONEncoding.default)
    }
    
    var headers: [String : String]? {
        var headers: [String: String] = [:]
        
        switch self {
        case .realtimeWeather:
            headers["Content-type"] = "application/json;charset=utf-8"
        }
        
        return headers
    }
    
}

NetworkPlugin.swift文件如下:
这里是自定义的插件,当然你也可以使用Moya默认的四种

import Foundation
import Moya

/*
 Moya默认有4个插件分别为:
 AccessTokenPlugin 管理AccessToken的插件
 CredentialsPlugin 管理认证的插件
 NetworkActivityPlugin 管理网络状态的插件
 NetworkLoggerPlugin 管理网络log的插件
 */
// 插件,实现pluginType可以实现在网络请求前转菊花,请求完成结束转菊花,或者写日志等功能
struct NetworkPlugin: PluginType {

    /// Called to modify a request before sending.(可进行数据加密等)
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        return request
    }
    
    /// Called immediately before a request is sent over the network (or stubbed).(可进行网络等待,loading等)
    func willSend(_ request: RequestType, target: TargetType) {
        
    }

    /// Called after a response has been received, but before the MoyaProvider has invoked its completion handler.(loading结束等)
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        
    }

    /// Called to modify a result before completion.(可进行数据解密等)
    func process(_ result: Result<Response, MoyaError>, target: TargetType) -> Result<Response, MoyaError> {
        return result
    }

}

NetworkManager.swift文件如下
这里就是我们的网络请求管理中心了,一些配置信息都在这里设置

import Foundation
import Moya


/// 超时时长
private var requestTimeOut: Double = 30

//这个closure存放了一些moya进行网络请求前的一些数据,可以在闭包中设置公共headers
private let endpointClosure = { (target: NetworkAPI) -> Endpoint in
     var endpoint: Endpoint = MoyaProvider.defaultEndpointMapping(for: target)
//     endpoint = endpoint.adding(newHTTPHeaderFields: ["platform": "iOS", "version" : "1.0"])
     return endpoint
 }
//这个闭包是moya提供给我们对网络请求开始前最后一次机会对请求进行修改,比如设置超时时间(默认是60s),禁用cookie等
private let requestClosure = { (endpoint: Endpoint, done: @escaping MoyaProvider<NetworkAPI>.RequestResultClosure) -> Void in
//     guard var request = try? endpoint.urlRequest() else { return }
//     // 设置请求超时时间
//     request.timeoutInterval = 30
//     done(.success(request))
     do {
         var request = try endpoint.urlRequest()
         // 设置请求时长
         request.timeoutInterval = requestTimeOut
         // 打印请求参数
         if let requestData = request.httpBody {
             print("请求的url:\(request.url!)" + "\n" + "\(request.httpMethod ?? "")" + "发送参数" + "\(String(data: request.httpBody!, encoding: String.Encoding.utf8) ?? "")")
         } else {
             print("请求的url:\(request.url!)" + "\(String(describing: request.httpMethod))")
         }

         if let header = request.allHTTPHeaderFields {
             print("请求头内容\(header)")
         }

         done(.success(request))
     } catch {
         done(.failure(MoyaError.underlying(error, nil)))
     }
     
 }

private let networkProvider = MoyaProvider<NetworkAPI>(endpointClosure: endpointClosure, requestClosure: requestClosure, plugins: [NetworkPlugin()], trackInflights: false)



class NetworkManager {
    /// progress的回调
    typealias NetworkProgress = (CGFloat) -> Void
    
    static func request(_ target: NetworkAPI, progress: NetworkProgress? = nil, completion: @escaping Completion) -> Cancellable {
        
        let task = networkProvider.request(target, callbackQueue: DispatchQueue.main) { progressResponse in
            progress?(CGFloat(progressResponse.progress))
        } completion: { result in
            completion(result)
        }
        return task
        
    }
}

以上封装还不够彻底,因为我们并没有对返回的数据进行处理,所以等有时间,我会在NetworkManager做一个对返回数据进行简单处理成json的方法,最后在给到我们的业务层使用,先到这里吧,祝大家国庆节快乐!

10.10日更新##

Response的封装

第一层封装:我们返回一个NetworkResult的结构体

import Foundation

struct NetworkResult<M> {
    
    var data: M?
    var info: String?
    var code: String
    
}

那么NetworkManager文件里面的请求接口就改为以下:

struct NetworkManager<M> {
    /// progress的回调
    typealias NetworkProgress = (CGFloat) -> Void
    /// 请求完成的回调
    typealias NetworkCompletion = (NetworkResult<M>) -> Void
    
    @discardableResult
    func request(_ target: NetworkAPI, progress: NetworkProgress? = nil, completion: @escaping NetworkCompletion) -> Cancellable {
        
        let task = networkProvider.request(target, callbackQueue: DispatchQueue.main) { progressResponse in
            progress?(CGFloat(progressResponse.progress))
        } completion: { result in
            //result转为NetworkResult结构体
            switch result {
            case .success(let response):
                do{
                    guard let json = try response.mapJSON() as? [String: Any] else {
                        let networkResult = NetworkResult<M>(info: "服务器返回的不是JSON数据", code: "-1")
                        completion(networkResult)
                        return
                    }
                    
                    let networkResult = NetworkResult<M>(data: json["data"] as? M, info: json["message"] as? String, code: json["code"] as? String ?? "-1")
                    completion(networkResult)
                    
                } catch {
                    let networkResult = NetworkResult<M>(info: "解析出错:\(error.localizedDescription)", code: "-1")
                    completion(networkResult)
                }
                
            case .failure(let error):
                let networkResult = NetworkResult<M>(info: "请求失败:\(String(describing: error.errorDescription))", code: "-1")
                completion(networkResult)
                
            }
        }
        return task
        
    }
}

第二层封装:结合Swift4.0中的Encodable、Decodable协议,利用泛型知识将NetworkResult中的data转为对应的数据模型返回出去。
NetworkResult.swift文件如下

import Foundation
import Moya

struct NetworkResult<M: Decodable> {
    
    var data: M?
    var info: String?
    var code: String
    
    init(json: [String: Any]) {
        code = json["code"] as? String ?? "-1"
        info = json["message"] as? String
//        data = parseData(jsonObj: json["data"])
        data = parseData(jsonObj: json["data"])
    }
    
    init(errorMsg: String?) {
        code = "-1"
        info = errorMsg
    }
    
    /// 解析数据
    func parseData(jsonObj: Any?) -> M? {
        /// 判断是否为nil
        guard let dataObj = jsonObj else {
            return nil
        }
        
        /// 判断是否为NSNull
        guard !(dataObj as AnyObject).isEqual(NSNull()) else {
            return nil
        }
        
        /// 本身为M类型,直接赋值
        if let dataObj = dataObj as? M {
            return dataObj
        }
        
        /// 转模型
        let jsonData = try? JSONSerialization.data(withJSONObject: dataObj, options: .prettyPrinted)
        guard let data = jsonData else { return nil }
        do{
            return try JSONDecoder().decode(M.self, from: data)
        } catch {
            print(error)
            return nil
        }
    }
    
}

NetworkManager.swift文件请求接口如下:

struct NetworkManager<M: Codable> {
    /// progress的回调
    typealias NetworkProgress = (CGFloat) -> Void
    /// 请求完成的回调
    typealias NetworkCompletion = (NetworkResult<M>) -> Void
    
    @discardableResult
    func request(_ target: NetworkAPI, progress: NetworkProgress? = nil, completion: @escaping NetworkCompletion) -> Cancellable {
        
        let task = networkProvider.request(target, callbackQueue: DispatchQueue.main) { progressResponse in
            progress?(CGFloat(progressResponse.progress))
        } completion: { result in
            //result转为NetworkResult结构体
            switch result {
            case .success(let response):
                do{
                    guard let json = try response.mapJSON() as? [String: Any] else {
                        completion(NetworkResult<M>(errorMsg: "服务器返回的不是JSON数据"))
                        return
                    }
                    
                    completion(NetworkResult<M>(json: json))
                    
                } catch {
                    //解析出错
                    completion(NetworkResult<M>(errorMsg:error.localizedDescription))
                }
                
            case .failure(let error):
                //请求出错
                completion(NetworkResult<M>(errorMsg: error.errorDescription))
                
            }
        }
        return task
        
    }
}

当然,我们也可以给Result再增加一个扩展方法,优化我们的NetworkManager文件,以下就是最终的文件了
NetworkResult

import Foundation
import Moya

struct NetworkResult<M: Decodable> {
    
    var data: M?
    var info: String?
    var code: String
    
    init(json: [String: Any]) {
        code = json["code"] as? String ?? "-1"
        info = json["message"] as? String
//        data = parseData(jsonObj: json["data"])
        data = parseData(jsonObj: json["data"])
    }
    
    init(errorMsg: String?) {
        code = "-1"
        info = errorMsg
    }
    
    /// 解析数据
    func parseData(jsonObj: Any?) -> M? {
        /// 判断是否为nil
        guard let dataObj = jsonObj else {
            return nil
        }
        
        /// 判断是否为NSNull
        guard !(dataObj as AnyObject).isEqual(NSNull()) else {
            return nil
        }
        
        /// 本身为M类型,直接赋值
        if let dataObj = dataObj as? M {
            return dataObj
        }
        
        /// 转模型
        let jsonData = try? JSONSerialization.data(withJSONObject: dataObj, options: .prettyPrinted)
        guard let data = jsonData else { return nil }
        do{
            return try JSONDecoder().decode(M.self, from: data)
        } catch {
            print(error)
            return nil
        }
    }
    
}
//给Result增加一个扩展方法
extension Result where Success: Response, Failure == MoyaError {
    func mapNetworkResult<M>(_ type: M.Type) -> NetworkResult<M> where M: Decodable {
        switch self {
        case .success(let response):
            do {
                guard let json = try response.mapJSON() as? [String: Any] else {
                    /// 不是JSON数据
                    return NetworkResult(errorMsg: "服务器返回的不是JSON数据")
                }
                return NetworkResult(json: json)
            } catch {
                /// 解析出错
                return NetworkResult(errorMsg: error.localizedDescription)
            }
        case .failure(let error):
            /// 请求出错
            return NetworkResult(errorMsg: error.errorDescription)
        }
    }
    
}

NetworkManager

import Foundation
import Moya


/// 超时时长
private var requestTimeOut: Double = 30

//这个closure存放了一些moya进行网络请求前的一些数据,可以在闭包中设置公共headers
private let endpointClosure = { (target: NetworkAPI) -> Endpoint in
     var endpoint: Endpoint = MoyaProvider.defaultEndpointMapping(for: target)
//     endpoint = endpoint.adding(newHTTPHeaderFields: ["platform": "iOS", "version" : "1.0"])
     return endpoint
 }
//这个闭包是moya提供给我们对网络请求开始前最后一次机会对请求进行修改,比如设置超时时间(默认是60s),禁用cookie等
private let requestClosure = { (endpoint: Endpoint, done: @escaping MoyaProvider<NetworkAPI>.RequestResultClosure) -> Void in
//     guard var request = try? endpoint.urlRequest() else { return }
//     // 设置请求超时时间
//     request.timeoutInterval = 30
//     done(.success(request))
     do {
         var request = try endpoint.urlRequest()
         // 设置请求时长
         request.timeoutInterval = requestTimeOut
         // 打印请求参数
         if let requestData = request.httpBody {
             print("请求的url:\(request.url!)" + "\n" + "\(request.httpMethod ?? "")" + "发送参数" + "\(String(data: request.httpBody!, encoding: String.Encoding.utf8) ?? "")")
         } else {
             print("请求的url:\(request.url!)" + "\(String(describing: request.httpMethod))")
         }

         if let header = request.allHTTPHeaderFields {
             print("请求头内容\(header)")
         }

         done(.success(request))
     } catch {
         done(.failure(MoyaError.underlying(error, nil)))
     }
     
 }

private let networkProvider = MoyaProvider<NetworkAPI>(endpointClosure: endpointClosure, requestClosure: requestClosure, plugins: [NetworkPlugin()], trackInflights: false)


struct NetworkManager<M: Codable> {
    /// progress的回调
    typealias NetworkProgress = (CGFloat) -> Void
    /// 请求完成的回调
    typealias NetworkCompletion = (NetworkResult<M>) -> Void
    
    @discardableResult
    func request(_ target: NetworkAPI, progress: NetworkProgress? = nil, completion: @escaping NetworkCompletion) -> Cancellable {
        
        let task = networkProvider.request(target, callbackQueue: DispatchQueue.main) { progressResponse in
            progress?(CGFloat(progressResponse.progress))
        } completion: { result in
            //result转为NetworkResult结构体
            let networkResult = result.mapNetworkResult(M.self)
            completion(networkResult)
        }
        return task
        
    }
}

使用起来就简单多了,比如我们来个用户登录:
先来创建个和服务器商量好的结构体模型

import Foundation

struct LoginModel: Codable  {
    
    var token: String
    var user: User
}

struct User: Codable {
    
    var id: String
    var mobile: String
    var userName: String?
    var url: String?
    
    init(id: String, mobile: String) {
        self.id = id
        self.mobile = mobile
    }
}

然后我们配置NetworkApi文件(这个就不写了),接下来就是调用:

        let target = NetworkAPI.login(mobile: "12345789", verifyCode: "6666")
        NetworkManager<LoginModel>().request(target){ [weak self] (result) in
            guard let self = self else { return }
            if let loginModel = result.data {
                print("登录成功!")
                let userMessage = "token: \(loginModel.token)" + "\n\nid: \(loginModel.user.id)" + "\n\nmobile: \(loginModel.user.mobile)"
               print(userMessage)
            } else {
                //失败
                print(result.info)
            }
            
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容