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下來一張圖,僅供參考:

將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)在簡單說一下注意點:
- 正常的網(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()
}
取消請求:
request?.cancel()如果我們需要一個請求操作,但是不需要它的返回值以及參數(shù)。那么就可以調(diào)用
startRequest
超時時間
requestTimeOut,默認(rèn)30s,可以自己設(shè)置。最上面的閉包,可以根據(jù)自己需求修改:
typealias successCallback = ((String) -> (Void))
typealias failureCallback = ((String) -> (Void))
可以通過JSON解析,返回你想要的結(jié)果:
typealias RequestResultClosure = ((ResponseModel) -> Void)
- 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里面。
完!