最近開源了一個(gè)面向協(xié)議設(shè)計(jì)的網(wǎng)絡(luò)請求庫 MBNetwork,基于 Alamofire 和 ObjectMapper 實(shí)現(xiàn),目的是簡化業(yè)務(wù)層的網(wǎng)絡(luò)請求操作。
對于大部分 App 而言,業(yè)務(wù)層做一次網(wǎng)絡(luò)請求通常關(guān)心的問題有如下幾個(gè):
如何在任意位置發(fā)起網(wǎng)絡(luò)請求。表單創(chuàng)建。包含請求地址、請求方式(GET/POST/……)、請求頭等……加載遮罩。目的是阻塞 UI 交互,同時(shí)告知用戶操作正在進(jìn)行。比如提交表單時(shí)在提交按鈕上顯示 “菊花”,同時(shí)使其失效。加載進(jìn)度展示。下載上傳圖片等資源時(shí)提示用戶當(dāng)前進(jìn)度。斷點(diǎn)續(xù)傳。下載上傳圖片等資源發(fā)生錯(cuò)誤時(shí)可以在之前已完成部分的基礎(chǔ)上繼續(xù)操作,這個(gè)Alamofire 可以支持。數(shù)據(jù)解析。因?yàn)槟壳爸髁鞣?wù)端和客戶端數(shù)據(jù)交換采用的格式是 JSON,所以我們暫時(shí)先考慮 JSON 格式的數(shù)據(jù)解析,這個(gè) ObjectMapper 可以支持。出錯(cuò)提示。發(fā)生業(yè)務(wù)異常時(shí),直接顯示服務(wù)端返回的異常信息。前提是服務(wù)端異常信息足夠友好。成功提示。請求正常結(jié)束時(shí)提示用戶。網(wǎng)絡(luò)異常重新請求。顯示網(wǎng)絡(luò)異常界面,點(diǎn)擊之后重新發(fā)送請求。POP 而不是 OOP關(guān)于 POP 和 OOP 這兩種設(shè)計(jì)思想及其特點(diǎn)的文章很多,所以我就不廢話了,主要說說為啥要用 POP 來寫 MBNetwork。
OOP 的方式實(shí)現(xiàn),使用者需要通過繼承的方式來獲得某個(gè)類實(shí)現(xiàn)的功能,如果使用者還需要另外某個(gè)類實(shí)現(xiàn)的功能,就會很尷尬。而 POP 是通過對協(xié)議進(jìn)行擴(kuò)展來實(shí)現(xiàn)功能,使用者可以同時(shí)遵循多個(gè)協(xié)議,輕松解決 OOP 的這個(gè)硬傷。OOP 繼承的方式會使某些子類獲得它們不需要的功能。如果因?yàn)闃I(yè)務(wù)的增多,需要對某些業(yè)務(wù)進(jìn)行分離,OOP 的方式還是會碰到子類不能繼承多個(gè)父類的問題,而 POP 則完全不會,分離之后,只需要遵循分離后的多個(gè)協(xié)議即可。OOP 繼承的方式入侵性比較強(qiáng)。POP 可以通過擴(kuò)展的方式對各個(gè)協(xié)議進(jìn)行默認(rèn)實(shí)現(xiàn),降低使用者的學(xué)習(xí)成本。同時(shí) POP 還能讓使用者對協(xié)議做自定義的實(shí)現(xiàn),保證其高度可配置性。Alamofire 的肩膀上很多人都喜歡說 Alamofire 是 Swift 版本的 AFNetworking,但是在我看來,Alamofire 比 AFNetworking 更純粹。這和 Swift 語言本身的特性也是有關(guān)系的,Swift 開發(fā)者們,更喜歡寫一些輕量的框架。比如 AFNetworking 把很多 UI 相關(guān)的擴(kuò)展功能都做在框架內(nèi),而 Alamofire 的做法則是放在另外的擴(kuò)展庫中。比如 AlamofireImage 和 AlamofireNetworkActivityIndicator
而 MBNetwork 就可以當(dāng)做是 Alamofire 的一個(gè)擴(kuò)展庫,所以,MBNetwork 很大程度上遵循了 Alamofire 接口的設(shè)計(jì)規(guī)范。一方面,降低了 MBNetwork 的學(xué)習(xí)成本,另一方面,從個(gè)人角度來看,Alamofire 確實(shí)有很多特別值得借鑒的地方。
POP首先當(dāng)然是 POP 啦,Alamofire 大量運(yùn)用了  PRotocol + extension 的實(shí)現(xiàn)方式。
enum做為檢驗(yàn)寫 Swift 姿勢正確與否的重要指標(biāo),Alamofire 當(dāng)然不會缺。
這是讓 Alamofire 成為一個(gè)優(yōu)雅的網(wǎng)絡(luò)框架的重要原因之一。這一點(diǎn) MBNetwork 也進(jìn)行了完全的 Copy。
@discardableResult在 Alamofire 所有帶返回值的方法前面,都會有這么一個(gè)標(biāo)簽,其實(shí)作用很簡單,因?yàn)樵?Swift 中,返回值如果沒有被使用,Xcode 會產(chǎn)生告警信息。加上這個(gè)標(biāo)簽之后,表示這個(gè)方法的返回值就算沒有被使用,也不產(chǎn)生告警。
ObjectMapper引入 ObjectMapper 很大一部分原因是需要做錯(cuò)誤和成功提示。因?yàn)橹挥薪馕龇?wù)端的錯(cuò)誤信息節(jié)點(diǎn)才能知道返回結(jié)果是否正確,所以我們引入  ObjectMapper 來做 JSON 解析。 而只做 JSON 解析的原因是目前主流的服務(wù)端客戶端數(shù)據(jù)交互格式是 JSON。
這里需要提到的就是另外一個(gè) Alamofire 的擴(kuò)展庫 AlamofireObjectMapper,從名字就可以看出來,這個(gè)庫就是參照 Alamofire 的 API 規(guī)范來做  ObjectMapper 做的事情。這個(gè)庫的代碼很少,但實(shí)現(xiàn)方式非常 Alamofire,大家可以拜讀一下它的源碼,基本上就知道如何基于 Alamofire 做自定義數(shù)據(jù)解析了。
注:被 @Foolish 安利,正在接入 ProtoBuf 中…
Alamofire 的請求有三種: request、upload 和 download,這三種請求都有相應(yīng)的參數(shù),MBNetwork 把這些參數(shù)抽象成了對應(yīng)的協(xié)議,具體內(nèi)容參見:MBForm.swift。這種做法有幾個(gè)優(yōu)點(diǎn):
headers 這樣的參數(shù),一般全局都是一致的,可以直接 extension 指定。通過協(xié)議的名字即可知道表單的功能,簡單明確。下面是 MBNetwork 表單協(xié)議的用法舉例:
指定全局 headers 參數(shù):
創(chuàng)建具體業(yè)務(wù)表單:
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}表單協(xié)議化可能有過度設(shè)計(jì)的嫌疑,有同感的仍然可以使用 Alamofire 對應(yīng)的接口去做網(wǎng)絡(luò)請求,不影響 MBNetwork 其他功能的使用。
表單已經(jīng)抽象成協(xié)議,現(xiàn)在就可以基于表單發(fā)送網(wǎng)絡(luò)請求了,因?yàn)橹耙呀?jīng)說過需要在任意位置發(fā)送網(wǎng)絡(luò)請求,而實(shí)現(xiàn)這一點(diǎn)的方法基本就這幾種:
單例。全局方法,Alamofire 就是這么干的。協(xié)議擴(kuò)展。MBNetwork 采用了最后一種方法。原因很簡單,MBNetwork 是以一切皆協(xié)議的原則設(shè)計(jì)的,所以我們把網(wǎng)絡(luò)請求抽象成 MBRequestable 協(xié)議。
首先,MBRequestable 是一個(gè)空協(xié)議 。
為什么是空協(xié)議,因?yàn)椴恍枰裱@個(gè)協(xié)議的對象干啥。
然后對它做 extension,實(shí)現(xiàn)網(wǎng)絡(luò)請求相關(guān)的一系列接口:
這些就是網(wǎng)絡(luò)請求的接口,參數(shù)是各種表單協(xié)議,接口內(nèi)部調(diào)用的其實(shí)是 Alamofire 對應(yīng)的接口。注意它們都返回了類型為 DataRequest、UploadRequest 或者 DownloadRequest 的對象,通過返回值我們可以繼續(xù)調(diào)用其他方法。
到這里 MBRequestable 的實(shí)現(xiàn)就完成了,使用方法很簡單,只需要設(shè)置類型遵循 MBRequestable 協(xié)議,就可以在該類型內(nèi)發(fā)起網(wǎng)絡(luò)請求。如下:
對于加載我們關(guān)心的點(diǎn)有如下幾個(gè):
加載開始需要干啥。加載結(jié)束需要干啥。是否需要顯示加載遮罩。在何處顯示遮罩。顯示遮罩的內(nèi)容。對于這幾點(diǎn),我對協(xié)議的劃分是這樣的:
MBContainable 協(xié)議。遵循該協(xié)議的對象可以做為加載的容器。MBMaskable 協(xié)議。遵循該協(xié)議的 UIView 可以做為加載遮罩。MBLoadable 協(xié)議。遵循該協(xié)議的對象可以定義加載的配置和流程。MBContainable遵循這個(gè)協(xié)議的對象只需要實(shí)現(xiàn)下面的方法即可:
func containerView() -> UIView?這個(gè)方法返回做為遮罩容器的 UIView。做為遮罩的 UIView 最終會被添加到 containerView 上。
不同類型的容器的 containerView 是不一樣的,下面是各種類型容器 containerView 的列表:
| 容器 | containerView | 
|---|---|
| UIViewController | view | 
| UIView | self | 
| UITableViewCell | contentView | 
| UIScrollView | 最近一個(gè)不是 UIScrollView的superview | 
UIScrollView 這個(gè)地方有點(diǎn)特殊,因?yàn)槿绻苯釉?UIScrollView 上添加遮罩視圖,遮罩視圖的中心點(diǎn)是非常難控制的,所以這里用了一個(gè)技巧,遞歸尋找 UIScrollView 的 superview,發(fā)現(xiàn)不是 UIScrollView 類型的直接返回即可。代碼如下:
最后我們對 MBContainable 做 extension,添加一個(gè) latestMask 方法,這個(gè)方法實(shí)現(xiàn)的功能很簡單,就是返回 containerView 上最新添加的、而且遵循 MBMaskable 協(xié)議的 subview。
MBMaskable協(xié)議內(nèi)部只定義了一個(gè)屬性 maskId,作用是用來區(qū)分多種遮罩。
MBNetwork 內(nèi)部實(shí)現(xiàn)了兩個(gè)遵循 MBMaskable 協(xié)議的 UIView,分別是 MBActivityIndicator 和 MBMaskView,其中 MBMaskView 的效果是參照 MBProgressHUD 實(shí)現(xiàn),所以對于大部分場景來說,直接使用這兩個(gè) UIView 即可。
注:MBMaskable 協(xié)議唯一的作用是與 containerView 上其它 subview 做區(qū)分。
MBLoadable做為加載協(xié)議的核心部分,MBLoadable 包含如下幾個(gè)部分:
func mask() -> MBMaskable?:遮罩視圖,可選的原因是可能不需要遮罩。func inset() -> UIEdgeInsets:遮罩視圖和容器視圖的邊距,默認(rèn)值 UIEdgeInsets.zero。func maskContainer() -> MBContainable?:遮罩容器視圖,可選的原因是可能不需要遮罩。func begin():加載開始回調(diào)方法。func end():加載結(jié)束回調(diào)方法。然后對協(xié)議要求實(shí)現(xiàn)的幾個(gè)方法做默認(rèn)實(shí)現(xiàn):
func mask() -> MBMaskable? { return MBMaskView() // 默認(rèn)顯示 MBProgressHUD 效果的遮罩。} func inset() -> UIEdgeInsets { return UIEdgeInsets.zero // 默認(rèn)邊距為 0 。}func maskContainer() -> MBContainable? { return nil // 默認(rèn)沒有遮罩容器。}func begin() { show() // 默認(rèn)調(diào)用 show 方法。}func end() { hide() // 默認(rèn)調(diào)用 hide 方法。}上述代碼中的 show 方法和 hide 方法是實(shí)現(xiàn)加載遮罩的核心代碼。
show 方法的內(nèi)容如下:
這個(gè)方法做了下面幾件事情:
判斷mask 方法返回的是不是遵循 MBMaskable 協(xié)議的 UIView,因?yàn)槿绻皇?UIView,不能被添加到其它的 UIView 上。通過 MBContainable 協(xié)議上的 latestMask 方法獲取最新添加的、且遵循 MBMaskable 協(xié)議的 UIView。如果有,就把新添加的這個(gè)遮罩視圖隱藏起來,再添加到 maskContainer 的 containerView 上。為什么會有多個(gè)遮罩的原因是多個(gè)網(wǎng)絡(luò)請求可能同時(shí)遮罩某一個(gè) maskContainer,另外,多個(gè)遮罩不能都顯示出來,因?yàn)橛械恼谡挚赡苡邪胪该鞑糠郑孕枰鲭[藏操作。至于為什么都要添加到 maskContainer 上,是因?yàn)槲覀儾恢滥膫€(gè)請求會最后結(jié)束,所以就采取每個(gè)請求的遮罩我們都添加,然后結(jié)束一個(gè)請求就移除一個(gè)遮罩,請求都結(jié)束的時(shí)候,遮罩也就都移除了。對 maskContainer 是 UIScrollView 的情況做特殊處理,使其不可滾動(dòng)。然后是 hide 方法,內(nèi)容如下:
相比 show 方法,hide 方法做的事情要簡單一些,通過 MBContainable 協(xié)議上的 latestMask 方法獲取最新添加的、且遵循 MBMaskable 協(xié)議的 UIView,然后從 superview 上移除。對 maskContainer 是 UIScrollView 的情況做特殊處理,當(dāng)被移除的遮罩是最后一個(gè)時(shí),使其可以再滾動(dòng)。
MBLoadType為了降低使用成本,MBNetwork 提供了 MBLoadType 枚舉類型。
none:表示不需要加載。 default:傳入遵循 MBContainable 協(xié)議的 container 附加值。
然后對 MBLoadType 做 extension,使其遵循 MBLoadable 協(xié)議。
這樣對于不需要加載或者只需要指定 maskContainer 的情況(PS:比如全屏遮罩),就可以直接用 MBLoadType 來代替 MBLoadable。
UIControlmaskContainer 就是本身,比如 UIButton,加載時(shí)直接在按鈕上顯示“菊花”即可。mask 需要定制下,不能是默認(rèn)的 MBMaskView,而應(yīng)該是 MBActivityIndicator,然后 MBActivityIndicator “菊花”的顏色和背景色應(yīng)該和 UIControl 一致。加載開始和加載全部結(jié)束時(shí)需要設(shè)置 isEnabled。UIRefreshControlbeginRefreshing 和 endRefreshing。UITableViewCellmaskContainer 就是本身。mask 需要定制下,不能是默認(rèn)的 MBMaskView,而應(yīng)該是 MBActivityIndicator,然后 MBActivityIndicator “菊花”的顏色和背景色應(yīng)該和 UIControl 一致。至此,加載相關(guān)協(xié)議的定義和默認(rèn)實(shí)現(xiàn)都已經(jīng)完成。現(xiàn)在需要做的就是把加載和網(wǎng)絡(luò)請求結(jié)合起來,其實(shí)很簡單,之前 MBRequestable 協(xié)議擴(kuò)展的網(wǎng)絡(luò)請求方法都返回了類型為 DataRequest、UploadRequest 或者 DownloadRequest 的對象,所以我們對它們做 extension,然后實(shí)現(xiàn)下面的 load 方法即可。
傳入?yún)?shù)為遵循 MBLoadable 協(xié)議的 load 對象,默認(rèn)值為 MBLoadType.none。請求開始時(shí)調(diào)用其 begin 方法,請求返回時(shí)調(diào)用其 end 方法。
UIViewController 上顯示加載遮罩
UIButton 上顯示加載遮罩
UITableViewCell 上顯示加載遮罩
UIRefreshControl
除了基本的用法,MBNetwork 還支持對加載進(jìn)行完全的自定義,做法如下:

首先,我們創(chuàng)建一個(gè)遵循 MBLoadable 協(xié)議的類型 LoadConfig。
然后我們就可以這樣使用它了。
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)你會發(fā)現(xiàn)所有的東西都是可以自定義的,而且使用起來仍然很簡單。
下面是利用 LoadConfig 在 UITableView 上顯示自定義加載遮罩的的例子。

進(jìn)度的展示比較簡單,只需要有方法實(shí)時(shí)更新進(jìn)度即可,所以我們先定義 MBProgressable 協(xié)議,內(nèi)容如下:
因?yàn)橐话阒挥猩蟼骱拖螺d大文件才需要進(jìn)度展示,所以我們只對 UploadRequest 和 DownloadRequest 做 extension,添加 progress 方法,參數(shù)為遵循 MBProgressable 協(xié)議的 progress 對象 :
既然是進(jìn)度展示,當(dāng)然得讓 UiprogressView 遵循 MBProgressable 協(xié)議,實(shí)現(xiàn)如下:
然后我們就可以直接把 UIProgressView 對象當(dāng)做 progress 方法的參數(shù)了。

信息提示包括兩個(gè)部分,出錯(cuò)提示和成功提示。所以我們先抽象了一個(gè) MBMessageable 協(xié)議,協(xié)議的內(nèi)容僅僅包含了顯示消息的容器。
毫無疑問,返回的容器當(dāng)然也是遵循 MBContainable 協(xié)議的,這個(gè)容器將被用來展示出錯(cuò)和成功提示。
出錯(cuò)提示需要做的事情有兩步:
解析錯(cuò)誤信息展示錯(cuò)誤信息首先我們來完成第一步,解析錯(cuò)誤信息。這里我們把錯(cuò)誤信息抽象成協(xié)議 MBErrorable,其內(nèi)容如下:
其中 successCodes 用來定義哪些錯(cuò)誤碼是正常的; code 表示當(dāng)前錯(cuò)誤碼;message 定義了展示給用戶的信息。
具體怎么使用這個(gè)協(xié)議后面再說,我們接著看 JSON 錯(cuò)誤解析協(xié)議 MBJSONErrorable。
注意這里的 Mappable 協(xié)議來自 ObjectMapper,目的是讓遵循這個(gè)協(xié)議的對象實(shí)現(xiàn) Mappable 協(xié)議中的 func mapping(map: Map) 方法,這個(gè)方法定義了 JSON 數(shù)據(jù)中錯(cuò)誤信息到 MBErrorable 協(xié)議中 code 和 message 屬性的映射關(guān)系。
假設(shè)服務(wù)端返回的 JSON 內(nèi)容如下:
{ "data": { "code": "200", "message": "請求成功" }}那我們的錯(cuò)誤信息對象就可以定義成下面的樣子。
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 屬性上。至此,錯(cuò)誤信息的解析就完成了。
然后是第二步,錯(cuò)誤信息展示。定義 MBWarnable 協(xié)議:
這個(gè)協(xié)議遵循 MBMessageable 協(xié)議。遵循這個(gè)協(xié)議的對象除了要實(shí)現(xiàn) MBMessageable 協(xié)議的 messageContainer 方法,還需要實(shí)現(xiàn) show 方法,這個(gè)方法只有一個(gè)參數(shù),通過這個(gè)參數(shù)我們傳入遵循錯(cuò)誤信息協(xié)議的對象。
現(xiàn)在我們就可以使用 MBErrorable 和 MBWarnable 協(xié)議來進(jìn)行出錯(cuò)提示了。和之前一樣我們還是對 DataRequest 做 extension。添加 warn 方法。
這個(gè)方法包括三個(gè)參數(shù):
error:遵循 MBJSONErrorable 協(xié)議的泛型錯(cuò)誤解析對象。傳入這個(gè)對象到 AlamofireObjectMapper 的 responseObject 方法中即可獲得服務(wù)端返回的錯(cuò)誤信息。warn:遵循 MBWarnable 協(xié)議的錯(cuò)誤展示對象。 completionHandler:返回結(jié)果正確時(shí)調(diào)用的閉包。業(yè)務(wù)層一般通過這個(gè)閉包來做特殊錯(cuò)誤碼處理。做了如下的事情:
通過 Alamofire 的 response 方法獲取非業(yè)務(wù)錯(cuò)誤信息,如果存在,則調(diào)用 warn 的 show 方法展示錯(cuò)誤信息,這里大家可能會有點(diǎn)疑惑:為什么可以把 String 當(dāng)做 MBErrorable 傳入到 show 方法中?這是因?yàn)槲覀冏隽讼旅娴氖虑椋?/p>extension String: MBErrorable {    public var message: String? {        return self    }}
通過 AlamofireObjectMapper 的 responseObject 方法獲取到服務(wù)端返回的錯(cuò)誤信息,判斷返回的錯(cuò)誤碼是否包含在 successCodes 中,如果是,則交給業(yè)務(wù)層處理;(PS:對于某些需要特殊處理的錯(cuò)誤碼,也可以定義在 successCodes 中,然后在業(yè)務(wù)層單獨(dú)處理。)否則,直接調(diào)用 warn 的 show 方法展示錯(cuò)誤信息。 
相比錯(cuò)誤提示,成功提示會簡單一些,因?yàn)槌晒μ崾拘畔⒁话愣际窃诒镜囟x的,不需要從服務(wù)端獲取,所以成功提示協(xié)議的內(nèi)容如下:
public protocol MBInformable: MBMessageable { func show() func message() -> String}包含兩個(gè)方法, show 方法用于展示信息;message 方法定義展示的信息。
然后對 DataRequest 做擴(kuò)展,添加 inform 方法:
這里同樣也傳入遵循 MBJSONErrorable 協(xié)議的泛型錯(cuò)誤解析對象,因?yàn)槿绻?wù)端的返回結(jié)果是錯(cuò)的,則不應(yīng)該提示成功。還是通過 AlamofireObjectMapper 的 responseObject 方法獲取到服務(wù)端返回的錯(cuò)誤信息,判斷返回的錯(cuò)誤碼是否包含在 successCodes 中,如果是,則通過 inform 對象 的 show 方法展示成功信息。
觀察目前主流 App,信息提示一般是通過 UIAlertController 來展示的,所以我們通過 extension 的方式讓 UIAlertController 遵循 MBWarnable 和 MBInformable 協(xié)議。
發(fā)現(xiàn)這里我們沒有用到 messageContainer,這是因?yàn)閷τ?UIAlertController 來說,它的容器是固定的,使用 UIApplication.shared.keyWindow?.rootViewController? 即可。注意對于MBInformable,直接展示 UIAlertController, 而對于 MBWarnable,則是展示 error 中的 message。
下面是使用的兩個(gè)例子:


這樣就達(dá)到了業(yè)務(wù)層定義展示信息,MBNetwork 自動(dòng)展示的效果,是不是簡單很多?至于擴(kuò)展性,我們還是可以參照 UIAlertController 的實(shí)現(xiàn)添加對其它第三方提示庫的支持。
開發(fā)中……敬請期待
新聞熱點(diǎn)
疑難解答
圖片精選