Moya15.0的使用

Alamofire可以理解为Swift版本的AFNetworking,是同一个团队写的开源库,Moya是对Alamofire的再次封装!如果从难易程度上说,Alamofire可能会更简单一些!

网上已经有很多的关于Moya使用的文章,但是大多都是前几年的。Moya14和Moya15,几乎没有相关的文章了。

多谢评论区小伙伴提醒,本文中 JSON 属于SwiftyJSON中的类型。SwiftyJSON 这个库非常值得推荐,是目前比较好用的解析库,我平时会将SwiftyJSON和HandyJSON配合使用。

本文适用版本:
--> Moya15.0.0 (对应 Alamofire5.4.4)

--> Moya14.0.0 (对应 Alamofire5.4.3)

系统版本:
--> iOS 10.0以上(Swift5.0以上)

本篇文章就是一个Moya的封装使用,不具体讲原理
至于Moya的好处,我从网上copy下来一张图,仅供参考:


网络copy图

将Moya pod到工程中后,我们需要创建三个文件:


核心文件
API.swift     我们的API接口相关的东西,都写在里面。
NetworkNanager.swift    Moya网络框架封装相关的都写在这里面
MoyaConfig.swift   一些公用的网络配置参数,统一写在这里面

一、MoyaConfig.swift:

在MoyaConfig文件里面配置需要的公用的参数:
比如:

/// 定义基础域名
let Moya_baseURL = "https://zhou.xuanhe.com"
/// 定义返回的JSON数据字段
let RESULT_CODE = "flag"      //状态码
let RESULT_MESSAGE = "message"  //错误消息提示
二、API.swift
2.1 首先创建接口API的枚举
enum API {
    case login(parameters:[String:Any])    //参数可以是字典
    case testApi       //无参数接口
    case register(email:String,password:String)   //参数可以是字符串
    case uploadHeadImage(parameters:[String:Any],imageData:Data)
}
2.2 遵守TargetType协议

首先在API.swift文件内,导入头文件import Moya
通过遵守TargetType协议,实现协议内的相关api

extension API:TargetType{

}

遵守TargetType协议后(如上图),XCode会提示相关信息的,让你实现TargetType协议里面的属性或者函数

extension API:TargetType{
    //baseURL 也可用枚举区分不同的baseURL,不过一般只需要一个baseURL
    var baseURL: URL {
        return URL.init(string: Moya_baseURL)!
    }
    
    //不同接口的子路径
    var path: String {
        switch self {
        case .login:
            return "user/login"
        case .testApi:
            return "1111"
        case .updateApi:
            return "update/info"
        case .register(let email, _):
            return  "/user/register/" + email 
        case .uploadHeadImage:
            return "/image/upload"
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .login:
            return .post
        default:
            return .get
        }
    }
    
    /// 这个是做单元测试模拟的数据,必须要实现,只在单元测试文件中有作用
    var sampleData: Data {
        return "".data(using: String.Encoding.utf8)!
    }
    
    var task: Task {
        switch self {
        case let .login(parameters):
            return .requestParameters(parameters: parameters, encoding: URLEncoding.default)
        case .testApi:
            return .requestPlain
            
        case let .updateApi(parameters):
            return .requestParameters(parameters: parameters, encoding: URLEncoding.default)
            
        case let .register(email, password):
            return .requestParameters(parameters: ["email": email, "password": password], encoding: URLEncoding.default)
            
        case .uploadHeadImage(let parameters, let imageData):
            let formData = MultipartFormData(provider: .data(imageData), name: "file", fileName: "zhou.png", mimeType: "image/png")
            return .uploadCompositeMultipart([formData], urlParameters: parameters)
        }
    }
    
    // 同task,具体选择看后台 有application/x-www-form-urlencoded 、application/json
    var headers: [String : String]? {
        switch self {
        case .updateApi(_):
            return ["Content-type" : "multipart/form-data"]
        default:
           return ["Content-Type":"application/x-www-form-urlencoded"]
        }
    }
}

三、NetworkManager.swift

3.1 直接上代码了:

import Foundation
import Moya
import SwiftyJSON
import Alamofire

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

// 回调 包括:网络请求的模型(code,message,data等,具体根据业务来定)
typealias RequestResultClosure = ((ResponseModel) -> Void)

///先添加一个闭包用于成功时后台返回数据的回调
typealias successCallback = ((String) -> (Void))
typealias failureCallback = ((String) -> (Void))

/// dataKey一般是 "data"  这里用的知乎daily 的接口 为stories
let dataKey = "stories"
let messageKey = "message"
let codeKey = "code"
let successCode: Int = -999


/// endpointClosure
private let myEndpointClosure = { (target : TargetType) -> Endpoint in
    ///这里的endpointClosure和网上其他实现有些不太一样。
    ///主要是为了解决URL带有?无法请求正确的链接地址的bug
    let url = target.baseURL.absoluteString + target.path
    var endpoint = Endpoint(
        url: url,
        sampleResponseClosure: {
            .networkResponse(200, target.sampleData)
        },
        method: target.method,
        task: target.task,
        httpHeaderFields: target.headers)
    
    requestTimeOut = 30 // 每次请求都会调用endpointClosure 到这里设置超时时长 也可单独每个接口设置
    
    // 针对于某个具体的业务模块来做接口配置
    if let apiTarget = target as? API {
        switch apiTarget {
        case .testApi:
            return endpoint
        case .register:
            requestTimeOut = 5
            return endpoint
            
        default:
            return endpoint
        }
    }
    
    return endpoint.adding(newHTTPHeaderFields: ["Accept-Language":"zh-Hans-CN",
                                                 "accessToken" : "26A125",
                                                 "deviceId" : "9E726A1256C2F178FE72",
                                                 "loginType" : "1",
                                                 "mobileModel" : "iPhone 7",
                                                 "os" : "14.2",
                                                 "platform" : "IOS",
                                                 "platformCode" : "xuanhe",
                                                 "timesRequest" : "1131214.393186",
                                                 "version" : "1.1.0",
                                                 "versionCode" : "10",
                                         ])
}


private let requestClosure = { (endpoint: Endpoint, done: MoyaProvider.RequestResultClosure) in
    do {
        var request = try endpoint.urlRequest()
        request.timeoutInterval = requestTimeOut
        //打印请求参数
        if let requestData = request.httpBody {
            print("\(request.url!)"+"\n"+"\(request.httpMethod ?? "")"+"发送参数"+"\n"+"\(String(data: request.httpBody!, encoding: String.Encoding.utf8) ?? "")")
        }else{
            print("\(request.url!)"+"\(String(describing: request.httpMethod))")
        }
        
        if let header = request.allHTTPHeaderFields {
            print("请求头内容\(header)")
        }
        done(.success(request))
        
    } catch  {
        done(.failure(MoyaError.underlying(error, nil)))
    }
}


// 用Moya默认的Manager还是Alamofire的Manager看实际需求。HTTPS就要手动实现Manager了
// private func defaultAlamofireManager() -> Manager {
//
//    let configuration = URLSessionConfiguration.default
//
////     configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders
//    configuration.httpAdditionalHeaders = Alamofire.SessionManager.defaultHTTPHeaders
//
//    let path: String = Bundle.main.path(forResource: "0302xuanhe", ofType: "cer") ?? ""
//    let certificationData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData
//    let certificate = SecCertificateCreateWithData(nil, certificationData!)
//    let certificates: [SecCertificate] = [certificate!]
//
//    let policies: [String: ServerTrustPolicy] = [Moya_baseURL: ServerTrustPolicy.pinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true)]
//    let manager = Manager(configuration: configuration, serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))
//
//    return manager
// }
//把defaultAlamofireManager当参数传进去就行了


//MARK: 设置ssl  处理https证书验证
let session : Session = {
   //证书数据
   func certificate() -> SecCertificate? {
       let filePath = Bundle.main.path(forResource: "0302xuanhe", ofType: "cer")
       if filePath == nil {
           return nil
       }
       let data = try! Data(contentsOf: URL(fileURLWithPath: filePath ?? ""))
       let certificate = SecCertificateCreateWithData(nil, data as CFData)!
       return certificate
   }

   guard let certificate = certificate() else {
       return Session()
   }

   let trusPolicy = PinnedCertificatesTrustEvaluator(certificates: [certificate], acceptSelfSignedCertificates: true, performDefaultValidation: true, validateHost: false)
    let trustManager = ServerTrustManager(allHostsMustBeEvaluated: false, evaluators: [Moya_baseURL : trusPolicy])
   return Session(serverTrustManager: trustManager)
}()
//把session当参数传进去就行了


/// NetworkActivityPlugin插件用来监听网络请求
private let networkPlugin = NetworkActivityPlugin.init { changeType, TargetType in
    print("networkPlugin \(changeType)")
    
    //TargetType 是当前请求的基本信息
    switch (changeType){
    case .began :
        print("\n")
        print(TargetType)
        print("\n")       
        print("开始请求网络")
    case .ended :
        print("网络请求结束")
    }
}

/// 网络请求发送的核心初始化方法,创建网络请求对象
fileprivate let Provider = MoyaProvider<MultiTarget>(endpointClosure: myEndpointClosure, requestClosure: requestClosure, plugins: [networkPlugin], trackInflights: false)


class ResponseModel {
    var isSuccess : Bool = false
    var code: Int = -999
    var message: String = ""
    // 这里的data用String类型 保存response.data
    var data: String = ""
    /// 分页的游标 根据具体的业务选择是否添加这个属性
    var cursor: String = ""
}



/// 错误处理
/// - Parameters:
///   - code: code码
///   - message: 错误消息
///   - needShowFailAlert: 是否显示网络请求失败的弹框
///   - failure: 网络请求失败的回调
private func errorHandler(code: Int, message: String, failure: RequestResultClosure?) {
    print("发生错误:\(code)--\(message)")
    let model = ResponseModel()
    model.code = code
    model.message = message
    model.isSuccess = false
    failure?(model)
}

/// 预判断后台返回的数据有效性 如通过Code码来确定数据完整性等  根据具体的业务情况来判断  有需要自己可以打开注释
/// - Parameters:
///   - response: 后台返回的数据
///   - showFailAlet: 是否显示失败的弹框
///   - failure: 失败的回调
/// - Returns: 数据是否有效
private func validateRepsonse(response: [String: JSON]?, failure: RequestResultClosure?) -> Bool {
    /**
     var errorMessage: String = ""
     if response != nil {
     if !response!.keys.contains(codeKey) {
     errorMessage = "返回值不匹配:缺少状态码"
     } else if response![codeKey]!.int == 500 {
     errorMessage = "服务器开小差了"
     }
     } else {
     errorMessage = "服务器数据开小差了"
     }
     
     if errorMessage.count > 0 {
     var code: Int = 999
     if let codeNum = response?[codeKey]?.int {
     code = codeNum
     }
     if let msg = response?[messageKey]?.stringValue {
     errorMessage = msg
     }
     errorHandler(code: code, message: errorMessage, showFailAlet: showFailAlet, failure: failure)
     return false
     }
     */
    
    return true
}


/// 请求方法
/// - Parameters:
///   - target: TargetType
///   - successCallback: 成功回调
///   - failureCallback: 失败回调
/// - Returns: 请求操作
@discardableResult
func NetWorkRequest(_ target: TargetType, successCallback:@escaping RequestResultClosure, failureCallback: RequestResultClosure? = nil) -> Cancellable? {
    // 先判断网络是否有链接 没有的话直接返回--代码略
    if !UIDevice.isNetworkConnect {
        // code = 9999 代表无网络  这里根据具体业务来自定义
        errorHandler(code: 9999, message: "网络似乎出现了问题", failure: failureCallback)
        return nil
    }
    return Provider.request(MultiTarget(target)) { result in
        switch result {
        case let .success(response):
            do {
                let jsonData = try JSON(data: response.data)
                print("返回结果是:\(jsonData)")
                //改行代码为项目返回结果自测,可根据情况处理
                if !validateRepsonse(response: jsonData.dictionary, failure: failureCallback) { return }
                let respModel = ResponseModel()
                /// 这里的 -999的code码 需要根据具体业务来设置
                respModel.code = jsonData[codeKey].int ?? -999
                respModel.message = jsonData[messageKey].stringValue
                respModel.isSuccess = true
                if respModel.code == successCode {
                    respModel.data = jsonData[dataKey].rawString() ?? ""
                    successCallback(respModel)
                } else {
                    errorHandler(code: respModel.code , message: respModel.message , failure: failureCallback)
                    return
                }
            } catch {
                // code = 1000000 代表JSON解析失败  这里根据具体业务来自定义
                errorHandler(code: 1000000, message: String(data: response.data, encoding: String.Encoding.utf8)!, failure: failureCallback)
            }
        case let .failure(error as NSError):
            errorHandler(code: error.code, message: "网络连接失败", failure: failureCallback)
        }
    }    
}


/// 当不需要返回值的时候,调用请求
/// - Parameter target: TargetType
/// - Returns: 请求操作
func startRequest(_ target: TargetType)  -> Cancellable? {
    // 先判断网络是否有链接 没有的话直接返回--代码略
    if !UIDevice.isNetworkConnect {
        // code = 9999 代表无网络  这里根据具体业务来自定义
        return nil
    }
    return Provider.request(MultiTarget(target)) { result in        
    }
}

/// 基于Alamofire,网络是否连接,这个方法不建议放到这个类中,可以放在全局的工具类中判断网络链接情况
/// 用计算型属性是因为这样才会在获取isNetworkConnect时实时判断网络链接请求,如有更好的方法可以fork
extension UIDevice {
    static var isNetworkConnect: Bool {
        let network = NetworkReachabilityManager()
        return network?.isReachable ?? true // 无返回就默认网络已连接
    }
}

里面有很多的注释代码,其实都是有用的。
我们现在简单说一下注意点:

  1. 正常的网络请求操作就是调用:
func NetWorkRequest(_ target: TargetType, successCallback:@escaping RequestResultClosure, failureCallback: RequestResultClosure? = nil) -> Cancellable? 

🌰:

func testApi() {
        let request = NetWorkRequest(API.testApi) { responseModel in
            if responseModel.code == 200 {
                
            }
        } failureCallback: { responseModel in
            
        }
        request?.cancel()
    }
  1. 取消请求:request?.cancel()

  2. 如果我们需要一个请求操作,但是不需要它的返回值以及参数。那么就可以调用

startRequest
  1. 超时时间requestTimeOut,默认30s,可以自己设置。

  2. 最上面的闭包,可以根据自己需求修改:

typealias successCallback = ((String) -> (Void))
typealias failureCallback = ((String) -> (Void))

可以通过JSON解析,返回你想要的结果:

typealias RequestResultClosure = ((ResponseModel) -> Void)
  1. cer证书

目前网上的大所述证书设置方法都是:HTTPS就要手动实现Manager

 private func defaultAlamofireManager() -> Manager {

    let configuration = URLSessionConfiguration.default

//     configuration.httpAdditionalHeaders = Manager.defaultHTTPHeaders
    configuration.httpAdditionalHeaders = Alamofire.SessionManager.defaultHTTPHeaders


    let path: String = Bundle.main.path(forResource: "albbCloud", ofType: "cer") ?? ""
    let certificationData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData
    let certificate = SecCertificateCreateWithData(nil, certificationData!)
    let certificates: [SecCertificate] = [certificate!]

    let policies: [String: ServerTrustPolicy] = [Moya_baseURL: ServerTrustPolicy.pinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true)]
    let manager = Manager(configuration: configuration, serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies))

    return manager
 }

然后将其作为参数传出去就行:

let kProvider = MoyaProvider<MultiTarget>(endpointClosure: myEndpointClosure, requestClosure: requestClosure, manager:defaultAlamofireManager(), plugins: [networkPlugin], trackInflights: false)

基本都完成了。

这种写法是没问题的,但是Moya是封装的Alamofire,Alamofire5.0以后对其进行了修改,Moya也相应的做客修改,MoyaProvider()函数里面没有manager这个参数了,就没办法加载了。

针对最新的Alamofire5.4.3(对应Moya14.0.0)以后的版本,需要换一种写法,设置ssl,让后将ssl作为参数传进去:

//设置ssl
let session : Session = {
    //加载cer的代码,上面有写
}

let Provider3 = MoyaProvider<MultiTarget>(endpointClosure: myEndpointClosure, requestClosure: requestClosure,session : session, plugins: [networkPlugin], trackInflights: false)

这些代码,上面NetworkManager.swift里面的代码都有,我将其注释掉了,根据需要打开即可!

业务模块拆分

项目里面,我们可以根据需求,将不同的请求写在不同的业务模块里面,如果所有请求接口都写在API.swift里面,会显得非常繁杂:

分拆🌰

比如,将登录相关的api都写在APILogin里面。

完!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容