最近开源了一个面向协议设计的网络请求库 ,基于
和
- Alamofire
实现,目的是简化业务层的网络请求操作。
- ObjectMapper
对于大部分 App 而言,业务层做一次网络请求通常关心的问题有如下几个:
可以支持。
- 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
把很多 UI 相关的扩展功能都做在框架内,而
- AFNetworking
的做法则是放在另外的扩展库中。比如 和
- Alamofire
而
就可以当做是
- MBNetwork
的一个扩展库,所以,
- Alamofire
很大程度上遵循了
- MBNetwork
接口的设计规范。一方面,降低了
- Alamofire
的学习成本,另一方面,从个人角度来看,
- MBNetwork
确实有很多特别值得借鉴的地方。
- Alamofire
- POP
首先当然是
啦,
- POP
大量运用了
- Alamofire
+
- protocol
的实现方式。
- extension
- enum
做为检验写
姿势正确与否的重要指标,
- Swift
当然不会缺。
- Alamofire
这是让
成为一个优雅的网络框架的重要原因之一。这一点
- Alamofire
也进行了完全的 Copy。
- MBNetwork
- @discardableResult
- ObjectMapper
引入
很大一部分原因是需要做错误和成功提示。因为只有解析服务端的错误信息节点才能知道返回结果是否正确,所以我们引入
- ObjectMapper
来做
- ObjectMapper
解析。 而只做
- JSON
解析的原因是目前主流的服务端客户端数据交互格式是
- JSON
。
- JSON
这里需要提到的就是另外一个
的扩展库 ,从名字就可以看出来,这个库就是参照
- Alamofire
的 API 规范来做
- Alamofire
做的事情。这个库的代码很少,但实现方式非常
- ObjectMapper
,大家可以拜读一下它的源码,基本上就知道如何基于
- Alamofire
做自定义数据解析了。
- Alamofire
的请求有三种:
- Alamofire
、
- request
和
- upload
,这三种请求都有相应的参数,
- download
把这些参数抽象成了对应的协议,具体内容参见:。这种做法有几个优点:
- MBNetwork
这样的参数,一般全局都是一致的,可以直接 extension 指定。
- headers
下面是
表单协议的用法举例:
- 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
协议。
- 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
容器 |
|
---|---|
|
|
|
|
|
|
|
最近一个不是 的
|
这个地方有点特殊,因为如果直接在
- 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
的效果是参照
- MBMaskView
实现,所以对于大部分场景来说,直接使用这两个
- MBProgressHUD
即可。
- UIView
- 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
- }
- }
- }
这样对于不需要加载或者只需要指定
的情况(PS:比如全屏遮罩),就可以直接用
- maskContainer
来代替
- 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
方法,这个方法定义了 JSON 数据中错误信息到
- func mapping(map: Map)
协议中
- 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
做 extension。添加
- DataRequest
方法。
- warn
- func warn(
- 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) 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
中,如果是,则交给业务层处理;(PS:对于某些需要特殊处理的错误码,也可以定义在
- successCodes
中,然后在业务层单独处理。)否则,直接调用
- successCodes
的
- warn
方法展示错误信息。
- show
相比错误提示,成功提示会简单一些,因为成功提示信息一般都是在本地定义的,不需要从服务端获取,所以成功提示协议的内容如下:
- public protocol MBInformable: MBMessageable {
- func show()
- func message() -> String
- }
包含两个方法,
方法用于展示信息;
- show
方法定义展示的信息。
- message
然后对
做扩展,添加
- DataRequest
方法:
- inform
- func inform(error: T, inform: MBInformable) -> Self {
- return responseObject(queue: nil, keyPath: nil, mapToObject: nil, context: nil) { (response: DataResponse) 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,信息提示一般是通过
来展示的,所以我们通过 extension 的方式让
- UIAlertController
遵循
- 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
开发中…… 敬请期待
来源: