WXNetworkingSwift
簡(jiǎn)介
有沒(méi)有遇到過(guò)這樣一種情況爷辙,每次在項(xiàng)目中使用請(qǐng)求庫(kù)去請(qǐng)求數(shù)據(jù)時(shí),各種小功能需要自己在每個(gè)請(qǐng)求里面單獨(dú)去開(kāi)發(fā)褐墅,比如請(qǐng)求緩存痴颊、請(qǐng)求HUD赏迟、設(shè)置請(qǐng)求頭、設(shè)置失敗重試機(jī)制蠢棱、判斷是否請(qǐng)求成功锌杀、請(qǐng)求個(gè)性化打印日志、控制批量請(qǐng)求泻仙、頁(yè)面請(qǐng)求重復(fù)寫數(shù)據(jù)轉(zhuǎn)模型......, 甚至使用了很久的第三方網(wǎng)絡(luò)某一天不維護(hù)了抛丽,導(dǎo)致項(xiàng)目那里面每個(gè)頁(yè)面到處直接使用的Api更換起來(lái)簡(jiǎn)直就是災(zāi)難,面對(duì)這種情況特意 底層基于Alamofire
庫(kù) 封裝一套支持高度自定義擴(kuò)展多功能的網(wǎng)絡(luò)請(qǐng)求庫(kù)饰豺,幫我們?cè)陂_(kāi)發(fā)時(shí)省去了很多配置亿鲜,也可以增加自定義插件使,即使以后更換底層請(qǐng)求庫(kù)也很方便,后續(xù)也會(huì)不斷維護(hù)更新各種小功能蒿柳,目前支持的主要功能如下: OC版本的見(jiàn)這里
功能列表:
1饶套、自定義請(qǐng)求頭;簡(jiǎn)單配置請(qǐng)求頭或加密頭
2垒探、自動(dòng)處理是否緩存妓蛮;設(shè)置緩存機(jī)制,自動(dòng)失效時(shí)間等
3圾叼、請(qǐng)求失敗自定義多次重試蛤克;支持失敗后每隔幾秒嘗試再試請(qǐng)求,如啟動(dòng)App后一定要請(qǐng)求的必要數(shù)據(jù)接口夷蚊。
4构挤、支持上傳接口抓包日志;如上傳到公司內(nèi)部日志服務(wù)器系統(tǒng)上惕鼓,供測(cè)試人員排查問(wèn)題或快速抓包排查問(wèn)題筋现。
5、極簡(jiǎn)上傳下載文件監(jiān)聽(tīng); 簡(jiǎn)單配置監(jiān)聽(tīng)上傳下載文件進(jìn)度箱歧。
6矾飞、支持全局/單個(gè)配置請(qǐng)求成功后keyPath模型映射;頁(yè)面上無(wú)需每個(gè)接口編寫解析字典轉(zhuǎn)模型的重新代碼呀邢,支持?jǐn)?shù)組和自定義模型洒沦;
7、約定全局請(qǐng)求的提示Hud ToastKey价淌;支持單個(gè)配置或全局配置請(qǐng)求失敗時(shí)的HUD Toast自動(dòng)彈框提示申眼。
8、請(qǐng)求遇到相應(yīng)Code時(shí)觸發(fā)通知输钩;如:Token失效全部重新登錄等;
9、網(wǎng)絡(luò)請(qǐng)求過(guò)程多鏈路回調(diào)管理仲智;如:請(qǐng)求將要開(kāi)始回調(diào)买乃,請(qǐng)求回調(diào)將要停止,請(qǐng)求已經(jīng)回調(diào)完成;
10钓辆、格式化打印網(wǎng)絡(luò)日志剪验;輸出日志一目了然,如:請(qǐng)求接口地址前联、參數(shù)功戚、請(qǐng)求頭、耗時(shí)似嗤、響應(yīng);
11啸臀、批量請(qǐng)求;支持自定義每個(gè)請(qǐng)求的所有配置,并且可配置等待全部完成才回調(diào)還是一起完成才回調(diào);
-
12乘粒、支持debug模式不請(qǐng)求網(wǎng)絡(luò)快速調(diào)試模擬接口響應(yīng)數(shù)據(jù)豌注;如:本地json string,Dictionary灯萍,local json file, http test url
. . . . . .(持續(xù)完善-ing)
使用環(huán)境
iOS, swift 5.0
安裝方式
WXNetworkingSwift is available through CocoaPods. To install
it, simply add the following line to your Podfile:
pod 'WXNetworkingSwift'
用法方法見(jiàn) (示例Demo)
可靈活配置的基礎(chǔ)請(qǐng)求對(duì)象
///請(qǐng)求基礎(chǔ)對(duì)象, 外部上不建議直接用轧铁,請(qǐng)使用子類請(qǐng)求方法
open class WXBaseRequest: NSObject {
///請(qǐng)求Method類型
fileprivate (set) var requestMethod: HTTPMethod = .post
///請(qǐng)求地址
fileprivate (set) var requestURL: String = ""
///請(qǐng)求參數(shù)
fileprivate var parameters: WXDictionaryStrAny? = nil
///請(qǐng)求超時(shí),默認(rèn)30是
public var timeOut: TimeInterval = 30
///請(qǐng)求自定義頭信息
public var requestHeaderDict: Dictionary<String, String>? = nil
///請(qǐng)求序列化對(duì)象 (json, form表單)
public var requestSerializer: WXRequestSerializerType = .EncodingJSON
///請(qǐng)求任務(wù)對(duì)象
fileprivate var requestDataTask: Request? = nil
///請(qǐng)求方法見(jiàn)源碼
}
可靈活配置的單個(gè)請(qǐng)求對(duì)象:
/// 單個(gè)請(qǐng)求對(duì)象, 功能根據(jù)需求可多種自定義
open class WXRequestApi: WXBaseRequest {
///請(qǐng)求成功時(shí)是否自動(dòng)緩存響應(yīng)數(shù)據(jù), 默認(rèn)不緩存
public var autoCacheResponse: Bool = false
///自定義請(qǐng)求成功時(shí)的緩存數(shù)據(jù), (返回的字典為此次需要保存的緩存數(shù)據(jù), 返回nil時(shí)底層則不緩存)
public var cacheResponseBlock: ( (WXResponseModel) -> (WXDictionaryStrAny?) )? = nil
///自定義解析成功時(shí)的響應(yīng)數(shù)據(jù), (例如: 在請(qǐng)求成功后 需要解密響應(yīng)的json結(jié)果后才能真正獲取成功標(biāo)識(shí), 解析模型等等..)
public var decryptHandlerResponse: ((AnyObject) -> AnyObject)? = nil
///自定義請(qǐng)求成功映射Key/Value, (key可以是KeyPath模式進(jìn)行匹配 如: data.status)
///注意: 每個(gè)請(qǐng)求狀態(tài)優(yōu)先使用此屬性判斷, 如果此屬性值為空, 則再取全局的 WXNetworkConfig.successStatusMap的值進(jìn)行判斷
public var successStatusMap: (key: String, value: String)? = nil
///請(qǐng)求成功時(shí)自動(dòng)解析數(shù)據(jù)模型映射:Key/ModelType, (key可以是KeyPath模式進(jìn)行匹配 如: data.returnData)
///成功解析的模型在 WXResponseModel.parseKeyPathModel 中返回
public var parseModelMap: (parseKey: String, modelType: Convertible.Type)? = nil
///times: 請(qǐng)求失敗之后重新請(qǐng)求次數(shù), delay: 每次重試的間隔
public var retryWhenFailTuple: (times: Int, delay: Double)? = nil
/// [??僅DEBUG模式生效??] 作用:方便開(kāi)發(fā)時(shí)調(diào)試接口使用,設(shè)置的值可為以下4種:
/// 1. json String: 則不會(huì)請(qǐng)求網(wǎng)絡(luò), 直接響應(yīng)回調(diào)此json值
/// 2. Dictionary: 則不會(huì)請(qǐng)求網(wǎng)絡(luò), 直接響應(yīng)回調(diào)此Dictionary值
/// 3. local file path: 則直接讀取當(dāng)前本地的path文件內(nèi)容
/// 4. http(是) path: 則直接請(qǐng)求當(dāng)前設(shè)置的path
public var debugJsonResponse: Any? = nil
///請(qǐng)求轉(zhuǎn)圈的父視圖
public var loadingSuperView: UIView? = nil
///上傳文件Data數(shù)組
public var uploadFileDataArr: [ Data ]? = nil
///自定義上傳時(shí)包裝的數(shù)據(jù)Data對(duì)象
public var uploadConfigDataBlock: ( (MultipartFormData) -> Void )? = nil
///監(jiān)聽(tīng)上傳/下載進(jìn)度
public var fileProgressBlock: WXProgressBlock? = nil
///網(wǎng)絡(luò)請(qǐng)求過(guò)程多鏈路回調(diào)<將要開(kāi)始, 將要停止, 已經(jīng)完成>
/// 注意: 如果沒(méi)有實(shí)現(xiàn)此代理則會(huì)回調(diào)單例中的全局代理<globleMulticenterDelegate>
public var multicenterDelegate: WXNetworkMulticenter? = nil
///可以用來(lái)添加幾個(gè)accossories對(duì)象 來(lái)做額外的插件等特殊功能
///如: (請(qǐng)求HUD, 加解密, 自定義打印, 上傳統(tǒng)計(jì))
public var requestAccessories: [WXNetworkMulticenter]? = nil
///請(qǐng)求方法見(jiàn)源碼
}
請(qǐng)求響應(yīng)對(duì)象的豐富信息
//MARK: - 請(qǐng)求響應(yīng)對(duì)象
///包裝的響應(yīng)數(shù)據(jù)
public class WXResponseModel: NSObject {
/**
* 是否請(qǐng)求成功,優(yōu)先使用 WXRequestApi.successStatusMap 來(lái)判斷是否成功
* 否則使用 WXNetworkConfig.successStatusMap 標(biāo)識(shí)來(lái)判斷是否請(qǐng)求成功
***/
public var isSuccess: Bool = false
///本次響應(yīng)Code碼
public var responseCode: Int? = nil
///本次響應(yīng)的提示信息 (頁(yè)面可直接用于Toast提示,
///如果接口有返回messageTipKeyAndFailInfo.tipKey則會(huì)取這個(gè)值, 如果沒(méi)有返回則取defaultTip的默認(rèn)值)
public var responseMsg: String? = nil
///本次數(shù)據(jù)是否為緩存
public var isCacheData: Bool = false
///請(qǐng)求耗時(shí)(毫秒)
public var responseDuration: TimeInterval? = nil
///解析數(shù)據(jù)的模型: 可KeyPath匹配, 返回 Model對(duì)象 或者數(shù)組模型 [Model]
public var parseKeyPathModel: AnyObject? = nil
///本次響應(yīng)的原始數(shù)據(jù): NSDictionary/ UIImage/ NSData /...
public var responseObject: AnyObject? = nil
///本次響應(yīng)的原始字典數(shù)據(jù)
public var responseDict: WXDictionaryStrAny? = nil
///本次響應(yīng)的數(shù)據(jù)是否為Debug測(cè)試數(shù)據(jù)
public var isDebugResponse: Bool = false
///失敗時(shí)的錯(cuò)誤信息
public var error: NSError? = nil
///原始響應(yīng)
public var urlResponse: HTTPURLResponse? = nil
///原始請(qǐng)求
public var urlRequest: URLRequest? = nil
}
可靈活配置的批量請(qǐng)求對(duì)象:
///批量請(qǐng)求對(duì)象, 可以
open class WXBatchRequestApi {
///全部請(qǐng)求是否都成功了
public var isAllSuccess: Bool = false
///全部響應(yīng)數(shù)據(jù), 按請(qǐng)求requestArray的Api添加順序排序返回
public var responseDataArray: [WXResponseModel] = []
///全部請(qǐng)求對(duì)象, 響應(yīng)時(shí)Api按添加順序返回
fileprivate var requestArray: [WXRequestApi]
///請(qǐng)求轉(zhuǎn)圈的父視圖
fileprivate (set) var loadingSuperView: UIView? = nil
///請(qǐng)求方法見(jiàn)源碼
}
1.單個(gè)請(qǐng)求示例
func testRequest() {
let url = "http://123.207.32.32:8000/home/multidata"
let api = WXRequestApi(url, method: .get)
api.timeOut = 40 //設(shè)置超時(shí)時(shí)間
api.loadingSuperView = view //請(qǐng)求loading HUD
api.autoCacheResponse = true //是否需要緩存
api.requestHeaderDict = [:] //設(shè)置請(qǐng)求自定義頭信息
api.successStatusMap = (key: "returnCode", value: "SUCCESS") //設(shè)置請(qǐng)求成功標(biāo)識(shí)key(支持keyPath)
api.parseModelMap = (parseKey: "data.dKeyword", modelType: DKeywordModel.self) //設(shè)置請(qǐng)求成功模型解析(支持keyPath)
api.retryWhenFailTuple = (times: 3, delay: 2.0) //設(shè)置請(qǐng)求失敗重試機(jī)制
api.multicenterDelegate = self //網(wǎng)絡(luò)請(qǐng)求過(guò)程多鏈路回調(diào)<將要開(kāi)始, 將要停止, 已經(jīng)完成>
//設(shè)置自定義解析成功時(shí)的響應(yīng)數(shù)據(jù)
api.decryptHandlerResponse = { (response: AnyObject) -> AnyObject in
// 自定義解析數(shù)據(jù)
}
//設(shè)置自定義請(qǐng)求成功時(shí)的緩存數(shù)據(jù)
api.cacheResponseBlock = { WXResponseModel -> WXDictionaryStrAny? in
//自定義緩存
}
requestTask = api.startRequest { [weak self] responseModel in
if responseModel.isSuccess {
self?.textView.text = responseModel.parseKeyPathModel?.description
} else {
self?.textView.text = responseModel.responseMsg
}
}
}
2.批量請(qǐng)求示例
func testBatchRequest() {
let url1 = "https://httpbin.org/get"
let api1 = WXRequestApi(url1, method: .get)
api1.autoCacheResponse = true
let url2 = "https://httpbin.org/delay/5"
let para2: [String : Any] = ["name" : "張三"]
let api2 = WXRequestApi(url2, method: .get, parameters: para2)
let api = WXBatchRequestApi(apiArray: [api1, api2], loadingTo: view)
api.startRequest({ [weak self] batchApi in
print("批量請(qǐng)求回調(diào)", batchApi.responseDataArray)
self?.textView.text = batchApi.responseForRequest(request: api1)?.responseDict?.description
}, waitAllDone: true)
}
3.Json請(qǐng)求解析模型示例
func testParseModel() {
let url = "http://app.u17.com/v3/appV3_3/ios/phone/comic/boutiqueListNew"
let param: [String : Any] = ["sexType" : 1]
let api = WXRequestApi(url, method: .get, parameters: param)
// api.debugJsonResponse = "http://10.8.41.162:8090/app/activity/page/detail/92546" //http( s ) test URL
// api.debugJsonResponse = "/Users/xinGe/Desktop/test.json" //Desktop json file
// api.debugJsonResponse = "test.json" //Bundle json file
// api.debugJsonResponse = ["code" : "1", "data" : ["message" : "測(cè)試字典"]] //Dictionary Object
// api.debugJsonResponse =
//"""
// {"data":{"message":"成功","stateCode":1,"returnData":{"galleryItems":[],"comicLists":[{"comics":[{"subTitle":"少年 搞笑","short_description":"突破次元壁的漫畫旦棉!","is_vip":4,"cornerInfo":"190","comicId":181616,"author_name":"壁水羽","cover":"https://cover-oss.u17i.com/2021/07/12647_1625125865_1za73F2a4fD1.sbig.jpg","description":"漫畫角色發(fā)現(xiàn)自己生活在一個(gè)漫畫的籠子里齿风,于是奮起反抗作者,面對(duì)角色的不配合绑洛,作者不得已要不斷更改題材救斑,恐怖,魔幻诊笤,勵(lì)志輪番上陣系谐,主角們要一一面對(duì),全力通關(guān)","name":"籠中人","tags":["少年","搞笑"]}],"comicType":6,"sortId":"86","newTitleIconUrl":"https://image.mylife.u17t.com/2017/07/10/1499657929_N7oo9pPOhaYH.png","argType":3,"argValue":8,"titleIconUrl":"https://image.mylife.u17t.com/2017/08/29/1503986106_7TY5gK000yjZ.png","itemTitle":"強(qiáng)力推薦作品","description":"更多","canedit":0,"argName":"topic"}],"textItems":[],"editTime":"0"}},"code":1}
//"""
api.timeOut = 40
api.loadingSuperView = view
api.autoCacheResponse = false
api.retryWhenFailTuple = (times: 3, delay: 1.0)
api.successStatusMap = (key: "code", value: "1")
// api.parseModelMap = (parseKey: "data.returnData.comicLists", modelType: ComicListModel.self)
requestTask = api.startRequest { [weak self] responseModel in
self?.textView.backgroundColor = .groupTableViewBackground
if let rspData = responseModel.responseObject as? Data {
if let image = UIImage(data: rspData) {
self?.textView.backgroundColor = .init(patternImage: image)
}
}
}
}
4.上傳文件示例
func testUploadFile() {
let url = "http://10.8.31.5:8090/uploadImage"
let param = [
"appName" : "TEST",
"platform" : "iOS",
"version" : "7.3.3",
]
let api = WXRequestApi(url, method: .post, parameters: param)
api.loadingSuperView = view
api.retryWhenFailTuple = (times: 3, delay: 3.0)
api.successStatusMap = (key: "code", value: "200")
let image = UIImage(named: "womenPic")!
let imageData = UIImagePNGRepresentation(image)
api.uploadFileDataArr = [imageData!]
api.uploadConfigDataBlock = { multipartFormData in
multipartFormData.append(imageData!, withName: "files", fileName: "womenPic.png", mimeType: "image/png")
}
api.fileProgressBlock = { progress in
let total = Float(progress.totalUnitCount)
let completed = Float(progress.completedUnitCount)
let percentage = completed / total * 100
print("上傳進(jìn)度: \(String(format:"%.2f",percentage)) %")
}
requestTask = api.uploadFile { [weak self] responseModel in
if responseModel.isSuccess {
self?.textView.backgroundColor = .init(patternImage: image)
}
}
}
5.下載文文件示例
func testDownloadFile() {
//壓縮包
var url = "http://i.gtimg.cn/qqshow/admindata/comdata/vipThemeNew_item_2135/2135_i_4_7_i_1.zip"
//視頻: (來(lái)源于: http://sp.jzsc.net)
url = "http://down1.jzsc.net//sp/video/2019-06-22/d4fe4a94-0c21-4c99-ad35-2bdb23ab4de9.mp4"
//圖片
url = "https://picsum.photos/375/667?random=1"
let api = WXRequestApi(url, method: .get, parameters: nil)
api.loadingSuperView = view
api.fileProgressBlock = { progress in
let total = Double(progress.totalUnitCount)
let completed = Double(progress.completedUnitCount)
let percentage = completed / total * 100.0
print("下載進(jìn)度: \(String(format:"%.2f",percentage)) %")
}
requestTask = api.downloadFile { [weak self] responseModel in
if let rspData = responseModel.responseObject as? Data {
if let image = UIImage(data: rspData) {
self?.textView.backgroundColor = .init(patternImage: image)
}
if var mimeType = responseModel.urlResponse?.mimeType {
mimeType = mimeType.replacingOccurrences(of: "/", with: ".")
let url = URL(fileURLWithPath: "/Users/xin610582/Desktop/" + mimeType, isDirectory: true)
try? rspData.write(to: url)
}
}
}
}