Swift 運用協議泛型封裝網絡層

swift 版本: 4.1

Xcode 版本 9.3 (9E145)

基于 AlamofireMoya 再封裝

代碼 Github 地址: MoyaDemo

一、前言

最近進入新公司開展新項目,我發現公司項目的網絡層很 OC ,最讓人無法忍受的是數據解析是在網絡層之外的,每一個數據模型都需要單獨寫解析代碼。趁著項目才開始,我提議由我寫一個網絡層小工具來代替以前的網絡層,順便把加載菊花,緩存也封裝到了里面。

二、Moya工具和Codable協議簡介

這里只是展示一下 Moya 的基本使用方法和 Codable協議 的基本知識,如果對這兩塊感興趣,讀者可以自行去搜索研究。

2.1 Moya工具

使用 Moya 是因為筆者覺得它很方便,如果讀者不想使用 Moya,也不影響你閱讀這篇文章的內容。

Alamofire 這里就不作介紹了,如果沒有接觸過,你可以把它當做是 Swift 版本的 AFNetworkingMoya 是一個對 Alamofire 進行了再次封裝的工具庫。如果只使用 Alamofire ,你的網絡請求可能會是這樣:

let url = URL(string: "your url")!
Alamofire.request(url).response { (response) in
    // handle response
}

當然讀者也會基于它進行二次封裝,不會僅僅是上面代碼那么簡單。

如果使用 Moya, 你首先做的不是直接請求,而是根據項目模塊建立一個個文件定義接口。例如我喜歡根據模塊的功能取名 模塊名 + 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
        }
    }
    
    // 這里只是帶參數的網絡請求
    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)
    }
    
    // 單元測試使用    
    var sampleData : Data {
        return Data()
    }
}

定義如上的文件后,你就可以使用如下方式進行網絡請求:

MoyaProvider<YourModuleAPI>().request(YourModuleAPI.yourAPI1) { (result) in
    // handle result            
}

2.2 Codable協議

Codable協議Swift4 才更新的,用來解析和編碼數據,它是由編碼協議和解碼協議組成。

public typealias Codable = Decodable & Encodable

Swift 更新 Codable協議 之前,筆者一直用的 SwiftyJSON 來解析網絡請求返回的數據。最近使用 Codable協議 后,發現還蠻好用的,就直接用上了。

不過 Codable協議 還是有一些坑點的,例如這篇文章所描述的:

When JSONDecoder meets the real world, things get ugly…

下面的 Person 模型類儲存了一個簡單的個人信息,這里只是使用了解碼,所以只遵守了 Decodable協議

struct Person: Decodable {
  var name: String
  var age: Int
}

StringInt 是系統默認的可編解碼類型,所以我們無需再寫其他代碼了,編譯器將默認為我們實現。

let jsonString = """
        {   "name": "swordjoy",
            "age": 99
        }
"""

if let data = jsonString.data(using: .utf8) {
    let decoder = JSONDecoder()
    if let person = try? decoder.decode(Person.self, from: data) {
        print(person.age)    // 99
        print(person.name)   // swordjoy
    }
}

只需要將 Person 類型傳給 JSONDecoder 對象,它就能直接將 JSON 數據轉換成 Person 數據模型對象。實際使用中由于解析規則的各種嚴格的限制,遠遠沒有上面看著這么簡單。

三、分析和解決方案

3.1.1 重復解析數據到模型

例如這里有兩個接口,一個是請求商品列表,一個是請求商城首頁。筆者以前是這樣寫的:

enum MallAPI {
    case getMallHome
    case getGoodsList
}
extension MallAPI: TargetType {
    // 略   
}
let mallProvider = MoyaProvider<MallAPI>()
mallProvider.request(MallAPI.getGoodsList) { (response) in
    // 將 response 解析成 Goods 模型數組用 success 閉包傳出去
}

mallProvider.request(MallAPI.getMallHome) { (response) in
    // 將 response 解析成 Home 模型用 success 閉包傳出去
}

以上是簡化的實用場景,每一個網絡請求都會單獨的寫一次將返回的數據解析成數據模型或者數據模型數組。就算是將數據解析的功能封裝成一個單例工具類,也僅僅是稍稍好了一些。

筆者想要的是指定數據模型類型后,網絡層直接返回解析完成后的數據模型供我們使用。

3.1.2 運用泛型來解決

泛型就是用來解決上面這種問題的,
使用泛型創建一個網絡工具類,并給定泛型的條件約束:遵守 Codable 協議。

struct NetworkManager<T> where T: Codable {
    
}

這樣我們在使用時,就可以指定需要解析的數據模型類型了。

NetworkManager<Home>().reqest...
NetworkManager<Goods>().reqest...

細心的讀者會發現這和 Moya 初始化 MoyaProvider 類的使用方式一樣。

3.2.1 使用Moya后,如何將加載控制器和緩存封裝到網絡層

由于使用了 Moya 進行再次封裝,每對代碼進行一次封裝的代價就是自由度的犧牲。如何將加載控制器&緩存功能和 Moya 契合起來呢?

一個很簡單的做法是在請求方法里添加是否顯示控制器和是否緩存布爾值參數。看著我的請求方法參數已經5,6個,這個方案立馬被排除了。看著 MoyaTargetType 協議,給了我靈感。

3.2.2 運用協議來解決

既然 MallAPI 能遵守 TargetType 來實現配置網絡請求信息,那當然也能遵守我們自己的協議來進行一些配置。

自定義一個 Moya 的補充協議

protocol MoyaAddable {
    var cacheKey: String? { get }
    var isShowHud: Bool { get }
}

這樣 MallAPI 就需要遵守兩個協議了

extension MallAPI: TargetType, MoyaAddable {
    // 略   
}

四、部分代碼展示和解析

完整的代碼,讀者可以到 Github 上去下載。

4.1 封裝后的網絡請求

通過給定需要返回的數據類型,返回的 response 可以直接調取 dataList 屬性獲取解析后的 Goods 數據模型數組。錯誤閉包里面也能直接通過 error.message 獲取報錯信息,然后根據業務需求選擇是否使用彈出框提示用戶。

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)
    }
}

4.2 返回數據的封裝

筆者公司服務端返回的數據結構大致如下:

{
    "code": 0,
    "msg": "成功",
    "data": {
        "hasMore": false,
        "list": []
    }
}

出于目前業務和解析數據的考慮,筆者將返回的數據類型封裝成了兩類,同時也將解析的操作放在了里面。

后面的請求方法也分成了兩個,這不是必要的,讀者可以根據自己的業務和喜好選擇。

  • 請求列表接口返回的數據
  • 請求普通接口返回的數據
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? { ... } // 解析
}

這樣我們直接返回相應的封裝類對象就能獲取解析后的數據了。

4.3 錯誤的封裝

網絡請求過程中,肯定有各種各樣的錯誤,這里使用了 Swift 語言的錯誤機制。

// 網絡錯誤處理枚舉
public enum NetworkError: Error  {
    // 略...
    // 服務器返回的錯誤
    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
        }
    }
}

這里的擴展很重要,它能幫我們在處理錯誤時獲取錯誤的 messagecode.

4.4 請求網絡方法

最終請求的方法

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 定義的接口,指定了必須同時遵守 TargetTypeMoyaAddable 協議,其余的都是常規操作了。
和封裝的返回數據一樣,這里也分了普通接口和列表接口。

@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)
}

我綜合目前項目和 Codable 協議的坑點考慮,將這里寫得有點死板,萬一來個既是列表又有其他數據的就不適用了。不過到時候可以添加一個類似這種方法,將數據傳出去處理。

// Demo里沒有這個方法
func requestCustom<R: TargetType & MoyaAddable>(
    _ type: R,
    test: Bool = false,
    completion: (Response) -> ()) -> Cancellable? 
{
    // 略
}

4.5 緩存和加載控制器

想到添加 MoyaAddable 協議后,其他就沒什么困難的了,直接根據 type 獲取接口定義文件中的配置做出相應的操作就行了。

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 接口請求中的兩個功能

  • 請求返回數據后會通過給定的緩存 Key 進行緩存
  • 網絡請求過程中自動顯示和隱藏加載控制器。

如果讀者的加載控制器有不同的樣式,還可以添加一個加載控制器樣式的屬性。甚至緩存的方式是同步還是異步,都可以通過這個 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 自帶的插件工具。

// 創建moya請求類
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
}

4.6 避免重復請求

定義一個數組來保存網絡請求的信息,一個并行隊列使用 barrier 函數來保證數組元素添加和移除線程安全。

// 用來處理只請求一次的柵欄隊列
private let barrierQueue = DispatchQueue(label: "cn.tsingho.qingyun.NetworkManager",
attributes: .concurrent)
// 用來處理只請求一次的數組,保存請求的信息 唯一
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:
            // 不會調用
            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:
            // 不會調用
            ()
    }
}

這種實現方式目前有一個小問題,多個界面使用同一接口,并且參數也相同的話,只會請求一次,不過這種情況還是極少的,暫時沒遇到就沒有處理。

五、后記

目前封裝的這個網絡層代碼有點強業務類型,畢竟我的初衷就是給自己公司項目重新寫一個網絡層,因此可能不適用于某些情況。不過這里使用泛型和協議的方法是通用的,讀者可以使用同樣的方式實現匹配自己項目的網絡層。如果讀者有更好的建議,還希望評論出來一起討論。

轉載評論留轉載地址即可轉載。
文章掘金地址

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容

  • 國家電網公司企業標準(Q/GDW)- 面向對象的用電信息數據交換協議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 11,043評論 6 13
  • 1.ios高性能編程 (1).內層 最小的內層平均值和峰值(2).耗電量 高效的算法和數據結構(3).初始化時...
    歐辰_OSR閱讀 29,464評論 8 265
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,596評論 25 707
  • 有了與春天相遇的驚喜,必定有與它告別的傷心。所謂懷春,懷的即是春天,又是少年。 韓愈《春雪》詩說,新年都未有芳華,...
    熱水c閱讀 206評論 0 1
  • 1、適當有氧運動,一定不可以做劇烈的運動,以免關節過勞。最好選擇游泳、太極拳、乒乓球之類的有氧運動,以此減輕體重。...
    Fun_Day閱讀 228評論 0 0