近期开源了一个面向协议设计的网络请求库 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
的实现加入对其余第三方提示库的支持。
开发中……敬请期待