天天看點

造輪子 | 如何設計一個面向協定的 iOS 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來

最近開源了一個面向協定設計的網絡請求庫 MBNetwork,基于 Alamofire 和 ObjectMapper 實作,目的是簡化業務層的網絡請求操作。

需要幹些啥

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

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

為什麼是 POP 而不是 OOP

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

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

站在 Alamofire 的肩膀上

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

而 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 的請求有三種:

request

upload

download

,這三種請求都有相應的參數,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 對應的接口。注意它們都傳回了類型為

DataRequest

UploadRequest

或者

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

最近一個不是

UIScrollView

superview

UIScrollView

這個地方有點特殊,因為如果直接在

UIScrollView

上添加遮罩視圖,遮罩視圖的中心點是非常難控制的,是以這裡用了一個技巧,遞歸尋找

UIScrollView

superview

,發現不是

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
}
           

最後我們對

MBContainable

extension

,添加一個

latestMask

方法,這個方法實作的功能很簡單,就是傳回

containerView

上最新添加的、而且遵循

MBMaskable

協定的

subview

MBMaskable

協定内部隻定義了一個屬性

maskId

,作用是用來區分多種遮罩。

MBNetwork 内部實作了兩個遵循

MBMaskable

協定的

UIView

,分别是

MBActivityIndicator

MBMaskView

,其中

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

    。如果有,就把新添加的這個遮罩視圖隐藏起來,再添加到

    maskContainer

    containerView

    上。為什麼會有多個遮罩的原因是多個網絡請求可能同時遮罩某一個

    maskContainer

    ,另外,多個遮罩不能都顯示出來,因為有的遮罩可能有半透明部分,是以需要做隐藏操作。至于為什麼都要添加到

    maskContainer

    上,是因為我們不知道哪個請求會最後結束,是以就采取每個請求的遮罩我們都添加,然後結束一個請求就移除一個遮罩,請求都結束的時候,遮罩也就都移除了。
  • maskContainer

    UIScrollView

    的情況做特殊處理,使其不可滾動。

然後是

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

上移除。對

maskContainer

UIScrollView

的情況做特殊處理,當被移除的遮罩是最後一個時,使其可以再滾動。

MBLoadType

為了降低使用成本,MBNetwork 提供了

MBLoadType

枚舉類型。

public enum MBLoadType {
    case none
    case `default`(container: MBContainable)
}
           

none

:表示不需要加載。

default

:傳入遵循

MBContainable

協定的

container

附加值。

然後對

MBLoadType

extension

,使其遵循

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

  • 不需要顯示加載遮罩。
  • 加載開始和加載全部結束時需要調用

    beginRefreshing

    endRefreshing

UITableViewCell

  • maskContainer

    就是本身。
  • mask

    需要定制下,不能是預設的

    MBMaskView

    ,而應該是

    MBActivityIndicator

    ,然後

    MBActivityIndicator

    “菊花”的顔色和背景色應該和

    UIControl

    一緻。

結合網絡請求

至此,加載相關協定的定義和預設實作都已經完成。現在需要做的就是把加載和網絡請求結合起來,其實很簡單,之前

MBRequestable

協定擴充的網絡請求方法都傳回了類型為

DataRequest

UploadRequest

或者

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 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來

UIButton

上顯示加載遮罩
造輪子 | 如何設計一個面向協定的 iOS 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來

UITableViewCell

上顯示加載遮罩
造輪子 | 如何設計一個面向協定的 iOS 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來
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 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來
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 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來

首先,我們建立一個遵循

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(+, , UIScreen.main.bounds.height--(*++*), ))
request(WeatherForm()).load(load: load)
           

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

下面是利用

LoadConfig

UITableView

上顯示自定義加載遮罩的的例子。

造輪子 | 如何設計一個面向協定的 iOS 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來
let load = LoadConfig(container:self.tableView, mask: MBActivityIndicator(), inset: UIEdgeInsetsMake(UIScreen.main.bounds.width - self.tableView.contentOffset.y >  ? UIScreen.main.bounds.width - self.tableView.contentOffset.y : , , , ))
request(WeatherForm()).load(load: load)
           

加載進度展示

進度的展示比較簡單,隻需要有方法實時更新進度即可,是以我們先定義

MBProgressable

協定,内容如下:

public protocol MBProgressable {
    func progress(_ progress: Progress)
}
           

因為一般隻有上傳和下載下傳大檔案才需要進度展示,是以我們隻對

UploadRequest

DownloadRequest

extension

,添加

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 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來

資訊提示

資訊提示包括兩個部分,出錯提示和成功提示。是以我們先抽象了一個

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

協定中

code

message

屬性的映射關系。

假設服務端傳回的 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.code

data.message

的值映射到

code

message

屬性上。至此,錯誤資訊的解析就完成了。

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

MBWarnable

協定:

public protocol MBWarnable: MBMessageable {
    func show(error: MBErrorable?)
}
           

這個協定遵循

MBMessageable

協定。遵循這個協定的對象除了要實作

MBMessageable

協定的

messageContainer

方法,還需要實作

show

方法,這個方法隻有一個參數,通過這個參數我們傳入遵循錯誤資訊協定的對象。

現在我們就可以使用

MBErrorable

MBWarnable

協定來進行出錯提示了。和之前一樣我們還是對

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

    協定的泛型錯誤解析對象。傳入這個對象到 AlamofireObjectMapper 的

    responseObject

    方法中即可獲得服務端傳回的錯誤資訊。
  • warn

    :遵循

    MBWarnable

    協定的錯誤展示對象。
  • completionHandler

    :傳回結果正确時調用的閉包。業務層一般通過這個閉包來做特殊錯誤碼處理。

做了如下的事情:

  • 通過 Alamofire 的

    response

    方法擷取非業務錯誤資訊,如果存在,則調用

    warn

    show

    方法展示錯誤資訊,這裡大家可能會有點疑惑:為什麼可以把

    String

    當做

    MBErrorable

    傳入到

    show

    方法中?這是因為我們做了下面的事情:
    extension String: MBErrorable {
        public var message: String? {
            return self
        }
    }
               
  • 通過 AlamofireObjectMapper 的

    responseObject

    方法擷取到服務端傳回的錯誤資訊,判斷傳回的錯誤碼是否包含在

    successCodes

    中,如果是,則交給業務層處理;(PS:對于某些需要特殊處理的錯誤碼,也可以定義在

    successCodes

    中,然後在業務層單獨處理。)否則,直接調用

    warn

    show

    方法展示錯誤資訊。

成功提示

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

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

協定的泛型錯誤解析對象,因為如果服務端的傳回結果是錯的,則不應該提示成功。還是通過 AlamofireObjectMapper 的

responseObject

方法擷取到服務端傳回的錯誤資訊,判斷傳回的錯誤碼是否包含在

successCodes

中,如果是,則通過

inform

對象 的

show

方法展示成功資訊。

常用控件支援

觀察目前主流 App,資訊提示一般是通過

UIAlertController

來展示的,是以我們通過 extension 的方式讓

UIAlertController

遵循

MBWarnable

MBInformable

協定。

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

下面是使用的兩個例子:

造輪子 | 如何設計一個面向協定的 iOS 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來
造輪子 | 如何設計一個面向協定的 iOS 網絡請求庫需要幹些啥為什麼是 POP 而不是 OOP站在 Alamofire 的肩膀上當然還有 ObjectMapper一步一步來
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

的實作添加對其它第三方提示庫的支援。

重新請求

開發中……敬請期待