iOS架构篇-3 网络接口封装
iOS架构篇-3 网络接口封装
关键字:iOS,网络接口封装,Alamofire,swift
网络接口API通常都需要自己封装一套管理,这里以swift版的Alamofire为例.
实现功能:
1.暴露参数请求地址url、请求方法method、请求参数params、请求头header、请求响应response(响应数据、响应头responseHeader)、https请求证书
2.支持get、post、put、delete、head、patch、文件上传下载、https证书
3.本地开发环境、测试环境、生产环境切换
1.添加Alamofire库
source 'https://gitee.com/mirrors/CocoaPods-Specs.git'
platform :ios, '9.0'
workspace 'ProjectApp'
inhibit_all_warnings!
use_frameworks!
install! 'cocoapods', :deterministic_uuids => false
abstract_target 'common' do
pod 'Alamofire', '~> 4.9.1'
target :'ProjectApp' do
project 'ProjectApp'
pod 'RxSwift', '6.0.0'
pod 'RxCocoa', '6.0.0'
end
target :'ComponentNetwork' do
project '../ComponentNetwork/ComponentNetwork'
end
end
Alamofire需要同时添加到ComponentNetwork、ProjectApp两个project,不能只添加到ComponentNetwork project,这里用cocopods的abstract_target为多个project添加同样的库, CocoPods的具体使用参考iOS技术篇-CocoaPods
2.先定义需要暴露的API接口
// 定义别名
public typealias CPMethod = Alamofire.HTTPMethod
public typealias CPRespHandle = (_ suc: Bool, _ data: String?, _ error: Error?, _ responseHeader: [AnyHashable : Any]?) -> Void
public typealias CPProgressHandle = (_ progress: Progress) -> Void
public typealias CPDownloadRespHandle = (_ suc: Bool, _ resp: Data?, _ destinationURL: URL?, _ error: Error?, _ responseHeader: [AnyHashable : Any]?) -> Void
// 定义方法
public func CPRequest(url: String, requestMethod: CPMethod, params: [String: Any]? = nil, header: [String: String]? = nil, respHandle: CPRespHandle? = nil, cer: Data? = nil) {}
public func CPUploadFile(url: String, header: [String: String]? = nil, params: [String: Any]? = nil, file: AnyObject, fileName: String, key: String, mimeType: String, uploadingHandle:@escaping CPProgressHandle, respHandle: CPRespHandle? = nil) -> Void {}
public func CPDownloadFile(url: String, header: [String: String]? = nil, filePath:URL, downloaDingHandle: CPProgressHandle? = nil, respHandle: CPDownloadRespHandle? = nil) -> Void {}
3.封装Alamofire
针对暴露的API定义方法
//
// AFService.swift
//
//
// Created by wrs on 2018/7/13.
// Copyright © 2018年 wrs. All rights reserved.
//
import UIKit
import Alamofire
public let shareSessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders
configuration.timeoutIntervalForRequest = 20
return SessionManager(configuration: configuration)
}()
func AFRequest(url: String, requestMethod: CPMethod, params: [String: Any]? = nil, header: [String: String]? = nil, respHandle: CPRespHandle? = nil, cer localCer: Data?)
{
let manager = shareSessionManager
manager.delegate.sessionDidReceiveChallenge = { session, challenge in
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { // 认证服务器!
if let localCertificateData = localCer { // 有证书就认证
let serverTrust:SecTrust = challenge.protectionSpace.serverTrust!
let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)!
let remoteCertificateData
= CFBridgingRetain(SecCertificateCopyData(certificate))!
if (remoteCertificateData.isEqual(localCertificateData) == true) { // 证书一致
let credential = URLCredential(trust: serverTrust)
challenge.sender?.use(credential, for: challenge)
return (.useCredential,
URLCredential(trust: serverTrust))
} else { // 证书不对
return (.cancelAuthenticationChallenge, nil)
}
} else { // 没有证书就不校验
return (.performDefaultHandling, nil)
}
} else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate { //认证客户端证书
// let identityAndTrust:IdentityAndTrust = extractIdentity();
// let urlCredential:URLCredential = URLCredential(
// identity: identityAndTrust.identityRef,
// certificates: identityAndTrust.certArray as? [AnyObject],
// persistence: URLCredential.Persistence.forSession);
//
// return (.useCredential, urlCredential);
return (.performDefaultHandling, nil)
} else { // 其它情况(不接受认证)
return (.cancelAuthenticationChallenge, nil)
}
}
var encoding: ParameterEncoding = URLEncoding.methodDependent
switch requestMethod {
case .post:
encoding = JSONEncoding.default
default:
break
}
let request: DataRequest = manager.request(url, method: requestMethod, parameters: params, encoding: encoding, headers: header)
request.responseString(queue: nil, encoding: String.Encoding.utf8) { (response: DataResponse<String>) in
if let handle = respHandle {
switch response.result {
case .success:
handle(true, response.result.value, nil, response.response?.allHeaderFields)
case .failure(let error):
handle(false, response.result.value, error, nil)
}
}
}
// request.responseString { (response: DataResponse<String>) in
// if let handle = respHandle {
// switch response.result {
// case .success:
// handle(true, response.result.value, nil)
// case .failure(let error):
// handle(true, response.result.value, error)
// }
// }
// }
// request.responseJSON { (response: DataResponse<Any>) in
// if let handle = respHandle {
// switch response.result {
// case .success:
// handle(true, response.result.value, nil)
// case .failure(let error):
// if let _ = response.data {
// let respStr = String(data: response.data!, encoding: .utf8)
// handle(false, respStr, error)
// } else {
// handle(false, nil, error)
// }
// }
// }
// }
}
func AFUploadFile(url: String, header: [String: String]? = nil, params: [String: Any]? = nil, file: AnyObject?, fileName: String, key: String, mimeType: String, uploadingHandle:@escaping CPProgressHandle, respHandle: CPRespHandle? = nil) -> Void {
if let aUrl = URL(string: url) {
Alamofire.upload(multipartFormData: { (formData) in
if let f = file {
if f is Data {
formData.append(file as! Data, withName: key, fileName: fileName, mimeType: mimeType)
} else if f is String {
formData.append(URL(fileURLWithPath: file as! String), withName: key, fileName: fileName, mimeType: mimeType)
} else if f is UIImage {
if let data = (file as! UIImage).pngData() {
formData.append(data, withName: key, fileName: fileName, mimeType: mimeType)
}
}
}
if let para = params {
for (key, value) in para {
if let content = value as? String {
if let valueData = content.data(using: String.Encoding.utf8) {
formData.append(valueData, withName: key)
}
} else if let file = value as? Data {
formData.append(file , withName: key, fileName: fileName, mimeType: mimeType)
} else if let file = value as? UIImage {
if let data = (file).pngData() {
formData.append(data, withName: key, fileName: fileName, mimeType: mimeType)
}
}
}
}
}, usingThreshold: SessionManager.multipartFormDataEncodingMemoryThreshold, to:aUrl, method: .post, headers: header) { (encodingResult: SessionManager.MultipartFormDataEncodingResult) in
switch encodingResult {
case .success(let uploadRequest, _, _):
uploadRequest.uploadProgress(closure: { (progress: Progress) in
uploadingHandle(progress)
})
uploadRequest.responseString(completionHandler: { (dataResponse) in
if let handle = respHandle {
handle(true, dataResponse.result.value, nil, dataResponse.response?.allHeaderFields)
}
})
case .failure(let error):
if let handle = respHandle {
handle(false, nil, error, nil)
}
}
}
} else {
if let handle = respHandle {
handle(false, nil, nil, nil)
}
}
}
func AFDownloadFile(url: String, header: [String: String]? = nil, filePath:URL, downloaDingHandle: CPProgressHandle? = nil, respHandle: CPDownloadRespHandle? = nil) -> Void {
if let aUrl = URL(string: url) {
let destination: DownloadRequest.DownloadFileDestination = { (resultUrl:URL, resp:HTTPURLResponse) in
let fileUrl = filePath.appendingPathComponent(resp.suggestedFilename!)
return (fileUrl, DownloadRequest.DownloadOptions.removePreviousFile)
}
Alamofire.download(aUrl, method: .get, parameters: nil, encoding: URLEncoding.default, headers: header, to: destination).downloadProgress { (progress) in
if let handle = downloaDingHandle {
handle(progress)
}
}.responseData { (response:DownloadResponse<Data>) in
if let handle = respHandle {
switch response.result {
case .success:
handle(true, response.result.value, response.destinationURL, nil, response.response?.allHeaderFields)
case .failure(let error):
handle(false, response.result.value, response.destinationURL, error, nil)
}
}
}
} else {
if let handle = respHandle {
handle(false, nil, nil, nil, nil)
}
}
}
//获取客户端证书相关信息
func extractIdentity() -> IdentityAndTrust {
var identityAndTrust:IdentityAndTrust!
var securityError:OSStatus = errSecSuccess
let path: String = Bundle.main.path(forResource: "client", ofType: "p12")!
let PKCS12Data = NSData(contentsOfFile:path)!
let key : NSString = kSecImportExportPassphrase as NSString
let options : NSDictionary = [key : "xxxxxxxxxxxx"] //客户端证书密码
var items : CFArray?
securityError = SecPKCS12Import(PKCS12Data, options, &items)
if securityError == errSecSuccess {
let certItems:CFArray = items!;
let certItemsArray:Array = certItems as Array
let dict:AnyObject? = certItemsArray.first;
if let certEntry:Dictionary = dict as? Dictionary<String, AnyObject> {
// grab the identity
let identityPointer:AnyObject? = certEntry["identity"];
let secIdentityRef:SecIdentity = identityPointer as! SecIdentity
// grab the trust
let trustPointer:AnyObject? = certEntry["trust"]
let trustRef:SecTrust = trustPointer as! SecTrust
// grab the cert
let chainPointer:AnyObject? = certEntry["chain"]
identityAndTrust = IdentityAndTrust(identityRef: secIdentityRef,
trust: trustRef, certArray: chainPointer!)
}
}
return identityAndTrust;
}
//定义一个结构体,存储认证相关信息
struct IdentityAndTrust {
var identityRef:SecIdentity
var trust:SecTrust
var certArray:AnyObject
}
4.抽取接口参数,增加参数API
public protocol CPRequestParams {
var url: String { get }
var method: CPMethod { get }
var params: [String : Any]? { get }
var headers: [String: String]? { get }
var cer: Data? { get }
}
public func CPRequest(params: CPRequestParams, respHandle: CPRespHandle? = nil) {
AFRequest(url: params.url, requestMethod: params.method, params: params.params, header: params.headers, respHandle: respHandle, cer: params.cer)
}
5.定义业务层API
实现功能:
1)API环境切换,如本地静态配置文件数据、swagger mock数据、开发环境数据、测试环境数据、灰度环境数据、生产环境数据
2)接口请求是否需要HUD Loading
3)上层屏蔽网络出错、后台系统出错、后台业务错误等数据,上层业务只关注正确的接口数据
4)接口返回数据统一模型,不同业务通过范型统一返回
5)支持入参建模
6)debug模式打印接口数据,方便调试和数据监听
7)所有接口统一加上固定请求头,登录后再加上token
业务层接口API定义:
/// 业务接口请求
/// - Parameters:
/// - params: 业务参数
/// - errorToast: 错误时是否进行错误toast提示
/// - hud: 是否显示请求loading
/// - handle: 业务回调
func request<T: Codable>(params: RequestParams, errorToast: Bool, hud: Bool, handle: @escaping RespHandle<T>)
// API环境切换枚举定义
enum DataEnv {
case QA(host: String)
case Staging(host: String)
case Production(host: String)
case Mock(host: String)
case Custom(host: String)
var host: String {
var path = ServerProdPath
switch self {
case let .QA(host):
path = host
case let .Staging(host):
path = host
case let .Production(host):
path = host
case let .Mock(host):
path = host
case let .Custom(host):
path = host
}
return path
}
}
定义一个API环境变量,通过更改此变量来切换不同环境:
var dataEnv: DataEnv = .QA(host: ServerDevPath)
var ServerProdPath = "http://www.xxxx.cn"
let ServerDevPath = "http://192.168.0.169:8089"
定义业务接口类型,继承CPRequestParams:
enum RequestParams: CPRequestParams{
case Login(account: String, password: String)
case Register(user: RegisterEntity)
var target: RequestParamsTarget {
get {
switch self {
case let .Login(account, password):
return RequestParamsTarget(suffix: "/login", method: .post, params: [
"account": account,
"password": password
])
case let .Register(user):
var params: [String: Any]? = nil
if let data: Data = try? JSONEncoder().encode(user), let dic = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String: Any] {
params = dic
}
return RequestParamsTarget(suffix: "/register", method: .post, params: params)
}
}
}
var url: String {
self.target.url
}
var method: CPMethod {
self.target.method
}
var params: [String : Any]? {
self.target.params
}
var headers: [String : String]? {
getAppHeader()
}
var cer: Data? {
nil
}
}
定义接口返回数据模型
class RespEntity<T: Codable> {
var code: Int?
var msg: String?
var data: T?
init(dic: [String : Any]) {
if let value = dic["code"] as? Int {
self.code = value
}
if let value = dic["msg"] as? String {
self.msg = value
}
if let tempJson: Any = dic["data"] {
if !(tempJson is NSNull) { // 防止data为null
if tempJson is String { // T -> String
self.data = tempJson as? T
} else if tempJson is Double { // T -> Double, data为数字时,T不能是Float,使用Double通用
self.data = tempJson as? T
} else {
if let tempData: Data = try? JSONSerialization.data(withJSONObject: tempJson, options: JSONSerialization.WritingOptions.prettyPrinted){
if let model: T = try? JSONDecoder().decode(T.self, from: tempData) {
self.data = model
}
}
}
}
}
}
var suc: Bool {
var flag = false
if let c = self.code, c == 10000 {
flag = true
}
return flag
}
}
后台一般所有数据都返回统一模型,如:
{
"code": 10000,
"msg":"aaaa",
"data":[{
"date":"2020-12-01",
"title": "手机充值"
}]
}
注意这里的模型转换需要兼容后台返回错误的模型,防止后台出错导致app出现闪退或未知异常
调用例子
request(params: .Login(account: "", password: ""), errorToast: true, hud: true) { (suc: Bool, model: RespEntity<[VideoEntity]>?, error: Error?) in
if let flag = model?.suc, flag == true {
if let list = model?.data {
DPrint(message:list)
} else {
DPrint(message:"no list")
}
}
}
// let registerEntity: RegisterEntity = RegisterEntity()
// registerEntity.username = "wrs"
// registerEntity.password = "123456"
// request(params: .Register(user: registerEntity), errorToast: true, hud: true) { (suc, model:RespEntity<String>?, error) in
//
// }
源码下载