造轮子 | 怎样设计一个面向协议的 iOS 网络请求库
2017-08-08 14:30
661 查看
近期开源了一个面向协议设计的网络请求库 MBNetwork,基于 Alamofire 和 ObjectMapper 实现,目的是简化业务层的网络请求操作。
怎样在任何位置发起网络请求。
表单创建。
包括请求地址、请求方式(GET/POST/……)、请求头等……
载入遮罩。
目的是堵塞 UI 交互,同一时候告知用户操作正在进行。
比方提交表单时在提交按钮上显示 “菊花”,同一时候使其失效。
载入进度展示。下载上传图片等资源时提示用户当前进度。
断点续传。下载上传图片等资源错误发生时能够在之前已完毕部分的基础上继续操作。这个 Alamofire 能够支持。
数据解析。由于眼下主流服务端和client数据交换採用的格式是 JSON,所以我们临时先考虑 JSON 格式的数据解析,这个 ObjectMapper 能够支持。
出错提示。发生业务异常时,直接显示服务端返回的异常信息。前提是服务端异常信息足够友好。
成功提示。请求正常结束时提示用户。
网络异常又一次请求。显示网络异常界面,点击之后又一次发送请求。
想尝试一下一切皆协议的设计方式。所以这个库的设计仅仅是一次极限尝试,并不代表这就是最完美的设计方式。
假设以 OOP 的方式实现,使用者须要通过继承的方式来获得某个类实现的功能。假设使用者还须要另外某个类实现的功能,就会非常尴尬。
而 POP 是通过对协议进行扩展来实现功能。使用者能够同一时候遵循多个协议。轻松解决 OOP 的这个硬伤。
OOP 继承的方式会使某些子类获得它们不须要的功能。
假设由于业务的增多,须要对某些业务进行分离,OOP 的方式还是会碰到子类不能继承多个父类的问题,而 POP 则全然不会,分离之后。仅仅须要遵循分离后的多个协议就可以。
OOP 继承的方式入侵性比較强。
POP 能够通过扩展的方式对各个协议进行默认实现,减少使用者的学习成本。
同一时候 POP 还能让使用者对协议做自己定义的实现,保证其高度可配置性。
比方 AlamofireImage 和 AlamofireNetworkActivityIndicator
而 MBNetwork 就能够当做是 Alamofire 的一个扩展库,所以,MBNetwork 非常大程度上遵循了 Alamofire 接口的设计规范。一方面。减少了 MBNetwork 的学习成本,还有一方面。从个人角度来看。Alamofire 确实有非常多特别值得借鉴的地方。
做为检验写 Swift 姿势正确与否的重要指标。Alamofire 当然不会缺。
在 Alamofire 所有带返回值的方法前面。都会有这么一个标签,事实上作用非常easy,由于在 Swift 中,返回值假设没有被使用,Xcode 会产生告警信息。加上这个标签之后,表示这种方法的返回值就算没有被使用,也不产生告警。
由于仅仅有解析服务端的错误信息节点才干知道返回结果是否正确,所以我们引入 ObjectMapper 来做 JSON 解析。 而仅仅做 JSON 解析的原因是眼下主流的服务端client数据交互格式是 JSON。
这里须要提到的就是另外一个 Alamofire 的扩展库 AlamofireObjectMapper。从名字就能够看出来。这个库就是參照 Alamofire 的 API 规范来做 ObjectMapper 做的事情。这个库的代码非常少。但实现方式非常 Alamofire。大家能够拜读一下它的源代码。基本上就知道怎样基于 Alamofire 做自己定义数据解析了。
注:被 @Foolish 安利,正在接入 ProtoBuf 中…
对于相似
通过协议的名字就可以知道表单的功能。简单明白。
以下是 MBNetwork 表单协议的使用方法举例:
指定全局
创建详细业务表单:
表单协议化可能有过度设计的嫌疑。有同感的仍然能够使用 Alamofire 相应的接口去做网络请求,不影响 MBNetwork 其他功能的使用。
单例。
全局方法。Alamofire 就是这么干的。
协议扩展。
MBNetwork 採用了最后一种方法。
原因非常easy。MBNetwork 是以一切皆协议的原则设计的。所以我们把网络请求抽象成
首先,
为什么是空协议,由于不须要遵循这个协议的对象干啥。
然后对它做
这些就是网络请求的接口。參数是各种表单协议,接口内部调用的事实上是 Alamofire 相应的接口。注意它们都返回了类型为
到这里
例如以下:
载入開始须要干啥。
载入结束须要干啥。
是否须要显示载入遮罩。
在何处显示遮罩。
显示遮罩的内容。
对于这几点,我对协议的划分是这种:
遵循该协议的对象能够做为载入的容器。
遵循该协议的对象能够定义载入的配置和流程。
遵循这个协议的对象仅仅须要实现以下的方法就可以:
这种方法返回做为遮罩容器的
不同类型的容器的
最后我们对
协议内部仅仅定义了一个属性
MBNetwork 内部实现了两个遵循
注:
做为载入协议的核心部分,
然后对协议要求实现的几个方法做默认实现:
上述代码中的
这种方法做了以下几件事情:
推断
通过
假设有。就把新加入的这个遮罩视图隐藏起来,再加入到
为什么会有多个遮罩的原因是多个网络请求可能同一时候遮罩某一个
对
然后是
相比
为了减少使用成本,MBNetwork 提供了
然后对
这样对于不须要载入或者仅仅须要指定
载入開始和载入所有结束时须要设置
不须要显示载入遮罩。
载入開始和载入所有结束时须要调用
传入參数为遵循
在
在
首先,我们创建一个遵循
然后我们就能够这样使用它了。
你会发现所有的东西都是能够自己定义的,并且使用起来仍然非常easy。
以下是利用
由于一般仅仅有上传和下载大文件才须要进度展示。所以我们仅仅对
然后我们就能够直接把
毫无疑问,返回的容器当然也是遵循
解析错误信息
展示错误信息
首先我们来完毕第一步。解析错误信息。
这里我们把错误信息抽象成协议
当中
详细怎么使用这个协议后面再说,我们接着看 JSON 错误解析协议
注意这里的
假设服务端返回的 JSON 内容例如以下:
那我们的错误信息对象就能够定义成以下的样子。
ObjectMapper 会把
至此。错误信息的解析就完毕了。
然后是第二步,错误信息展示。定义
这个协议遵循
如今我们就能够使用
加入
这种方法包括三个參数:
传入这个对象到 AlamofireObjectMapper 的
做了例如以下的事情:
通过 Alamofire 的
通过 AlamofireObjectMapper 的
包括两个方法。
然后对
这里相同也传入遵循
还是通过 AlamofireObjectMapper 的
发现这里我们没实用到
以下是使用的两个样例:
这样就达到了业务层定义展示信息,MBNetwork 自己主动展示的效果,是不是简单非常多?至于扩展性。我们还是能够參照
须要干些啥
对于大部分 App 而言,业务层做一次网络请求通常关心的问题有例如以下几个:怎样在任何位置发起网络请求。
表单创建。
包括请求地址、请求方式(GET/POST/……)、请求头等……
载入遮罩。
目的是堵塞 UI 交互,同一时候告知用户操作正在进行。
比方提交表单时在提交按钮上显示 “菊花”,同一时候使其失效。
载入进度展示。下载上传图片等资源时提示用户当前进度。
断点续传。下载上传图片等资源错误发生时能够在之前已完毕部分的基础上继续操作。这个 Alamofire 能够支持。
数据解析。由于眼下主流服务端和client数据交换採用的格式是 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 所有带返回值的方法前面。都会有这么一个标签,事实上作用非常easy,由于在 Swift 中,返回值假设没有被使用,Xcode 会产生告警信息。加上这个标签之后,表示这种方法的返回值就算没有被使用,也不产生告警。当然还有 ObjectMapper
引入 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 其他功能的使用。
基于表单请求数据
表单已经抽象成协议。如今就能够基于表单发送网络请求了。由于之前已经说过须要在任何位置发送网络请求,而实现这一点的方法基本就这几种:单例。
全局方法。Alamofire 就是这么干的。
协议扩展。
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的实现加入对其他第三方提示库的支持。
又一次请求
开发中……敬请期待相关文章推荐
- 造轮子 | 如何设计一个面向协议的 iOS 网络请求库
- 如何设计一个面向协议的 iOS 网络请求库
- 自己动手写一个 iOS 网络请求库(二)——封装接口
- ios网络:应用一个请求的7个步骤
- Android 框架设计Demo,一个简单的MVP示例搜索功能,网络请求用Retrofit+RxJava实现
- swift 面向协议的网络请求封装
- 自己动手写一个 iOS 网络请求库(NSURLSession 初探、封装接口、降低耦合、快速文件上传 )
- 怎样使用ListView实现一个带有网络请求,解析,分页,缓存的公共的List页面来大大的提高工作效率
- iOS使用AFNetworking请求回来的网络数据,不能显示中文, 新建一个分类解决。
- iOS 网络图片处理问题中,怎么解决一个相同的网络地址重复请求的问题
- 一个iOS 网络请求框架介绍:MKNetworkKit
- Java&Android开源库代码剖析】のandroid-async-http(如何设计一个优雅的Android网络请求框架,同...
- 自己动手写一个 iOS 网络请求库(一)—— NSURLSession 初探
- iOS开发中大部分App的网络数据交换是基于HTTP协议的。本文将简单介绍在Swift中使用HTTP进行网络请求的几种方法。
- 自己动手写一个 iOS 网络请求库(三)——降低耦合
- swift--用 Swift 编写面向协议的网络请求
- 自己动手写一个 iOS 网络请求库(五)——设置 SSL 钢钉
- iOS网络: 通过NSMutableURLRequest修改一个URL的请求
- iOS设计模式-开发思路提问(系列1:一个Button的三种状态怎样切换?)
- IOS开发—网络请求(HTTP协议)介绍