造轮子 | 怎样设计一个面向协议的 iOS 网络请求库
近期开源了一个面向协议设计的网络请求库 MBNetwork,基于 Alamofire 和 ObjectMapper 实现,目的是简化业务层的网络请求操作。
须要干些啥
对于大部分 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
上显示载入遮罩
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvbW1vYWF5/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="" title="">
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 网络请求库的三部曲[转]
代码示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary 开源项目:Pitaya,适合大 ...
- 动手造轮子:实现一个简单的 AOP 框架
动手造轮子:实现一个简单的 AOP 框架 Intro 最近实现了一个 AOP 框架 -- FluentAspects,API 基本稳定了,写篇文章分享一下这个 AOP 框架的设计. 整体设计 概览 I ...
- 动手造轮子:实现一个简单的 EventBus
动手造轮子:实现一个简单的 EventBus Intro EventBus 是一种事件发布订阅模式,通过 EventBus 我们可以很方便的实现解耦,将事件的发起和事件的处理的很好的分隔开来,很好的实 ...
- C#反射の一个泛型反射实现的网络请求框架
点击下载源码 C#反射の反射详解(点击跳转)C#反射の反射接口(点击跳转)C#反射反射泛型接口(点击跳转)C#反射の一个泛型反射实现的网络请求框架(点击跳转)
- 重复造轮子,编写一个轻量级的异步写日志的实用工具类(LogAsyncWriter)
一说到写日志,大家可能推荐一堆的开源日志框架,如:Log4Net.NLog,这些日志框架确实也不错,比较强大也比较灵活,但也正因为又强大又灵活,导致我们使用他们时需要引用一些DLL,同时还要学习各种用 ...
- 线程安全-一个VC下多个网络请求
一.线程安全变量控制显示隐藏loading框 问题描写叙述: 同一页面有两个异步网络请求,第一个请求開始,loading旋转.第二个请求開始loading旋转.第一个结束,loading停止旋转,但是 ...
- 造轮子系列(三): 一个简单快速的html虚拟语法树(AST)解析器
前言 虚拟语法树(Abstract Syntax Tree, AST)是解释器/编译器进行语法分析的基础, 也是众多前端编译工具的基础工具, 比如webpack, postcss, less等. 对于 ...
- 【造轮子】打造一个简单的万能Excel读写工具
大家工作或者平时是不是经常遇到要读写一些简单格式的Excel? shit!~很蛋疼,因为之前吹牛,就搞了个这东西,还算是挺实用,和大家分享下. 厌烦了每次搞简单类型的Excel读写?不怕~来,喜欢流式 ...
- 造轮子:实现一个简易的 Spring IoC 容器
作者:DeppWang.原文地址 我通过实现一个简易的 Spring IoC 容器,算是入门了 Spring 框架.本文是对实现过程的一个总结提炼,需要配合源码阅读,源码地址. 结合本文和源码,你应该 ...
随机推荐
- In-Place upgrade to Team Foundation Server (TFS) 2015 from TFS 2013Team Foundation Server TFS TFS 2015 TFS upgrade TFS with Sharepoint
This upgrade document gives detailed step by step procedure for the In-Place upgrade from TFS 2013 t ...
- [Win32]获取指定进程的父进程PID
// // #include <Windows.h> #include <winnt.h> #include <winternl.h> typedef NTSTAT ...
- ECSHOP商城网站建设之自定义调用广告方法(二)
原文地址:http://www.cnblogs.com/zgzy/p/3598991.html 使用ecshop进行商城网站建设时,ecshop默认的很多功能对于我们个性化设计之后不太使用.今天我们主 ...
- Struts2 注解模式
相信大家一定看到了两个class中定义了一样的action,不过看类的元数据,是不同的命名空间.这里比较重要(对我来说)的是 @Action(value = "/login", r ...
- 在NDK C++线程中如何调用JAVA API
from://http://www.eoeandroid.com/thread-150995-1-1.html 在NDK中创建的线程中, 只允许调用静态的Java API. 当在线程中调用env-&g ...
- android4.0 中关于内外置sd卡的获取及读写权限问题
from://http://blog.chinaunix.net/uid-26727976-id-3146895.html 在2.x的版本中,在manifest中配置的权限android.permis ...
- HBase的JavaAPI使用
Java Client API Overview HBase是用Java写的,支持用编程语言来动态操作管理数据库,能用命令行做的都能够用API来做. 主要的使用步骤例如以下: 1.创建一个 Confi ...
- python测试开发django-25.表单提交之post注册案例
前言 一个网站上新用户注册,会写个注册页面,如果用django写个注册页面的流程呢? 本篇以post请求示例,从html页面上输入用户注册信息,提交到后台处理数据,然后传参数据到User数据库表里面 ...
- CentOS安装sctp协议
转自:http://blog.csdn.net/fly_yr/article/details/48375247 序 最近学习Unix网络编程,在第10章节,SCTP客户/服务器 程序实现时,发现很多由 ...
- SharePoint PowerShell 批量删除遗弃视图
前言 最近,给SharePoint升级了,然后发现,有一大批视图不需要了,而且,名字是一样的,想着怎么清理,然后,就想到了powershell. powershell 示例: $siteUrl = & ...