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下来一张图,仅供参考:
将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(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 // 无返回就默认网络已连接
}
}
里面有很多的注释代码,其实都是有用的。
我们现在简单说一下注意点:
- 正常的网络请求操作就是调用:
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()
如果我们需要一个请求操作,但是不需要它的返回值以及参数。那么就可以调用
startRequest
超时时间
requestTimeOut
,默认30s,可以自己设置。最上面的闭包,可以根据自己需求修改:
typealias successCallback = ((String) -> (Void))
typealias failureCallback = ((String) -> (Void))
可以通过JSON解析,返回你想要的结果:
typealias RequestResultClosure = ((ResponseModel) -> Void)
- 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(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(endpointClosure: myEndpointClosure, requestClosure: requestClosure,session : session, plugins: [networkPlugin], trackInflights: false)
这些代码,上面NetworkManager.swift
里面的代码都有,我将其注释掉了,根据需要打开即可!
业务模块拆分
项目里面,我们可以根据需求,将不同的请求写在不同的业务模块里面,如果所有请求接口都写在API.swift里面,会显得非常繁杂:
比如,将登录相关的api都写在APILogin里面。
完!