造輪子 | 如何設計一個面向協議的 iOS 網路請求庫

餓了麼物流技術團隊發表於2018-03-19
2017-02-12 | MmoaaY | iOS

概述

最近開源了一個面向協議設計的網路請求庫 MBNetwork,基於 AlamofireObjectMapper 實現,目的是簡化業務層的網路請求操作。

需要幹些啥

對於大部分 App 而言,業務層做一次網路請求通常關心的問題有如下幾個:

  • 如何在任意位置發起網路請求。
  • 表單建立。包含請求地址、請求方式(GET/POST/……)、請求頭等……
  • 載入遮罩。目的是阻塞 UI 互動,同時告知使用者操作正在進行。比如提交表單時在提交按鈕上顯示 “菊花”,同時使其失效。
  • 載入進度展示。下載上傳圖片等資源時提示使用者當前進度。
  • 斷點續傳。下載上傳圖片等資源發生錯誤時可以在之前已完成部分的基礎上繼續操作,這個 Alamofire 可以支援。
  • 資料解析。因為目前主流服務端和客戶端資料交換採用的格式是 JSON,所以我們暫時先考慮 JSON 格式的資料解析,這個 ObjectMapper 可以支援。
  • 出錯提示。發生業務異常時,直接顯示服務端返回的異常資訊。前提是服務端異常資訊足夠友好。
  • 成功提示。請求正常結束時提示使用者。
  • 網路異常重新請求。顯示網路異常介面,點選之後重新傳送請求。

為什麼是 POP 而不是 OOP

關於 POPOOP 這兩種設計思想及其特點的文章很多,所以我就不廢話了,主要說說為啥要用 POP 來寫 MBNetwork

  • 想嘗試一下一切皆協議的設計方式。所以這個庫的設計只是一次極限嘗試,並不代表這就是最完美的設計方式。
  • 如果以 OOP 的方式實現,使用者需要通過繼承的方式來獲得某個類實現的功能,如果使用者還需要另外某個類實現的功能,就會很尷尬。而 POP 是通過對協議進行擴充套件來實現功能,使用者可以同時遵循多個協議,輕鬆解決 OOP 的這個硬傷。
  • OOP 繼承的方式會使某些子類獲得它們不需要的功能。
  • 如果因為業務的增多,需要對某些業務進行分離,OOP 的方式還是會碰到子類不能繼承多個父類的問題,而 POP 則完全不會,分離之後,只需要遵循分離後的多個協議即可。
  • OOP 繼承的方式入侵性比較強。
  • POP 可以通過擴充套件的方式對各個協議進行預設實現,降低使用者的學習成本。
  • 同時 POP 還能讓使用者對協議做自定義的實現,保證其高度可配置性。

站在 Alamofire 的肩膀上

很多人都喜歡說 AlamofireSwift 版本的 AFNetworking,但是在我看來,AlamofireAFNetworking 更純粹。這和 Swift 語言本身的特性也是有關係的,Swift 開發者們,更喜歡寫一些輕量的框架。比如 AFNetworking 把很多 UI 相關的擴充套件功能都做在框架內,而 Alamofire 的做法則是放在另外的擴充套件庫中。比如 AlamofireImageAlamofireNetworkActivityIndicator

MBNetwork 就可以當做是 Alamofire 的一個擴充套件庫,所以,MBNetwork 很大程度上遵循了 Alamofire 介面的設計規範。一方面,降低了 MBNetwork 的學習成本,另一方面,從個人角度來看,Alamofire 確實有很多特別值得借鑑的地方。

POP

首先當然是 POP 啦,Alamofire 大量運用了 protocol + extension 的實現方式。

enum

做為檢驗寫 Swift 姿勢正確與否的重要指標,Alamofire 當然不會缺。

鏈式呼叫

這是讓 Alamofire 成為一個優雅的網路框架的重要原因之一。這一點 MBNetwork 也進行了完全的 Copy。

@discardableResult

Alamofire 所有帶返回值的方法前面,都會有這麼一個標籤,其實作用很簡單,因為在 Swift 中,返回值如果沒有被使用,Xcode 會產生告警資訊。加上這個標籤之後,表示這個方法的返回值就算沒有被使用,也不產生告警。

當然還有 ObjectMapper

引入 ObjectMapper 很大一部分原因是需要做錯誤和成功提示。因為只有解析服務端的錯誤資訊節點才能知道返回結果是否正確,所以我們引入 ObjectMapper 來做 JSON 解析。 而只做 JSON 解析的原因是目前主流的服務端客戶端資料互動格式是 JSON

這裡需要提到的就是另外一個 Alamofire 的擴充套件庫 AlamofireObjectMapper,從名字就可以看出來,這個庫就是參照 Alamofire 的 API 規範來做 ObjectMapper 做的事情。這個庫的程式碼很少,但實現方式非常 Alamofire,大家可以拜讀一下它的原始碼,基本上就知道如何基於 Alamofire 做自定義資料解析了。

注:被 @Foolish 安利,正在接入 ProtoBuf 中…

一步一步來

表單建立

Alamofire 的請求有三種: requestuploaddownload,這三種請求都有相應的引數,MBNetwork 把這些引數抽象成了對應的協議,具體內容參見:MBForm.swift。這種做法有幾個優點:

  1. 對於類似 headers 這樣的引數,一般全域性都是一致的,可以直接 extension 指定。
  2. 通過協議的名字即可知道表單的功能,簡單明確。

下面是 MBNetwork 表單協議的用法舉例:

指定全域性 headers 引數:

extension MBFormable {
    public func headers() -> [String: String] {
        return ["accessToken":"xxx"];
    }
}
複製程式碼

建立具體業務表單:

struct WeatherForm: MBRequestFormable {
    var city = "shanghai"

    public func parameters() -> [String: Any] {
        return ["city": city]
    }

    var url = "https://raw.githubusercontent.com/tristanhimmelman/AlamofireObjectMapper/2ee8f34d21e8febfdefb2b3a403f18a43818d70a/sample_keypath_json"
    var method = Alamofire.HTTPMethod.get
}
複製程式碼

表單協議化可能有過度設計的嫌疑,有同感的仍然可以使用 Alamofire 對應的介面去做網路請求,不影響 MBNetwork 其他功能的使用。

基於表單請求資料

表單已經抽象成協議,現在就可以基於表單傳送網路請求了,因為之前已經說過需要在任意位置傳送網路請求,而實現這一點的方法基本就這幾種:

  • 單例。
  • 全域性方法,Alamofire 就是這麼幹的。
  • 協議擴充套件。

MBNetwork 採用了最後一種方法。原因很簡單,MBNetwork 是以一切皆協議的原則設計的,所以我們把網路請求抽象成 MBRequestable 協議。

首先,MBRequestable 是一個空協議 。

///  Network request protocol, object conforms to this protocol can make network request
public protocol MBRequestable: class {

}
複製程式碼

為什麼是空協議,因為不需要遵循這個協議的物件幹啥。

然後對它做 extension,實現網路請求相關的一系列介面:

func request(_ form: MBRequestFormable) -> DataRequest

func download(_ form: MBDownloadFormable) -> DownloadRequest

func download(_ form: MBDownloadResumeFormable) -> DownloadRequest

func upload(_ form: MBUploadDataFormable) -> UploadRequest

func upload(_ form: MBUploadFileFormable) -> UploadRequest

func upload(_ form: MBUploadStreamFormable) -> UploadRequest

func upload(_ form: MBUploadMultiFormDataFormable, completion: ((UploadRequest) -> Void)?)
複製程式碼

這些就是網路請求的介面,引數是各種表單協議,介面內部呼叫的其實是 Alamofire 對應的介面。注意它們都返回了型別為 DataRequestUploadRequest 或者 DownloadRequest 的物件,通過返回值我們可以繼續呼叫其他方法。

到這裡 MBRequestable 的實現就完成了,使用方法很簡單,只需要設定型別遵循 MBRequestable 協議,就可以在該型別內發起網路請求。如下:

class LoadableViewController: UIViewController, MBRequestable {
    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        request(WeatherForm())
    }
}
複製程式碼

載入

對於載入我們關心的點有如下幾個:

  • 載入開始需要幹啥。
  • 載入結束需要幹啥。
  • 是否需要顯示載入遮罩。
  • 在何處顯示遮罩。
  • 顯示遮罩的內容。

對於這幾點,我對協議的劃分是這樣的:

  • MBContainable 協議。遵循該協議的物件可以做為載入的容器。
  • MBMaskable 協議。遵循該協議的 UIView 可以做為載入遮罩。
  • MBLoadable 協議。遵循該協議的物件可以定義載入的配置和流程。

MBContainable

遵循這個協議的物件只需要實現下面的方法即可:

func containerView() -> UIView?
複製程式碼

這個方法返回做為遮罩容器的 UIView。做為遮罩的 UIView 最終會被新增到 containerView 上。

不同型別的容器的 containerView 是不一樣的,下面是各種型別容器 containerView 的列表:

容器 containerView
UIViewController view
UIView self
UITableViewCell contentView
UIScrollView 最近一個不是 UIScrollViewsuperview

UIScrollView 這個地方有點特殊,因為如果直接在 UIScrollView 上新增遮罩檢視,遮罩檢視的中心點是非常難控制的,所以這裡用了一個技巧,遞迴尋找 UIScrollViewsuperview,發現不是 UIScrollView 型別的直接返回即可。程式碼如下:

public override func containerView() -> UIView? {
    var next = superview
    while nil != next {
        if let _ = next as? UIScrollView {
            next = next?.superview
        } else {
            return next
        }
    }
    return nil
}
複製程式碼

最後我們對 MBContainableextension,新增一個 latestMask 方法,這個方法實現的功能很簡單,就是返回 containerView 上最新新增的、而且遵循 MBMaskable 協議的 subview

MBMaskable

協議內部只定義了一個屬性 maskId,作用是用來區分多種遮罩。

MBNetwork 內部實現了兩個遵循 MBMaskable 協議的 UIView,分別是 MBActivityIndicatorMBMaskView,其中 MBMaskView 的效果是參照 MBProgressHUD 實現,所以對於大部分場景來說,直接使用這兩個 UIView 即可。

注:MBMaskable 協議唯一的作用是與 containerView 上其它 subview 做區分。

MBLoadable

做為載入協議的核心部分,MBLoadable 包含如下幾個部分:

  • func mask() -> MBMaskable?:遮罩檢視,可選的原因是可能不需要遮罩。
  • func inset() -> UIEdgeInsets:遮罩檢視和容器檢視的邊距,預設值 UIEdgeInsets.zero
  • func maskContainer() -> MBContainable?:遮罩容器檢視,可選的原因是可能不需要遮罩。
  • func begin():載入開始回撥方法。
  • func end():載入結束回撥方法。

然後對協議要求實現的幾個方法做預設實現:

func mask() -> MBMaskable? {
    return MBMaskView() // 預設顯示 MBProgressHUD 效果的遮罩。
}

 func inset() -> UIEdgeInsets {
    return UIEdgeInsets.zero // 預設邊距為 0 。
}

func maskContainer() -> MBContainable? {
    return nil // 預設沒有遮罩容器。
}

func begin() {
    show() // 預設呼叫 show 方法。
}

func end() {
    hide() // 預設呼叫 hide 方法。
}
複製程式碼

上述程式碼中的 show 方法和 hide 方法是實現載入遮罩的核心程式碼。

show 方法的內容如下:

func show() {
    if let mask = self.mask() as? UIView {
        var isHidden = false
        if let _ = self.maskContainer()?.latestMask() {
            isHidden = true
        }
        self.maskContainer()?.containerView()?.addMBSubView(mask, insets: self.inset())
        mask.isHidden = isHidden

        if let container = self.maskContainer(), let scrollView = container as? UIScrollView {
            scrollView.setContentOffset(scrollView.contentOffset, animated: false)
            scrollView.isScrollEnabled = false
        }
    }
}
複製程式碼

這個方法做了下面幾件事情:

  • 判斷 mask 方法返回的是不是遵循 MBMaskable 協議的 UIView,因為如果不是 UIView,不能被新增到其它的 UIView 上。
  • 通過 MBContainable 協議上的 latestMask 方法獲取最新新增的、且遵循 MBMaskable 協議的 UIView。如果有,就把新新增的這個遮罩檢視隱藏起來,再新增到 maskContainercontainerView 上。為什麼會有多個遮罩的原因是多個網路請求可能同時遮罩某一個 maskContainer,另外,多個遮罩不能都顯示出來,因為有的遮罩可能有半透明部分,所以需要做隱藏操作。至於為什麼都要新增到 maskContainer 上,是因為我們不知道哪個請求會最後結束,所以就採取每個請求的遮罩我們都新增,然後結束一個請求就移除一個遮罩,請求都結束的時候,遮罩也就都移除了。
  • maskContainerUIScrollView 的情況做特殊處理,使其不可滾動。

然後是 hide 方法,內容如下:

func hide() {
    if let latestMask = self.maskContainer()?.latestMask() {
        latestMask.removeFromSuperview()

        if let container = self.maskContainer(), let scrollView = container as? UIScrollView {
            if false == latestMask.isHidden {
                scrollView.isScrollEnabled = true
            }
        }
    }
}
複製程式碼

相比 show 方法,hide 方法做的事情要簡單一些,通過 MBContainable 協議上的 latestMask 方法獲取最新新增的、且遵循 MBMaskable 協議的 UIView,然後從 superview 上移除。對 maskContainerUIScrollView 的情況做特殊處理,當被移除的遮罩是最後一個時,使其可以再滾動。

MBLoadType

為了降低使用成本,MBNetwork 提供了 MBLoadType 列舉型別。

public enum MBLoadType {
    case none
    case `default`(container: MBContainable)
}
複製程式碼

none:表示不需要載入。 default:傳入遵循 MBContainable 協議的 container 附加值。

然後對 MBLoadTypeextension,使其遵循 MBLoadable 協議。

extension MBLoadType: MBLoadable {
    public func maskContainer() -> MBContainable? {
        switch self {
        case .default(let container):
            return container
        case .none:
            return nil
        }
    }
}
複製程式碼

這樣對於不需要載入或者只需要指定 maskContainer 的情況(PS:比如全屏遮罩),就可以直接用 MBLoadType 來代替 MBLoadable

常用控制元件支援

UIControl

  • maskContainer 就是本身,比如 UIButton,載入時直接在按鈕上顯示“菊花”即可。
  • mask 需要定製下,不能是預設的 MBMaskView,而應該是 MBActivityIndicator,然後 MBActivityIndicator “菊花”的顏色和背景色應該和 UIControl 一致。
  • 載入開始和載入全部結束時需要設定 isEnabled

UIRefreshControl

  • 不需要顯示載入遮罩。
  • 載入開始和載入全部結束時需要呼叫 beginRefreshingendRefreshing

UITableViewCell

  • maskContainer 就是本身。
  • mask 需要定製下,不能是預設的 MBMaskView,而應該是 MBActivityIndicator,然後 MBActivityIndicator “菊花”的顏色和背景色應該和 UIControl 一致。

結合網路請求

至此,載入相關協議的定義和預設實現都已經完成。現在需要做的就是把載入和網路請求結合起來,其實很簡單,之前 MBRequestable 協議擴充套件的網路請求方法都返回了型別為 DataRequestUploadRequest 或者 DownloadRequest 的物件,所以我們對它們做 extension,然後實現下面的 load 方法即可。

func load(load: MBLoadable = MBLoadType.none) -> Self {
    load.begin()
    return response { (response: DefaultDataResponse) in
        load.end()
    }
}
複製程式碼

傳入引數為遵循 MBLoadable 協議的 load 物件,預設值為 MBLoadType.none。請求開始時呼叫其 begin 方法,請求返回時呼叫其 end 方法。

使用方法

基礎用法

UIViewController 上顯示載入遮罩

造輪子 | 如何設計一個面向協議的 iOS 網路請求庫

request(WeatherForm()).load(load: MBLoadType.default(container: self))
複製程式碼
UIButton 上顯示載入遮罩

造輪子 | 如何設計一個面向協議的 iOS 網路請求庫

request(WeatherForm()).load(load: button)
複製程式碼
UITableViewCell 上顯示載入遮罩

造輪子 | 如何設計一個面向協議的 iOS 網路請求庫

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView .deselectRow(at: indexPath, animated: false)
    let cell = tableView.cellForRow(at: indexPath)
    request(WeatherForm()).load(load: cell!)
}
複製程式碼
UIRefreshControl

造輪子 | 如何設計一個面向協議的 iOS 網路請求庫

refresh.attributedTitle = NSAttributedString(string: "Loadable UIRefreshControl")
refresh.addTarget(self, action: #selector(LoadableTableViewController.refresh(refresh:)), for: .valueChanged)
tableView.addSubview(refresh)
     
func refresh(refresh: UIRefreshControl) {
    request(WeatherForm()).load(load: refresh)
}
複製程式碼

進階

除了基本的用法,MBNetwork 還支援對載入進行完全的自定義,做法如下:

造輪子 | 如何設計一個面向協議的 iOS 網路請求庫

首先,我們建立一個遵循 MBLoadable 協議的型別 LoadConfig

class LoadConfig: MBLoadable {
    init(container: MBContainable? = nil, mask: MBMaskable? = MBMaskView(), inset: UIEdgeInsets = UIEdgeInsets.zero) {
        insetMine = inset
        maskMine = mask
        containerMine = container
    }
    
    func mask() -> MBMaskable? {
        return maskMine
    }
    
    func inset() -> UIEdgeInsets {
        return insetMine
    }
    
    func maskContainer() -> MBContainable? {
        return containerMine
    }
    
    func begin() {
        show()
    }
    
    func end() {
        hide()
    }
    
    var insetMine: UIEdgeInsets
    var maskMine: MBMaskable?
    var containerMine: MBContainable?
}
複製程式碼

然後我們就可以這樣使用它了。

let load = LoadConfig(container: view, mask:MBEyeLoading(), inset: UIEdgeInsetsMake(30+64, 15, UIScreen.main.bounds.height-64-(44*4+30+15*3), 15))
request(WeatherForm()).load(load: load)
複製程式碼

你會發現所有的東西都是可以自定義的,而且使用起來仍然很簡單。

下面是利用 LoadConfigUITableView 上顯示自定義載入遮罩的的例子。

造輪子 | 如何設計一個面向協議的 iOS 網路請求庫


let load = LoadConfig(container:self.tableView, mask: MBActivityIndicator(), inset: UIEdgeInsetsMake(UIScreen.main.bounds.width - self.tableView.contentOffset.y > 0 ? UIScreen.main.bounds.width - self.tableView.contentOffset.y : 0, 0, 0, 0))
request(WeatherForm()).load(load: load)
        
複製程式碼

載入進度展示

進度的展示比較簡單,只需要有方法實時更新進度即可,所以我們先定義 MBProgressable 協議,內容如下:

public protocol MBProgressable {
    func progress(_ progress: Progress)
}
複製程式碼

因為一般只有上傳和下載大檔案才需要進度展示,所以我們只對 UploadRequestDownloadRequestextension,新增 progress 方法,引數為遵循 MBProgressable 協議的 progress 物件 :

func progress(progress: MBProgressable) -> Self {
    return uploadProgress { (prog: Progress) in
        progress.progress(prog)
    }
}

複製程式碼

常用控制元件支援

既然是進度展示,當然得讓 UIProgressView 遵循 MBProgressable 協議,實現如下:

// MARK: - Making `UIProgressView` conforms to `MBLoadProgressable`
extension UIProgressView: MBProgressable {

    /// Updating progress
    ///
    /// - Parameter progress: Progress object generated by network request
    public func progress(_ progress: Progress) {
        self.setProgress(Float(progress.completedUnitCount).divided(by: Float(progress.totalUnitCount)), animated: true)
    }
}

複製程式碼

然後我們就可以直接把 UIProgressView 物件當做 progress 方法的引數了。

造輪子 | 如何設計一個面向協議的 iOS 網路請求庫

download(ImageDownloadForm()).progress(progress: progress)
複製程式碼

資訊提示

資訊提示包括兩個部分,出錯提示和成功提示。所以我們先抽象了一個 MBMessageable 協議,協議的內容僅僅包含了顯示訊息的容器。

public protocol MBMessageable {
    func messageContainer() -> MBContainable?
}
複製程式碼

毫無疑問,返回的容器當然也是遵循 MBContainable 協議的,這個容器將被用來展示出錯和成功提示。

出錯提示

出錯提示需要做的事情有兩步:

  1. 解析錯誤資訊
  2. 展示錯誤資訊

首先我們來完成第一步,解析錯誤資訊。這裡我們把錯誤資訊抽象成協議 MBErrorable,其內容如下:

public protocol MBErrorable {

    /// Using this set with code to distinguish successful code from error code
    var successCodes: [String] { get }

    /// Using this code with successCodes set to distinguish successful code from error code
    var code: String? { get }

    /// Corresponding message
    var message: String? { get }
}
複製程式碼

其中 successCodes 用來定義哪些錯誤碼是正常的; code 表示當前錯誤碼;message 定義了展示給使用者的資訊。

具體怎麼使用這個協議後面再說,我們接著看 JSON 錯誤解析協議 MBJSONErrorable

public protocol MBJSONErrorable: MBErrorable, Mappable {

}
複製程式碼

注意這裡的 Mappable 協議來自 ObjectMapper,目的是讓遵循這個協議的物件實現 Mappable 協議中的 func mapping(map: Map) 方法,這個方法定義了 JSON 資料中錯誤資訊到 MBErrorable 協議中 codemessage 屬性的對映關係。

假設服務端返回的 JSON 內容如下:

{
    "data": {
        "code": "200",    
        "message": "請求成功"
    }
}
複製程式碼

那我們的錯誤資訊物件就可以定義成下面的樣子。

class WeatherError: MBJSONErrorable {
    var successCodes: [String] = ["200"]

    var code: String?
    var message: String?

    init() { }

    required init?(map: Map) { }

    func mapping(map: Map) {
        code <- map["data.code"]
        message <- map["data.message"]
    }
}
複製程式碼

ObjectMapper 會把 data.codedata.message 的值對映到 codemessage 屬性上。至此,錯誤資訊的解析就完成了。

然後是第二步,錯誤資訊展示。定義 MBWarnable 協議:

public protocol MBWarnable: MBMessageable {
    func show(error: MBErrorable?)
}
複製程式碼

這個協議遵循 MBMessageable 協議。遵循這個協議的物件除了要實現 MBMessageable 協議的 messageContainer 方法,還需要實現 show 方法,這個方法只有一個引數,通過這個引數我們傳入遵循錯誤資訊協議的物件。

現在我們就可以使用 MBErrorableMBWarnable 協議來進行出錯提示了。和之前一樣我們還是對 DataRequest 做 extension。新增 warn 方法。

func warn<T: MBJSONErrorable>(
        error: T,
        warn: MBWarnable,
        completionHandler: ((MBJSONErrorable) -> Void)? = nil
        ) -> Self {

    return response(completionHandler: { (response: DefaultDataResponse) in
        if let err = response.error {
            warn.show(error: err.localizedDescription)
        }
    }).responseObject(queue: nil, keyPath: nil, mapToObject: nil, context: nil) { (response: DataResponse<T>) in
        if let err = response.result.value {
            if let code = err.code {
                if true == error.successCodes.contains(code) {
                    completionHandler?(err)
                } else {
                    warn.show(error: err)
                }
            }
        }
    }
}
複製程式碼

這個方法包括三個引數:

  • error:遵循 MBJSONErrorable 協議的泛型錯誤解析物件。傳入這個物件到 AlamofireObjectMapperresponseObject 方法中即可獲得服務端返回的錯誤資訊。
  • warn:遵循 MBWarnable 協議的錯誤展示物件。
  • completionHandler:返回結果正確時呼叫的閉包。業務層一般通過這個閉包來做特殊錯誤碼處理。

做了如下的事情:

  • 通過 Alamofireresponse 方法獲取非業務錯誤資訊,如果存在,則呼叫 warnshow 方法展示錯誤資訊,這裡大家可能會有點疑惑:為什麼可以把 String 當做 MBErrorable 傳入到 show 方法中?這是因為我們做了下面的事情:

    extension String: MBErrorable {
          public var message: String? {
              return self
          }
    }
    複製程式碼
  • 通過 AlamofireObjectMapperresponseObject 方法獲取到服務端返回的錯誤資訊,判斷返回的錯誤碼是否包含在 successCodes 中,如果是,則交給業務層處理;(PS:對於某些需要特殊處理的錯誤碼,也可以定義在 successCodes 中,然後在業務層單獨處理。)否則,直接呼叫 warnshow 方法展示錯誤資訊。

成功提示

相比錯誤提示,成功提示會簡單一些,因為成功提示資訊一般都是在本地定義的,不需要從服務端獲取,所以成功提示協議的內容如下:

public protocol MBInformable: MBMessageable {
    func show()

    func message() -> String
}
複製程式碼

包含兩個方法, show 方法用於展示資訊;message 方法定義展示的資訊。

然後對 DataRequest 做擴充套件,新增 inform 方法:

func inform<T: MBJSONErrorable>(error: T, inform: MBInformable) -> Self {

    return responseObject(queue: nil, keyPath: nil, mapToObject: nil, context: nil) { (response: DataResponse<T>) in
        if let err = response.result.value {
            if let code = err.code {
                if true == error.successCodes.contains(code) {
                    inform.show()
                }
            }
        }
    }
}
複製程式碼

這裡同樣也傳入遵循 MBJSONErrorable 協議的泛型錯誤解析物件,因為如果服務端的返回結果是錯的,則不應該提示成功。還是通過 AlamofireObjectMapperresponseObject 方法獲取到服務端返回的錯誤資訊,判斷返回的錯誤碼是否包含在 successCodes 中,如果是,則通過 inform 物件 的 show 方法展示成功資訊。

常用控制元件支援

觀察目前主流 App,資訊提示一般是通過 UIAlertController 來展示的,所以我們通過 extension 的方式讓 UIAlertController 遵循 MBWarnableMBInformable 協議。

extension UIAlertController: MBInformable {
    public func show() {
        UIApplication.shared.keyWindow?.rootViewController?.present(self, animated: true, completion: nil)
    }
}

extension UIAlertController: MBWarnable{
    public func show(error: MBErrorable?) {
        if let err = error {
            if "" != err.message {
                message = err.message
                
                UIApplication.shared.keyWindow?.rootViewController?.present(self, animated: true, completion: nil)
            }
        }
    }
}
複製程式碼

發現這裡我們沒有用到 messageContainer,這是因為對於 UIAlertController 來說,它的容器是固定的,使用 UIApplication.shared.keyWindow?.rootViewController? 即可。注意對於MBInformable,直接展示 UIAlertController, 而對於 MBWarnable,則是展示 error 中的 message

下面是使用的兩個例子:

這裡寫圖片描述

這裡寫圖片描述

let alert = UIAlertController(title: "Warning", message: "Network unavailable", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.cancel, handler: nil))
        
request(WeatherForm()).warn(
    error: WeatherError(),
    warn: alert
)

let alert = UIAlertController(title: "Notice", message: "Load successfully", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.cancel, handler: nil))
request(WeatherForm()).inform(
    error: WeatherInformError(),
    inform: alert
)
複製程式碼

這樣就達到了業務層定義展示資訊,MBNetwork 自動展示的效果,是不是簡單很多?至於擴充套件性,我們還是可以參照 UIAlertController 的實現新增對其它第三方提示庫的支援。

重新請求

開發中……敬請期待

如有任何智慧財產權、版權問題或理論錯誤,還請指正。

轉載請註明原作者及以上資訊。

相關文章