概述
最近開源了一個面向協議設計的網路請求庫 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。這種做法有幾個優點:
- 對於類似
headers
這樣的引數,一般全域性都是一致的,可以直接 extension 指定。 - 通過協議的名字即可知道表單的功能,簡單明確。
下面是 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
上顯示載入遮罩
request(WeatherForm()).load(load: MBLoadType.default(container: self))
複製程式碼
在 UIButton
上顯示載入遮罩
request(WeatherForm()).load(load: button)
複製程式碼
在 UITableViewCell
上顯示載入遮罩
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
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
還支援對載入進行完全的自定義,做法如下:
首先,我們建立一個遵循 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)
複製程式碼
你會發現所有的東西都是可以自定義的,而且使用起來仍然很簡單。
下面是利用 LoadConfig
在 UITableView
上顯示自定義載入遮罩的的例子。
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)
}
複製程式碼
因為一般只有上傳和下載大檔案才需要進度展示,所以我們只對 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
方法的引數了。
download(ImageDownloadForm()).progress(progress: progress)
複製程式碼
資訊提示
資訊提示包括兩個部分,出錯提示和成功提示。所以我們先抽象了一個 MBMessageable
協議,協議的內容僅僅包含了顯示訊息的容器。
public protocol MBMessageable {
func messageContainer() -> MBContainable?
}
複製程式碼
毫無疑問,返回的容器當然也是遵循 MBContainable
協議的,這個容器將被用來展示出錯和成功提示。
出錯提示
出錯提示需要做的事情有兩步:
- 解析錯誤資訊
- 展示錯誤資訊
首先我們來完成第一步,解析錯誤資訊。這裡我們把錯誤資訊抽象成協議 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
。
下面是使用的兩個例子:
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
的實現新增對其它第三方提示庫的支援。
重新請求
開發中……敬請期待
如有任何智慧財產權、版權問題或理論錯誤,還請指正。
轉載請註明原作者及以上資訊。