近期開源了一個面向協議設計的網絡請求庫 MBNetwork,基於 Alamofire 和 ObjectMapper 實現,目的是簡化業務層的網絡請求操做。git
對於大部分 App 而言,業務層作一次網絡請求一般關心的問題有例如如下幾個:github
包括請求地址、請求方式(GET/POST/……)、請求頭等……sql
目的是堵塞 UI 交互,同一時候告知用戶操做正在進行。json
比方提交表單時在提交按鈕上顯示 「菊花」,同一時候使其失效。swift
下載上傳圖片等資源時提示用戶當前進度。ruby
下載上傳圖片等資源錯誤發生時可以在以前已完畢部分的基礎上繼續操做,這個 Alamofire 可以支持。markdown
因爲眼下主流服務端和client數據交換採用的格式是 JSON,因此咱們臨時先考慮 JSON 格式的數據解析,這個 ObjectMapper 可以支持。網絡
請求正常結束時提示用戶。閉包
顯示網絡異常界面,點擊以後又一次發送請求。app
關於 POP 和 OOP 這兩種設計思想及其特色的文章很是多。因此我就不廢話了。主要說說爲啥要用 POP 來寫 MBNetwork。
很是多人都喜歡說 Alamofire 是 Swift 版本號的 AFNetworking,但是在我看來。Alamofire 比 AFNetworking 更純粹。這和 Swift 語言自己的特性也是有關係的,Swift 開發人員們。更喜歡寫一些輕量的框架。
比方 AFNetworking 把很是多 UI 相關的擴展功能都作在框架內。而 Alamofire 的作法則是放在另外的擴展庫中。比方 AlamofireImage 和 AlamofireNetworkActivityIndicator
而 MBNetwork 就可以當作是 Alamofire 的一個擴展庫,因此,MBNetwork 很是大程度上遵循了 Alamofire 接口的設計規範。
一方面。減小了 MBNetwork 的學習成本,還有一方面。從我的角度來看。Alamofire 確實有很是多特別值得借鑑的地方。
首先固然是 POP 啦,Alamofire 大量運用了 protocol
+ extension
的實現方式。
enum
作爲檢驗寫 Swift 姿式正確與否的重要指標。Alamofire 固然不會缺。
這是讓 Alamofire 成爲一個優雅的網絡框架的重要緣由之中的一個。這一點 MBNetwork 也進行了全然的 Copy。
@discardableResult
在 Alamofire 全部帶返回值的方法前面,都會有這麼一個標籤,事實上做用很是easy,因爲在 Swift 中,返回值假設沒有被使用,Xcode 會產生告警信息。加上這個標籤以後,表示這種方法的返回值就算沒有被使用。也不產生告警。
引入 ObjectMapper 很是大一部分緣由是需要作錯誤和成功提示。因爲僅僅有解析服務端的錯誤信息節點才幹知道返回結果是否正確,因此咱們引入 ObjectMapper 來作 JSON 解析。
而僅僅作 JSON 解析的緣由是眼下主流的服務端client數據交互格式是 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 其餘功能的使用。
表單已經抽象成協議,現在就可以基於表單發送網絡請求了,因爲以前已經說過需要在任何位置發送網絡請求,而實現這一點的方法基本就這幾種:
MBNetwork 採用了最後一種方法。緣由很是easy。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
的實現就完畢了。使用方法很是easy,僅僅需要設置類型遵循 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
方法,這種方法實現的功能很是easy,就是返回 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
一致。至此,載入相關協議的定義和默認實現都已經完畢。
現在需要作的就是把載入和網絡請求結合起來。事實上很是easy。以前 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)
你會發現全部的東西都是可以本身定義的,而且使用起來仍然很是easy。
如下是利用 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
的實現加入對其餘第三方提示庫的支持。
開發中……敬請期待