Alamofire源碼解讀系列(五)之結(jié)果封裝(Result)

本篇講解Result的封裝

前言

有時(shí)候,我們會(huì)根據(jù)現(xiàn)實(shí)中的事物來對(duì)程序中的某個(gè)業(yè)務(wù)關(guān)系進(jìn)行抽象胜茧,這句話很難理解垢揩。在Alamofire中闹获,使用Response來描述請(qǐng)求后的結(jié)果。我們都知道Alamofire返回的數(shù)據(jù)可以經(jīng)過特殊的處理颜懊,比如說序列化财岔,那么我們應(yīng)該如何在Response中獲取到這些類型不同的數(shù)據(jù)呢?

假如說序列化后的數(shù)據(jù)是data河爹,最直接的想法就是把data設(shè)置為Any類型匠璧,在實(shí)際用到的時(shí)候在進(jìn)行判斷,這也是最普通的一種開發(fā)思維∠陶猓現(xiàn)在我們就要打破這種思維夷恍。我們需要封裝一個(gè)對(duì)象,這個(gè)對(duì)象能夠表達(dá)任何結(jié)果媳维,這就用到了swift中的泛型酿雪。

接下來在講解Result之后,會(huì)給出兩個(gè)使用泛型的例子侄刽,第一個(gè)例子表達(dá)基本的網(wǎng)絡(luò)封裝思想指黎,第二個(gè)表達(dá)基本的viewModel思想。

Result

/// Used to represent whether a request was successful or encountered an error.
///
/// - success: The request and all post processing operations were successful resulting in the serialization of the
///            provided associated value.
///
/// - failure: The request encountered an error resulting in a failure. The associated values are the original data
///            provided by the server as well as the error that caused the failure.
public enum Result<Value> {
    case success(Value)
    case failure(Error)

}

關(guān)于如何描述結(jié)果,有兩種可能州丹,不是成功就是失敗醋安,因此考慮使用枚舉杂彭。在Alamofire源碼解讀系列(二)之錯(cuò)誤處理(AFError)這篇文章中我已經(jīng)詳細(xì)的講解了枚舉的使用方法。在上邊的代碼中吓揪,對(duì)枚舉的每個(gè)子選項(xiàng)都做了值關(guān)聯(lián)亲怠。

大家注意,泛型的寫法是類似這樣的:<T>柠辞,在<和>之間聲明一種類型团秽,這個(gè)T知識(shí)象征性的,在賦值的時(shí)候钾腺,可以是任何類型徙垫。還有一種用法,看下邊的代碼:

struct CellConfigurator<Cell> where Cell: Updatable, Cell: UITableViewCell {
}

上邊代碼中的Cell必須符合后邊給出的兩個(gè)條件才行放棒,這種用法是給泛型增加了條件限制姻报,這種用法還有另外一種方式,看下邊的代碼:

 func send<T: Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void);

其實(shí)道理都差不多间螟,都屬于對(duì)泛型的靈活運(yùn)用吴旋。

我們接著看看在Alamofire中是如何使用Result的。

  @discardableResult
    public func responseJSON(
        queue: DispatchQueue? = nil,
        options: JSONSerialization.ReadingOptions = .allowFragments,
        completionHandler: @escaping (DataResponse<Any>) -> Void)
        -> Self
    {
        return response(
            queue: queue,
            responseSerializer: DataRequest.jsonResponseSerializer(options: options),
            completionHandler: completionHandler
        )
    }

上邊的這個(gè)函數(shù)的主要目的是把請(qǐng)求成功后的結(jié)果序列化為JSON,completionHandler函數(shù)的參數(shù)類型為DataResponse<Any>厢破,其中的Any就會(huì)傳遞給Result荣瑟,也就是Result<Any>。

那么問題來了摩泪,不是把數(shù)據(jù)解析成JSON了嗎笆焰?為什么要返回Any類型呢?json本質(zhì)上很類似于JavaScript中的對(duì)象和數(shù)組见坑。JSONSerialization.jsonObject返回的類型是Any嚷掠,這是因?yàn)榻馕龊蟮臄?shù)據(jù)有可能是數(shù)組,也有可能是字典荞驴。

字典:

{
    "people":[
        {"firstName":"Brett","lastName":"McLaughlin","email":"aaaa"},
        {"firstName":"Jason","lastName":"Hunter","email":"bbbb"},
        {"firstName":"Elliotte","lastName":"Harold","email":"cccc"}
    ]
}

數(shù)組:

[
    "a",
    "b",
    "c"
]

當(dāng)然如果不是這兩種格式的數(shù)據(jù)不皆,使用JSONSerialization.jsonObject解析會(huì)拋出異常。

到這里我們就大概對(duì)這個(gè)Result有了一定的了解熊楼,下邊的代碼給result添加了一些屬性霹娄,主要目的是使用起來更方便:

   /// Returns `true` if the result is a success, `false` otherwise.
    public var isSuccess: Bool {
        switch self {
        case .success:
            return true
        case .failure:
            return false
        }
    }

    /// Returns `true` if the result is a failure, `false` otherwise.
    public var isFailure: Bool {
        return !isSuccess
    }

    /// Returns the associated value if the result is a success, `nil` otherwise.
    public var value: Value? {
        switch self {
        case .success(let value):
            return value
        case .failure:
            return nil
        }
    }

    /// Returns the associated error value if the result is a failure, `nil` otherwise.
    public var error: Error? {
        switch self {
        case .success:
            return nil
        case .failure(let error):
            return error
        }
    }

當(dāng)然,為了打印更加詳細(xì)的信息鲫骗,使Result實(shí)現(xiàn)了CustomStringConvertibleCustomDebugStringConvertible協(xié)議 :

// MARK: - CustomStringConvertible

extension Result: CustomStringConvertible {
    /// The textual representation used when written to an output stream, which includes whether the result was a
    /// success or failure.
    public var description: String {
        switch self {
        case .success:
            return "SUCCESS"
        case .failure:
            return "FAILURE"
        }
    }
}

// MARK: - CustomDebugStringConvertible

extension Result: CustomDebugStringConvertible {
    /// The debug textual representation used when written to an output stream, which includes whether the result was a
    /// success or failure in addition to the value or error.
    public var debugDescription: String {
        switch self {
        case .success(let value):
            return "SUCCESS: \(value)"
        case .failure(let error):
            return "FAILURE: \(error)"
        }
    }
}

總起來說犬耻,Result是一個(gè)比較簡(jiǎn)單的封裝。

基于泛型的網(wǎng)絡(luò)封裝

在實(shí)際的開發(fā)工作中挎峦,我們使用Alamofire發(fā)送請(qǐng)求香追,獲取服務(wù)器的數(shù)據(jù),往往會(huì)對(duì)其進(jìn)行二次封裝坦胶,在這里透典,我講解一個(gè)封裝的例子晴楔,內(nèi)容來自面向協(xié)議編程與 Cocoa 的邂逅

  1. 我們需要一個(gè)協(xié)議,這個(gè)協(xié)議提供一個(gè)函數(shù)峭咒,目的是把Data轉(zhuǎn)換成實(shí)現(xiàn)該協(xié)議的對(duì)象本身税弃。注意我們?cè)谶@時(shí)候是不知道這個(gè)對(duì)象的類型的,為了適配更多的類型凑队,這個(gè)對(duì)象暫時(shí)設(shè)計(jì)為泛型则果,因此協(xié)議中的函數(shù)應(yīng)該是靜態(tài)函數(shù)

     protocol Decodable {
         static func parse(data: Data) -> Self?
     }
    
  2. 封裝請(qǐng)求,同樣采用協(xié)議的方式

     public enum JZGHTTPMethod: String {
         case options = "OPTIONS"
         case get     = "GET"
         case head    = "HEAD"
         case post    = "POST"
         case put     = "PUT"
         case patch   = "PATCH"
         case delete  = "DELETE"
         case trace   = "TRACE"
         case connect = "CONNECT"
         
     }
     
     protocol Request {
     
         var path: String { get }
         var privateHost: String? { get }
         
         var HTTPMethod: JZGHTTPMethod { get }
         var timeoutInterval: TimeInterval { get }
         var parameter: [String: Any]? { get }
         
         associatedtype Response: Decodable
     }
    
  3. 封裝發(fā)送端漩氨,同樣采用協(xié)議的方式

     protocol Client {
         
         var host: String { get }
         
         func send<T: Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void);
     }
    
  4. 只要是實(shí)現(xiàn)了Client協(xié)議的對(duì)象西壮,就有能力發(fā)送請(qǐng)求,在這里Alamofire是作為中間層存在的叫惊,只提供請(qǐng)求能力款青,可以隨意換成其他的中間能力層

     struct AlamofireClient: Client {
         
         public static let `default` = { AlamofireClient() }()
    
         public enum HostType: String {
             case sandbox = "https://httpbin.org/post"
         }
         
         /// Base host URL
         var host: String = HostType.sandbox.rawValue
         
         func send<T : Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void) {
             
             let url = URL(string: r.privateHost ?? host.appending(r.path))!
             
             let sessionManager = Alamofire.SessionManager.default
             sessionManager.session.configuration.timeoutIntervalForRequest = r.timeoutInterval
             
             Alamofire.request(url, method: HTTPMethod(rawValue: r.HTTPMethod.rawValue)!,
                               parameters: r.parameter,
                               encoding: URLEncoding.default,
                               headers: nil)
                 .response { (response) in
                     
                     if let data = response.data, let res = T.Response.parse(data: data) {
                         
                         handler(res, nil)
                         
                     }else {
                         
                         handler(nil, response.error?.localizedDescription)
                     }
             }
         }
     
     }
    

封裝完成之后,我們來使用一下上邊封裝的功能:

  1. 創(chuàng)建一個(gè)TestRequest.swift文件霍狰,內(nèi)部代碼為:

     struct TestRequest: Request {
         let name: String
         let userId: String
         
         var path: String {
             return ""
         }
         
         var privateHost: String? {
             return nil
         }
         
         var timeoutInterval: TimeInterval {
             return 20.0
         }
         
         var HTTPMethod: JZGHTTPMethod {
             return .post
         }
         
         var parameter: [String : Any]? {
             return ["name" : name,
                     "userId" : userId]
         }
         
         typealias Response = TestResult
     }
    
  2. 創(chuàng)建TestResult.swift文件抡草,內(nèi)部代碼為:

     struct TestResult {
         var origin: String
     }
     
     extension TestResult: Decodable {
         static func parse(data: Data) -> TestResult? {
             do {
                let dic = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
                 
                 guard let dict = dic as? Dictionary<String, Any> else {
                     return nil
                 }
                 return TestResult(origin: dict["origin"] as! String)
             }catch {
                 return nil
             }
         }
     }
    
  3. 發(fā)送請(qǐng)求

     let request = TestRequest(name: "mama", userId: "12345");
     AlamofireClient.default.send(request) { (response, error) in
         print(response)
     }
    

對(duì)網(wǎng)絡(luò)的基本封裝就到此為止了 ,這里的Result可以是任何類型的對(duì)象蔗坯,比如說User康震,可以通過上邊的方法,直接解析成User對(duì)象宾濒。

基于泛型的cell封裝

這種設(shè)計(jì)通常應(yīng)用在MVVM之中腿短,我們看下邊的代碼:

  1. 定義一個(gè)協(xié)議,這個(gè)協(xié)議提供一個(gè)函數(shù)绘梦,函數(shù)會(huì)提供一個(gè)參數(shù)答姥,這個(gè)參數(shù)就是viewModel。cell只要實(shí)現(xiàn)了這個(gè)協(xié)議谚咬,就能夠通過這個(gè)參數(shù)拿到viewModel,然后根據(jù)viewModel來配置自身控件的屬性尚粘。

     protocol Updatable: class {
     
         associatedtype ViewData
     
         func update(viewData: ViewData)
     }
    
  2. 再定義一個(gè)協(xié)議择卦,這個(gè)協(xié)議需要表示cell的一些信息,比如reuseIdentifier郎嫁,cellClass秉继,同時(shí),這個(gè)協(xié)議還需要提供一個(gè)方法泽铛,賦予cell適配器更新cell的能力

     protocol CellConfiguratorType {
     
         var reuseIdentifier: String { get }
         var cellClass: AnyClass { get }
     
         func update(cell: UITableViewCell)
     }
    
  3. 創(chuàng)建CellConfigurator尚辑,這個(gè)CellConfigurator必須綁定一個(gè)viewData,這個(gè)viewData通過Updatable協(xié)議中的方法傳遞給cell

     struct CellConfigurator<Cell> where Cell: Updatable, Cell: UITableViewCell {
     
         let viewData: Cell.ViewData
         let reuseIdentifier: String = NSStringFromClass(Cell.self)
         let cellClass: AnyClass = Cell.self
     
         func update(cell: UITableViewCell) {
             if let cell = cell as? Cell {
                 cell.update(viewData: viewData)
             }
         }
     }
    

萬變不離其宗啊盔腔,我們?cè)谡?qǐng)求到數(shù)據(jù)之后杠茬,需要把數(shù)據(jù)轉(zhuǎn)變成CellConfigurator月褥,也就是在數(shù)組中存放的是CellConfigurator類型的數(shù)據(jù)。

看看使用示例:

  1. 創(chuàng)建數(shù)組

      let viewController = ConfigurableTableViewController(items: [
                 CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Foo")),
                 CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "og")!)),
                 CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "GoogleLogo")!)),
                 CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Bar")),
                 ])
    
  2. 注冊(cè)cell

       func registerCells() {
             for cellConfigurator in items {
                 tableView.register(cellConfigurator.cellClass, forCellReuseIdentifier: cellConfigurator.reuseIdentifier)
             }
         }
    
  3. 配置cell

        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
             let cellConfigurator = items[(indexPath as NSIndexPath).row]
             let cell = tableView.dequeueReusableCell(withIdentifier: cellConfigurator.reuseIdentifier, for: indexPath)
             cellConfigurator.update(cell: cell)
             return cell
         }
    

這個(gè)cell封裝思想出自這里https://github.com/fastred/ConfigurableTableViewController

總結(jié)

上邊兩個(gè)例子瓢喉,我解釋的并不是很詳細(xì)宁赤,只需要打開源碼,仔細(xì)琢磨琢磨就能體會(huì)到里邊的妙處栓票,如有問題决左,可以留言。

在這里獲取代碼:https://github.com/agelessman/TTestDemo

由于知識(shí)水平有限走贪,如有錯(cuò)誤佛猛,還望指出

鏈接

Alamofire源碼解讀系列(一)之概述和使用 簡(jiǎn)書博客園

Alamofire源碼解讀系列(二)之錯(cuò)誤處理(AFError) 簡(jiǎn)書博客園

Alamofire源碼解讀系列(三)之通知處理(Notification) 簡(jiǎn)書博客園

Alamofire源碼解讀系列(四)之參數(shù)編碼(ParameterEncoding) 簡(jiǎn)書博客園

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市坠狡,隨后出現(xiàn)的幾起案子继找,更是在濱河造成了極大的恐慌,老刑警劉巖擦秽,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件码荔,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡感挥,警方通過查閱死者的電腦和手機(jī)缩搅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來触幼,“玉大人硼瓣,你說我怎么就攤上這事≈们” “怎么了堂鲤?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)媒峡。 經(jīng)常有香客問我瘟栖,道長(zhǎng),這世上最難降的妖魔是什么谅阿? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任半哟,我火速辦了婚禮,結(jié)果婚禮上签餐,老公的妹妹穿的比我還像新娘寓涨。我一直安慰自己,他們只是感情好氯檐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布戒良。 她就那樣靜靜地躺著,像睡著了一般冠摄。 火紅的嫁衣襯著肌膚如雪糯崎。 梳的紋絲不亂的頭發(fā)上几缭,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音拇颅,去河邊找鬼奏司。 笑死,一個(gè)胖子當(dāng)著我的面吹牛樟插,可吹牛的內(nèi)容都是我干的韵洋。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼黄锤,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼搪缨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸵熟,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤副编,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后流强,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痹届,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年打月,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了队腐。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡奏篙,死狀恐怖柴淘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秘通,我是刑警寧澤为严,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站肺稀,受9級(jí)特大地震影響第股,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜话原,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一炸茧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧稿静,春花似錦、人聲如沸辕狰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蔓倍。三九已至悬钳,卻和暖如春盐捷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背默勾。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工碉渡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人母剥。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓滞诺,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親环疼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子习霹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容