一疏遏、前言
最近進(jìn)入新公司開展新項(xiàng)目,我發(fā)現(xiàn)公司項(xiàng)目的網(wǎng)絡(luò)層很 OC 戳寸,最讓人無法忍受的是數(shù)據(jù)解析是在網(wǎng)絡(luò)層之外的疫鹊,每一個(gè)數(shù)據(jù)模型都需要單獨(dú)寫解析代碼拆吆。趁著項(xiàng)目才開始锈拨,給大家講解Swift 運(yùn)用協(xié)議泛型封裝網(wǎng)絡(luò)層
(其實(shí)做為一個(gè)開發(fā)者奕枢,有一個(gè)學(xué)習(xí)的氛圍跟一個(gè)交流圈子特別重要缝彬,這是一個(gè)我的iOS學(xué)習(xí)交流群783941081添寺,不管你是小白還是大牛歡迎入駐活尊,大家一起交流學(xué)習(xí))
二墩邀、Moya工具和Codable協(xié)議簡(jiǎn)介
這里只是展示一下 Moya 的基本使用方法和 Codable協(xié)議 的基本知識(shí)荔茬,如果對(duì)這兩塊感興趣慕蔚,讀者可以自行去搜索研究孔飒。
- Moya工具
在OC中,我們使用AFNetworking來進(jìn)行網(wǎng)絡(luò)請(qǐng)求园细,簡(jiǎn)潔方便狮崩。在swift中睦柴,我們使用Moya來進(jìn)行網(wǎng)絡(luò)請(qǐng)求坦敌,Moya封裝了Alamofire狱窘,可以更加方便的進(jìn)行網(wǎng)絡(luò)請(qǐng)求躬络。初次使用Moya,還是覺得稍稍有些不習(xí)慣馁菜。在這里,記錄下使用過程德撬。
let url = URL(string: "your url")!
Alamofire.request(url).response { (response) in
// handle response
}
當(dāng)然讀者也會(huì)基于它進(jìn)行二次封裝,不會(huì)僅僅是上面代碼那么簡(jiǎn)單躲胳。
如果使用 Moya, 你首先做的不是直接請(qǐng)求蜓洪,而是根據(jù)項(xiàng)目模塊建立一個(gè)個(gè)文件定義接口。例如我喜歡根據(jù)模塊的功能取名 模塊名 + API坯苹,然后再在其中定義我們需要使用的接口隆檀,例:
import Foundation
import Moya
enum YourModuleAPI {
case yourAPI1
case yourAPI2(parameter: String)
}
extension YourModuleAPI: TargetType {
var baseURL : URL {
return URL(string: "your base url")!
}
var headers : [String : String]? {
return "your header"
}
var path: String {
switch self {
case .yourAPI1:
return "yourAPI1 path"
case .yourAPI2:
return "yourAPI2 path"
}
}
var method: Moya.Method {
switch self {
case .yourAPI1:
return .post
default:
return .get
}
}
// 這里只是帶參數(shù)的網(wǎng)絡(luò)請(qǐng)求
var task: Task {
var parameters: [String: Any] = [:]
switch self {
case let .yourAPI1:
parameters = [:]
case let .yourAPI2(parameter):
parameters = ["字段":parameter]
}
return .requestParameters(parameters: parameters,
encoding: URLEncoding.default)
}
// 單元測(cè)試使用
var sampleData : Data {
return Data()
}
}
定義如上的文件后,你就可以使用如下方式進(jìn)行網(wǎng)絡(luò)請(qǐng)求:
MoyaProvider<YourModuleAPI>().request(YourModuleAPI.yourAPI1) { (result) in
// handle result
}
- Codable協(xié)議
自Swift4發(fā)布以來已有一段時(shí)間了粹湃,各種新特性為我們提供更加高效的開發(fā)效率恐仑,其中在Swift4中使用Codable協(xié)議進(jìn)行模型與json數(shù)據(jù)之間的映射提供更加便利的方式。在Swift3中孤钦,對(duì)于從服務(wù)器獲取到的json數(shù)據(jù)后觉鼻,我們要進(jìn)行一系列繁瑣的操作才能將數(shù)據(jù)完整的轉(zhuǎn)化成模型仇矾,舉個(gè)??粗合,我們從服務(wù)器獲取了一段這樣的json數(shù)據(jù):
{
"student": {
"name": "Jone",
"age": 18,
"finger": {
"count": 10
}
}
}
然后我們用JSONSerialization來解析數(shù)據(jù)供屉,得到的是一個(gè)Any類型哗魂。當(dāng)我們要讀取count時(shí)就要采取以下操作:
let json = try! JSONSerialization.jsonObject(with: data, options: [])
if let jsonDic = json as? [String:Any] {
if let student = jsonDic["student"] as? [String:Any] {
if let finger = student["finger"] as? [String:Int] {
if let count = finger["count"] {
print(count)
}
}
}
}
在日常用Swift編寫代碼時(shí),就我而言,我喜歡使用SwiftyJSON或則ObjectMapper來進(jìn)行json轉(zhuǎn)模型,因?yàn)橄啾仍模褂眠@些第三方會(huì)給我們帶來更高的效率。于是在Swift4中,Apple官方就此提供了自己的方法,現(xiàn)在我們來了解其基本的用法。
三、Codable的簡(jiǎn)單使用
首先用狱,我們來對(duì)最簡(jiǎn)單的json數(shù)據(jù)進(jìn)行轉(zhuǎn)模型,現(xiàn)在我們有以下一組json數(shù)據(jù):
let res = """
{
"name": "Jone",
"age": 17
}
"""
let data = res.data(using: .utf8)!
然后我們定義一個(gè)Student結(jié)構(gòu)體作為數(shù)據(jù)的模型,并遵守Codable協(xié)議:
struct Student: Codable {
let name: String
let age: Int
}
而關(guān)于Codable協(xié)議的描述我們可以點(diǎn)進(jìn)去看一下:
public typealias Codable = Decodable & Encodable
public protocol Encodable {
public func encode(to encoder: Encoder) throws
}
public protocol Decodable {
public init(from decoder: Decoder) throws
}
其實(shí)就是遵守一個(gè)關(guān)于解碼的協(xié)議和一個(gè)關(guān)于編碼的協(xié)議埠偿,只要遵守這些協(xié)議才能進(jìn)行json與模型之間的編碼與解碼抖剿。
接下來我們就可以進(jìn)行講json解碼并映射成模型:
let decoder = JSONDecoder()
let stu = try! decoder.decode(Student.self, from: data)
print(stu) //Student(name: "Jone", age: 17)
然后甥温,我們可以將剛才得到的模型進(jìn)行編碼成json:
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted //輸出格式好看點(diǎn)
let data = try! encoder.encode(stu)
print(String(data: data, encoding: .utf8)!)
//{
// "name" : "Jone",
// "age" : 17
//}
就是這么簡(jiǎn)單~~~
這里對(duì)encode和decode使用try!是為了減少文章篇幅乃秀,正常使用時(shí)要對(duì)錯(cuò)誤進(jìn)行處理超凳,而常見的錯(cuò)誤會(huì)在第三篇講到
四创夜、分析和解決方案
4.1.1重復(fù)解析數(shù)據(jù)到模型
例如這里有兩個(gè)接口,一個(gè)是請(qǐng)求商品列表虫蝶,一個(gè)是請(qǐng)求商城首頁蝙泼。筆者以前是這樣寫的:
enum MallAPI {
case getMallHome
case getGoodsList
}
extension MallAPI: TargetType {
// 略
}
let mallProvider = MoyaProvider<MallAPI>()
mallProvider.request(MallAPI.getGoodsList) { (response) in
// 將 response 解析成 Goods 模型數(shù)組用 success 閉包傳出去
}
mallProvider.request(MallAPI.getMallHome) { (response) in
// 將 response 解析成 Home 模型用 success 閉包傳出去
}
以上是簡(jiǎn)化的實(shí)用場(chǎng)景才避,每一個(gè)網(wǎng)絡(luò)請(qǐng)求都會(huì)單獨(dú)的寫一次將返回的數(shù)據(jù)解析成數(shù)據(jù)模型或者數(shù)據(jù)模型數(shù)組舆驶。就算是將數(shù)據(jù)解析的功能封裝成一個(gè)單例工具類撬陵,也僅僅是稍稍好了一些蟋定。
筆者想要的是指定數(shù)據(jù)模型類型后抄淑,網(wǎng)絡(luò)層直接返回解析完成后的數(shù)據(jù)模型供我們使用郑原。
41..2 運(yùn)用泛型來解決
泛型就是用來解決上面這種問題的,
使用泛型創(chuàng)建一個(gè)網(wǎng)絡(luò)工具類,并給定泛型的條件約束:遵守 Codable 協(xié)議。
struct NetworkManager<T> where T: Codable {
}
這樣我們?cè)谑褂脮r(shí)逢捺,就可以指定需要解析的數(shù)據(jù)模型類型了谁鳍。
NetworkManager<Home>().reqest...
NetworkManager<Goods>().reqest...
細(xì)心的讀者會(huì)發(fā)現(xiàn)這和 Moya 初始化 MoyaProvider 類的使用方式一樣。
4.1.3使用Moya后劫瞳,如何將加載控制器和緩存封裝到網(wǎng)絡(luò)層
由于使用了 Moya 進(jìn)行再次封裝倘潜,每對(duì)代碼進(jìn)行一次封裝的代價(jià)就是自由度的犧牲。如何將加載控制器&緩存功能和 Moya 契合起來呢?
一個(gè)很簡(jiǎn)單的做法是在請(qǐng)求方法里添加是否顯示控制器和是否緩存布爾值參數(shù)志于′桃颍看著我的請(qǐng)求方法參數(shù)已經(jīng)5,6個(gè)恨憎,這個(gè)方案立馬被排除了蕊退。看著 Moya 的 TargetType 協(xié)議憔恳,給了我靈感瓤荔。
4.2.1 運(yùn)用協(xié)議來解決
既然 MallAPI 能遵守 TargetType 來實(shí)現(xiàn)配置網(wǎng)絡(luò)請(qǐng)求信息,那當(dāng)然也能遵守我們自己的協(xié)議來進(jìn)行一些配置钥组。
自定義一個(gè) Moya 的補(bǔ)充協(xié)議
protocol MoyaAddable {
var cacheKey: String? { get }
var isShowHud: Bool { get }
}
這樣 MallAPI 就需要遵守兩個(gè)協(xié)議了
extension MallAPI: TargetType, MoyaAddable {
// 略
}
五输硝、部分代碼展示和解析
完整的代碼,讀者可以到 Github 上去下載程梦。
5.1 封裝后的網(wǎng)絡(luò)請(qǐng)求
通過給定需要返回的數(shù)據(jù)類型点把,返回的 response 可以直接調(diào)取 dataList 屬性獲取解析后的 Goods 數(shù)據(jù)模型數(shù)組。錯(cuò)誤閉包里面也能直接通過 error.message 獲取報(bào)錯(cuò)信息屿附,然后根據(jù)業(yè)務(wù)需求選擇是否使用彈出框提示用戶郎逃。
NetworkManager<Goods>().requestListModel(MallAPI.getOrderList,
completion: { (response) in
let list = response?.dataList
let page = response?.page
}) { (error) in
if let msg = error.message else {
print(msg)
}
}
5.2 返回?cái)?shù)據(jù)的封裝
筆者公司服務(wù)端返回的數(shù)據(jù)結(jié)構(gòu)大致如下:
{
"code": 0,
"msg": "成功",
"data": {
"hasMore": false,
"list": []
}
}
出于目前業(yè)務(wù)和解析數(shù)據(jù)的考慮,筆者將返回的數(shù)據(jù)類型封裝成了兩類,同時(shí)也將解析的操作放在了里面挺份。
后面的請(qǐng)求方法也分成了兩個(gè)褒翰,這不是必要的,讀者可以根據(jù)自己的業(yè)務(wù)和喜好選擇匀泊。
請(qǐng)求列表接口返回的數(shù)據(jù)
請(qǐng)求普通接口返回的數(shù)據(jù)
class BaseResponse {
var code: Int { ... } // 解析
var message: String? { ... } // 解析
var jsonData: Any? { ... } // 解析
let json: [String : Any]
init?(data: Any) {
guard let temp = data as? [String : Any] else {
return nil
}
self.json = temp
}
func json2Data(_ object: Any) -> Data? {
return try? JSONSerialization.data(
withJSONObject: object,
options: [])
}
}
class ListResponse<T>: BaseResponse where T: Codable {
var dataList: [T]? { ... } // 解析
var page: PageModel? { ... } // 解析
}
class ModelResponse<T>: BaseResponse where T: Codable {
var data: T? { ... } // 解析
}
這樣我們直接返回相應(yīng)的封裝類對(duì)象就能獲取解析后的數(shù)據(jù)了优训。
5.3 錯(cuò)誤的封裝
網(wǎng)絡(luò)請(qǐng)求過程中,肯定有各種各樣的錯(cuò)誤各聘,這里使用了 Swift 語言的錯(cuò)誤機(jī)制揣非。
// 網(wǎng)絡(luò)錯(cuò)誤處理枚舉
public enum NetworkError: Error {
// 略...
// 服務(wù)器返回的錯(cuò)誤
case serverResponse(message: String?, code: Int)
}
extension NetworkError {
var message: String? {
switch self {
case let .serverResponse(msg, _): return msg
default: return nil
}
}
var code: Int {
switch self {
case let .serverResponse(_, code): return code
default: return -1
}
}
}
這里的擴(kuò)展很重要,它能幫我們?cè)谔幚礤e(cuò)誤時(shí)獲取錯(cuò)誤的 message 和 code.
5.4 請(qǐng)求網(wǎng)絡(luò)方法
最終請(qǐng)求的方法
private func request<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
progressBlock: ((Double) -> ())? = nil,
modelCompletion: ((ModelResponse<T>?) -> ())? = nil,
modelListCompletion: ((ListResponse<T>?) -> () )? = nil,
error: @escaping (NetworkError) -> () )
-> Cancellable?
{}
這里的 R 泛型是用來獲取 Moya 定義的接口躲因,指定了必須同時(shí)遵守 TargetType 和 MoyaAddable 協(xié)議早敬,其余的都是常規(guī)操作了忌傻。
和封裝的返回?cái)?shù)據(jù)一樣,這里也分了普通接口和列表接口搞监。
@discardableResult
func requestModel<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
progressBlock: ((Double) -> ())? = nil,
completion: @escaping ((ModelResponse<T>?) -> ()),
error: @escaping (NetworkError) -> () )
-> Cancellable?
{
return request(type,
test: test,
progressBlock: progressBlock,
modelCompletion: completion,
error: error)
}
@discardableResult
func requestListModel<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
completion: @escaping ((ListResponse<T>?) -> ()),
error: @escaping (NetworkError) -> () )
-> Cancellable?
{
return request(type,
test: test,
modelListCompletion: completion,
error: error)
}
我綜合目前項(xiàng)目和 Codable 協(xié)議的坑點(diǎn)考慮,將這里寫得有點(diǎn)死板芯勘,萬一來個(gè)既是列表又有其他數(shù)據(jù)的就不適用了。不過到時(shí)候可以添加一個(gè)類似這種方法腺逛,將數(shù)據(jù)傳出去處理。
// Demo里沒有這個(gè)方法
func requestCustom<R: TargetType & MoyaAddable>(
_ type: R,
test: Bool = false,
completion: (Response) -> ()) -> Cancellable?
{
// 略
}
5.5 緩存和加載控制器
想到添加 MoyaAddable 協(xié)議后衡怀,其他就沒什么困難的了棍矛,直接根據(jù) type 獲取接口定義文件中的配置做出相應(yīng)的操作就行了。
var cacheKey: String? {
switch self {
case .getGoodsList:
return "cache goods key"
default:
return nil
}
}
var isShowHud: Bool {
switch self {
case .getGoodsList:
return true
default:
return false
}
}
這就添加了 getGoodsList 接口請(qǐng)求中的兩個(gè)功能
請(qǐng)求返回?cái)?shù)據(jù)后會(huì)通過給定的緩存 Key 進(jìn)行緩存
網(wǎng)絡(luò)請(qǐng)求過程中自動(dòng)顯示和隱藏加載控制器抛杨。
如果讀者的加載控制器有不同的樣式够委,還可以添加一個(gè)加載控制器樣式的屬性。甚至緩存的方式是同步還是異步怖现,都可以通過這個(gè) MoyaAddable 添加茁帽。
// 緩存
private func cacheData<R: TargetType & MoyaAddable>(
_ type: R,
modelCompletion: ((Response<T>?) -> ())? = nil,
modelListCompletion: ( (ListResponse<T>?) -> () )? = nil,
model: (Response<T>?, ListResponse<T>?))
{
guard let cacheKey = type.cacheKey else {
return
}
if modelComletion != nil, let temp = model.0 {
// 緩存
}
if modelListComletion != nil, let temp = model.1 {
// 緩存
}
}
加載控制器的顯示和隱藏使用的是 Moya 自帶的插件工具。
// 創(chuàng)建moya請(qǐng)求類
private func createProvider<T: TargetType & MoyaAddable>(
type: T,
test: Bool)
-> MoyaProvider<T>
{
let activityPlugin = NetworkActivityPlugin { (state, targetType) in
switch state {
case .began:
DispatchQueue.main.async {
if type.isShowHud {
SVProgressHUD.showLoading()
}
self.startStatusNetworkActivity()
}
case .ended:
DispatchQueue.main.async {
if type.isShowHud {
SVProgressHUD.dismiss()
}
self.stopStatusNetworkActivity()
}
}
}
let provider = MoyaProvider<T>(
plugins: [activityPlugin,
NetworkLoggerPlugin(verbose: false)])
return provider
}
5.6 避免重復(fù)請(qǐng)求
定義一個(gè)數(shù)組來保存網(wǎng)絡(luò)請(qǐng)求的信息屈嗤,一個(gè)并行隊(duì)列使用 barrier 函數(shù)來保證數(shù)組元素添加和移除線程安全潘拨。
// 用來處理只請(qǐng)求一次的柵欄隊(duì)列
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// 用來處理只請(qǐng)求一次的數(shù)組,保存請(qǐng)求的信息 唯一
private var fetchRequestKeys = [String]()
private func isSameRequest<R: TargetType & MoyaAddable>(_ type: R) -> Bool {
switch type.task {
case let .requestParameters(parameters, _):
let key = type.path + parameters.description
var result: Bool!
barrierQueue.sync(flags: .barrier) {
result = fetchRequestKeys.contains(key)
if !result {
fetchRequestKeys.append(key)
}
}
return result
default:
// 不會(huì)調(diào)用
return false
}
}
private func cleanRequest<R: TargetType & MoyaAddable>(_ type: R) {
switch type.task {
case let .requestParameters(parameters, _):
let key = type.path + parameters.description
barrierQueue.sync(flags: .barrier) {
fetchRequestKeys.remove(key)
}
default:
// 不會(huì)調(diào)用
()
}
}
這種實(shí)現(xiàn)方式目前有一個(gè)小問題,多個(gè)界面使用同一接口饶号,并且參數(shù)也相同的話铁追,只會(huì)請(qǐng)求一次,不過這種情況還是極少的茫船,暫時(shí)沒遇到就沒有處理琅束。
六、后記
目前封裝的這個(gè)網(wǎng)絡(luò)層代碼有點(diǎn)強(qiáng)業(yè)務(wù)類型算谈,畢竟我的初衷就是給自己公司項(xiàng)目重新寫一個(gè)網(wǎng)絡(luò)層涩禀,因此可能不適用于某些情況。不過這里使用泛型和協(xié)議的方法是通用的然眼,讀者可以使用同樣的方式實(shí)現(xiàn)匹配自己項(xiàng)目的網(wǎng)絡(luò)層艾船。如果讀者有更好的建議,還希望評(píng)論出來一起討論罪治。