Moya15.0的使用

Alamofire可以理解為Swift版本的AFNetworking,是同一個團(tuán)隊寫的開源庫,Moya是對Alamofire的再次封裝!如果從難易程度上說,Alamofire可能會更簡單一些!

網(wǎng)上已經(jīng)有很多的關(guān)于Moya使用的文章,但是大多都是前幾年的。Moya14和Moya15,幾乎沒有相關(guān)的文章了。

多謝評論區(qū)小伙伴提醒,本文中 JSON 屬于SwiftyJSON中的類型。SwiftyJSON 這個庫非常值得推薦,是目前比較好用的解析庫,我平時會將SwiftyJSON和HandyJSON配合使用。

本文適用版本:
--> Moya15.0.0 (對應(yīng) Alamofire5.9)

系統(tǒng)版本:
--> iOS 10.0以上(Swift5.0以上)

本篇文章就是一個Moya的封裝使用,不具體講原理
至于Moya的好處,我從網(wǎng)上copy下來一張圖,僅供參考:


網(wǎng)絡(luò)copy圖

將Moya pod到工程中后,我們需要創(chuàng)建三個文件:


核心文件
API.swift     我們的API接口相關(guān)的東西,都寫在里面。
NetworkNanager.swift    Moya網(wǎng)絡(luò)框架封裝相關(guān)的都寫在這里面
MoyaConfig.swift   一些公用的網(wǎng)絡(luò)配置參數(shù),統(tǒng)一寫在這里面

一、MoyaConfig.swift:

在MoyaConfig文件里面配置需要的公用的參數(shù):
比如:

/// 定義基礎(chǔ)域名
let Moya_baseURL = "https://zhou.xuanhe.com"
/// 定義返回的JSON數(shù)據(jù)字段
let RESULT_CODE = "flag"      //狀態(tài)碼
let RESULT_MESSAGE = "message"  //錯誤消息提示
二、API.swift
2.1 首先創(chuàng)建接口API的枚舉
enum API {
    case login(parameters:[String:Any])    //參數(shù)可以是字典
    case testApi       //無參數(shù)接口
    case register(email:String,password:String)   //參數(shù)可以是字符串
    case uploadHeadImage(parameters:[String:Any],imageData:Data)
}
2.2 遵守TargetType協(xié)議

首先在API.swift文件內(nèi),導(dǎo)入頭文件import Moya
通過遵守TargetType協(xié)議,實現(xiàn)協(xié)議內(nèi)的相關(guān)api

extension API:TargetType{

}

遵守TargetType協(xié)議后(如上圖),XCode會提示相關(guān)信息的,讓你實現(xiàn)TargetType協(xié)議里面的屬性或者函數(shù)

extension API:TargetType{
    //baseURL 也可用枚舉區(qū)分不同的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
        }
    }
    
    /// 這個是做單元測試模擬的數(shù)據(jù),必須要實現(xiàn),只在單元測試文件中有作用
    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

// 回調(diào) 包括:網(wǎng)絡(luò)請求的模型(code,message,data等,具體根據(jù)業(yè)務(wù)來定)
typealias RequestResultClosure = ((ResponseModel) -> Void)

///先添加一個閉包用于成功時后臺返回數(shù)據(jù)的回調(diào)
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和網(wǎng)上其他實現(xiàn)有些不太一樣。
    ///主要是為了解決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 // 每次請求都會調(diào)用endpointClosure 到這里設(shè)置超時時長 也可單獨每個接口設(shè)置
    
    // 針對于某個具體的業(yè)務(wù)模塊來做接口配置
    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
        //打印請求參數(shù)
        if let requestData = request.httpBody {
            print("\(request.url!)"+"\n"+"\(request.httpMethod ?? "")"+"發(fā)送參數(shù)"+"\n"+"\(String(data: request.httpBody!, encoding: String.Encoding.utf8) ?? "")")
        }else{
            print("\(request.url!)"+"\(String(describing: request.httpMethod))")
        }
        
        if let header = request.allHTTPHeaderFields {
            print("請求頭內(nèi)容\(header)")
        }
        done(.success(request))
        
    } catch  {
        done(.failure(MoyaError.underlying(error, nil)))
    }
}


// 用Moya默認(rèn)的Manager還是Alamofire的Manager看實際需求。HTTPS就要手動實現(xiàn)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當(dāng)參數(shù)傳進(jìn)去就行了


//MARK: 設(shè)置ssl  處理https證書驗證
let session : Session = {
   //證書數(shù)據(jù)
   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當(dāng)參數(shù)傳進(jìn)去就行了


/// NetworkActivityPlugin插件用來監(jiān)聽網(wǎng)絡(luò)請求
private let networkPlugin = NetworkActivityPlugin.init { changeType, TargetType in
    print("networkPlugin \(changeType)")
    
    //TargetType 是當(dāng)前請求的基本信息
    switch (changeType){
    case .began :
        print("\n")
        print(TargetType)
        print("\n")       
        print("開始請求網(wǎng)絡(luò)")
    case .ended :
        print("網(wǎng)絡(luò)請求結(jié)束")
    }
}

/// 網(wǎng)絡(luò)請求發(fā)送的核心初始化方法,創(chuàng)建網(wǎng)絡(luò)請求對象
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 = ""
    /// 分頁的游標(biāo) 根據(jù)具體的業(yè)務(wù)選擇是否添加這個屬性
    var cursor: String = ""
}



/// 錯誤處理
/// - Parameters:
///   - code: code碼
///   - message: 錯誤消息
///   - needShowFailAlert: 是否顯示網(wǎng)絡(luò)請求失敗的彈框
///   - failure: 網(wǎng)絡(luò)請求失敗的回調(diào)
private func errorHandler(code: Int, message: String, failure: RequestResultClosure?) {
    print("發(fā)生錯誤:\(code)--\(message)")
    let model = ResponseModel()
    model.code = code
    model.message = message
    model.isSuccess = false
    failure?(model)
}

/// 預(yù)判斷后臺返回的數(shù)據(jù)有效性 如通過Code碼來確定數(shù)據(jù)完整性等  根據(jù)具體的業(yè)務(wù)情況來判斷  有需要自己可以打開注釋
/// - Parameters:
///   - response: 后臺返回的數(shù)據(jù)
///   - showFailAlet: 是否顯示失敗的彈框
///   - failure: 失敗的回調(diào)
/// - Returns: 數(shù)據(jù)是否有效
private func validateRepsonse(response: [String: JSON]?, failure: RequestResultClosure?) -> Bool {
    /**
     var errorMessage: String = ""
     if response != nil {
     if !response!.keys.contains(codeKey) {
     errorMessage = "返回值不匹配:缺少狀態(tài)碼"
     } else if response![codeKey]!.int == 500 {
     errorMessage = "服務(wù)器開小差了"
     }
     } else {
     errorMessage = "服務(wù)器數(shù)據(jù)開小差了"
     }
     
     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: 成功回調(diào)
///   - failureCallback: 失敗回調(diào)
/// - Returns: 請求操作
@discardableResult
func NetWorkRequest(_ target: TargetType, successCallback:@escaping RequestResultClosure, failureCallback: RequestResultClosure? = nil) -> Cancellable? {
    // 先判斷網(wǎng)絡(luò)是否有鏈接 沒有的話直接返回--代碼略
    if !UIDevice.isNetworkConnect {
        // code = 9999 代表無網(wǎng)絡(luò)  這里根據(jù)具體業(yè)務(wù)來自定義
        errorHandler(code: 9999, message: "網(wǎng)絡(luò)似乎出現(xiàn)了問題", 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("返回結(jié)果是:\(jsonData)")
                //改行代碼為項目返回結(jié)果自測,可根據(jù)情況處理
                if !validateRepsonse(response: jsonData.dictionary, failure: failureCallback) { return }
                let respModel = ResponseModel()
                /// 這里的 -999的code碼 需要根據(jù)具體業(yè)務(wù)來設(shè)置
                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解析失敗  這里根據(jù)具體業(yè)務(wù)來自定義
                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: "網(wǎng)絡(luò)連接失敗", failure: failureCallback)
        }
    }    
}


/// 當(dāng)不需要返回值的時候,調(diào)用請求
/// - Parameter target: TargetType
/// - Returns: 請求操作
func startRequest(_ target: TargetType)  -> Cancellable? {
    // 先判斷網(wǎng)絡(luò)是否有鏈接 沒有的話直接返回--代碼略
    if !UIDevice.isNetworkConnect {
        // code = 9999 代表無網(wǎng)絡(luò)  這里根據(jù)具體業(yè)務(wù)來自定義
        return nil
    }
    return Provider.request(MultiTarget(target)) { result in        
    }
}

/// 基于Alamofire,網(wǎng)絡(luò)是否連接,這個方法不建議放到這個類中,可以放在全局的工具類中判斷網(wǎng)絡(luò)鏈接情況
/// 用計算型屬性是因為這樣才會在獲取isNetworkConnect時實時判斷網(wǎng)絡(luò)鏈接請求,如有更好的方法可以fork
extension UIDevice {
    static var isNetworkConnect: Bool {
        let network = NetworkReachabilityManager()
        return network?.isReachable ?? true // 無返回就默認(rèn)網(wǎng)絡(luò)已連接
    }
}

里面有很多的注釋代碼,其實都是有用的。
我們現(xiàn)在簡單說一下注意點:

  1. 正常的網(wǎng)絡(luò)請求操作就是調(diào)用:
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. 如果我們需要一個請求操作,但是不需要它的返回值以及參數(shù)。那么就可以調(diào)用

startRequest
  1. 超時時間requestTimeOut,默認(rèn)30s,可以自己設(shè)置。

  2. 最上面的閉包,可以根據(jù)自己需求修改:

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

可以通過JSON解析,返回你想要的結(jié)果:

typealias RequestResultClosure = ((ResponseModel) -> Void)
  1. cer證書

目前網(wǎng)上的大所述證書設(shè)置方法都是:HTTPS就要手動實現(xiàn)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
 }

然后將其作為參數(shù)傳出去就行:

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

基本都完成了。

這種寫法是沒問題的,但是Moya是封裝的Alamofire,Alamofire5.0以后對其進(jìn)行了修改,Moya也相應(yīng)的做客修改,MoyaProvider()函數(shù)里面沒有manager這個參數(shù)了,就沒辦法加載了。

針對最新的Alamofire5.4.3(對應(yīng)Moya14.0.0)以后的版本,需要換一種寫法,設(shè)置ssl,讓后將ssl作為參數(shù)傳進(jìn)去:

//設(shè)置ssl
let session : Session = {
    //加載cer的代碼,上面有寫
}

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

這些代碼,上面NetworkManager.swift里面的代碼都有,我將其注釋掉了,根據(jù)需要打開即可!

業(yè)務(wù)模塊拆分

項目里面,我們可以根據(jù)需求,將不同的請求寫在不同的業(yè)務(wù)模塊里面,如果所有請求接口都寫在API.swift里面,會顯得非常繁雜:

分拆??

比如,將登錄相關(guān)的api都寫在APILogin里面。

完!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容