Alamofire源码解读系列(五)之结果封装(Result)
本篇讲解Result的封装
前言
有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解。在Alamofire中,使用Response
来描述请求后的结果。我们都知道Alamofire返回的数据可以经过特殊的处理,比如说序列化,那么我们应该如何在Response
中获取到这些类型不同的数据呢?
假如说序列化后的数据是data,最直接的想法就是把data设置为Any类型,在实际用到的时候在进行判断,这也是最普通的一种开发思维。现在我们就要打破这种思维。我们需要封装一个对象,这个对象能够表达任何结果,这就用到了swift中的泛型。
接下来在讲解Result
之后,会给出两个使用泛型的例子,第一个例子表达基本的网络封装思想,第二个表达基本的viewModel思想。
Result
/// Used to represent whether a request was successful or encountered an error.
///
/// - success: The request and all post processing operations were successful resulting in the serialization of the
/// provided associated value.
///
/// - failure: The request encountered an error resulting in a failure. The associated values are the original data
/// provided by the server as well as the error that caused the failure.
public enum Result<Value> {
case success(Value)
case failure(Error)
}
关于如何描述结果
,有两种可能,不是成功就是失败,因此考虑使用枚举。在Alamofire源码解读系列(二)之错误处理(AFError)这篇文章中我已经详细的讲解了枚举的使用方法。在上边的代码中,对枚举的每个子选项都做了值关联。
大家注意,泛型的写法是类似这样的:,在<和>之间声明一种类型,这个T知识象征性的,在赋值的时候,可以是任何类型。还有一种用法,看下边的代码:
struct CellConfigurator<Cell> where Cell: Updatable, Cell: UITableViewCell {
}
上边代码中的Cell必须符合后边给出的两个条件才行,这种用法是给泛型增加了条件限制,这种用法还有另外一种方式,看下边的代码:
func send<T: Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void);
其实道理都差不多,都属于对泛型的灵活运用。
我们接着看看在Alamofire中是如何使用Result的。
@discardableResult
public func responseJSON(
queue: DispatchQueue? = nil,
options: JSONSerialization.ReadingOptions = .allowFragments,
completionHandler: @escaping (DataResponse<Any>) -> Void)
-> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.jsonResponseSerializer(options: options),
completionHandler: completionHandler
)
}
上边的这个函数的主要目的是把请求成功后的结果序列化为JSON,completionHandler函数的参数类型为DataResponse,其中的Any就会传递给Result,也就是Result。
那么问题来了,不是把数据解析成JSON了吗?为什么要返回Any类型呢?json本质上很类似于JavaScript中的对象和数组。JSONSerialization.jsonObject返回的类型是Any,这是因为解析后的数据有可能是数组,也有可能是字典。
字典:
{
"people":[
{"firstName":"Brett","lastName":"McLaughlin","email":"aaaa"},
{"firstName":"Jason","lastName":"Hunter","email":"bbbb"},
{"firstName":"Elliotte","lastName":"Harold","email":"cccc"}
]
}
数组:
[
"a",
"b",
"c"
]
当然如果不是这两种格式的数据,使用JSONSerialization.jsonObject解析会抛出异常。
到这里我们就大概对这个Result有了一定的了解,下边的代码给result添加了一些属性,主要目的是使用起来更方便:
/// Returns `true` if the result is a success, `false` otherwise.
public var isSuccess: Bool {
switch self {
case .success:
return true
case .failure:
return false
}
}
/// Returns `true` if the result is a failure, `false` otherwise.
public var isFailure: Bool {
return !isSuccess
}
/// Returns the associated value if the result is a success, `nil` otherwise.
public var value: Value? {
switch self {
case .success(let value):
return value
case .failure:
return nil
}
}
/// Returns the associated error value if the result is a failure, `nil` otherwise.
public var error: Error? {
switch self {
case .success:
return nil
case .failure(let error):
return error
}
}
当然,为了打印更加详细的信息,使Result实现了CustomStringConvertible
和CustomDebugStringConvertible
协议 :
// MARK: - CustomStringConvertible
extension Result: CustomStringConvertible {
/// The textual representation used when written to an output stream, which includes whether the result was a
/// success or failure.
public var description: String {
switch self {
case .success:
return "SUCCESS"
case .failure:
return "FAILURE"
}
}
}
// MARK: - CustomDebugStringConvertible
extension Result: CustomDebugStringConvertible {
/// The debug textual representation used when written to an output stream, which includes whether the result was a
/// success or failure in addition to the value or error.
public var debugDescription: String {
switch self {
case .success(let value):
return "SUCCESS: \(value)"
case .failure(let error):
return "FAILURE: \(error)"
}
}
}
总起来说,Result是一个比较简单的封装。
基于泛型的网络封装
在实际的开发工作中,我们使用Alamofire发送请求,获取服务器的数据,往往会对其进行二次封装,在这里,我讲解一个封装的例子,内容来自面向协议编程与 Cocoa 的邂逅
我们需要一个协议,这个协议提供一个函数,目的是把Data转换成实现该协议的对象本身。注意我们在这时候是不知道这个对象的类型的,为了适配更多的类型,这个对象暂时设计为泛型,因此协议中的函数应该是静态函数
protocol Decodable {
static func parse(data: Data) -> Self?
}
封装请求,同样采用协议的方式
public enum JZGHTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT" } protocol Request { var path: String { get }
var privateHost: String? { get } var HTTPMethod: JZGHTTPMethod { get }
var timeoutInterval: TimeInterval { get }
var parameter: [String: Any]? { get } associatedtype Response: Decodable
}
封装发送端,同样采用协议的方式
protocol Client { var host: String { get } func send<T: Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void);
}
只要是实现了Client协议的对象,就有能力发送请求,在这里Alamofire是作为中间层存在的,只提供请求能力,可以随意换成其他的中间能力层
struct AlamofireClient: Client { public static let `default` = { AlamofireClient() }() public enum HostType: String {
case sandbox = "https://httpbin.org/post"
} /// Base host URL
var host: String = HostType.sandbox.rawValue func send<T : Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void) { let url = URL(string: r.privateHost ?? host.appending(r.path))! let sessionManager = Alamofire.SessionManager.default
sessionManager.session.configuration.timeoutIntervalForRequest = r.timeoutInterval Alamofire.request(url, method: HTTPMethod(rawValue: r.HTTPMethod.rawValue)!,
parameters: r.parameter,
encoding: URLEncoding.default,
headers: nil)
.response { (response) in if let data = response.data, let res = T.Response.parse(data: data) { handler(res, nil) }else { handler(nil, response.error?.localizedDescription)
}
}
} }
封装完成之后,我们来使用一下上边封装的功能:
创建一个TestRequest.swift文件,内部代码为:
struct TestRequest: Request {
let name: String
let userId: String var path: String {
return ""
} var privateHost: String? {
return nil
} var timeoutInterval: TimeInterval {
return 20.0
} var HTTPMethod: JZGHTTPMethod {
return .post
} var parameter: [String : Any]? {
return ["name" : name,
"userId" : userId]
} typealias Response = TestResult
}
创建TestResult.swift文件,内部代码为:
struct TestResult {
var origin: String
} extension TestResult: Decodable {
static func parse(data: Data) -> TestResult? {
do {
let dic = try JSONSerialization.jsonObject(with: data, options: .allowFragments) guard let dict = dic as? Dictionary<String, Any> else {
return nil
}
return TestResult(origin: dict["origin"] as! String)
}catch {
return nil
}
}
}
发送请求
let request = TestRequest(name: "mama", userId: "12345");
AlamofireClient.default.send(request) { (response, error) in
print(response)
}
对网络的基本封装就到此为止了 ,这里的Result可以是任何类型的对象,比如说User,可以通过上边的方法,直接解析成User对象。
基于泛型的cell封装
这种设计通常应用在MVVM之中,我们看下边的代码:
定义一个协议,这个协议提供一个函数,函数会提供一个参数,这个参数就是viewModel。cell只要实现了这个协议,就能够通过这个参数拿到viewModel,然后根据viewModel来配置自身控件的属性。
protocol Updatable: class { associatedtype ViewData func update(viewData: ViewData)
}
再定义一个协议,这个协议需要表示cell的一些信息,比如reuseIdentifier,cellClass,同时,这个协议还需要提供一个方法,赋予cell适配器更新cell的能力
protocol CellConfiguratorType { var reuseIdentifier: String { get }
var cellClass: AnyClass { get } func update(cell: UITableViewCell)
}
创建CellConfigurator,这个CellConfigurator必须绑定一个viewData,这个viewData通过Updatable协议中的方法传递给cell
struct CellConfigurator<Cell> where Cell: Updatable, Cell: UITableViewCell { let viewData: Cell.ViewData
let reuseIdentifier: String = NSStringFromClass(Cell.self)
let cellClass: AnyClass = Cell.self func update(cell: UITableViewCell) {
if let cell = cell as? Cell {
cell.update(viewData: viewData)
}
}
}
万变不离其宗啊,我们在请求到数据之后,需要把数据转变成CellConfigurator,也就是在数组中存放的是CellConfigurator类型的数据。
看看使用示例:
创建数组
let viewController = ConfigurableTableViewController(items: [
CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Foo")),
CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "og")!)),
CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "GoogleLogo")!)),
CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Bar")),
])
注册cell
func registerCells() {
for cellConfigurator in items {
tableView.register(cellConfigurator.cellClass, forCellReuseIdentifier: cellConfigurator.reuseIdentifier)
}
}
配置cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellConfigurator = items[(indexPath as NSIndexPath).row]
let cell = tableView.dequeueReusableCell(withIdentifier: cellConfigurator.reuseIdentifier, for: indexPath)
cellConfigurator.update(cell: cell)
return cell
}
这个cell封装思想出自这里https://github.com/fastred/ConfigurableTableViewController
总结
上边两个例子,我解释的并不是很详细,只需要打开源码,仔细琢磨琢磨就能体会到里边的妙处,如有问题,可以留言。
在这里获取代码:https://github.com/agelessman/TTestDemo
由于知识水平有限,如有错误,还望指出
链接
Alamofire源码解读系列(一)之概述和使用 简书博客园
Alamofire源码解读系列(二)之错误处理(AFError) 简书博客园
Alamofire源码解读系列(三)之通知处理(Notification) 简书博客园
Alamofire源码解读系列(四)之参数编码(ParameterEncoding) 简书博客园
Alamofire源码解读系列(五)之结果封装(Result)的更多相关文章
- Alamofire源码解读系列(九)之响应封装(Response)
本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...
- Alamofire源码解读系列(六)之Task代理(TaskDelegate)
本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...
- Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager)
Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 本篇主要讲解iOS开发中的网络监控 前言 在开发中,有时候我们需要获取这些信息: 手机是否联网 ...
- Alamofire源码解读系列(八)之安全策略(ServerTrustPolicy)
本篇主要讲解Alamofire中安全验证代码 前言 作为开发人员,理解HTTPS的原理和应用算是一项基本技能.HTTPS目前来说是非常安全的,但仍然有大量的公司还在使用HTTP.其实HTTPS也并不是 ...
- Alamofire源码解读系列(十)之序列化(ResponseSerialization)
本篇主要讲解Alamofire中如何把服务器返回的数据序列化 前言 和前边的文章不同, 在这一篇中,我想从程序的设计层次上解读ResponseSerialization这个文件.更直观的去探讨该功能是 ...
- Alamofire源码解读系列(十一)之多表单(MultipartFormData)
本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...
- Alamofire源码解读系列(十二)之时间轴(Timeline)
本篇带来Alamofire中关于Timeline的一些思路 前言 Timeline翻译后的意思是时间轴,可以表示一个事件从开始到结束的时间节点.时间轴的概念能够应用在很多地方,比如说微博的主页就是一个 ...
- Alamofire源码解读系列(十二)之请求(Request)
本篇是Alamofire中的请求抽象层的讲解 前言 在Alamofire中,围绕着Request,设计了很多额外的特性,这也恰恰表明,Request是所有请求的基础部分和发起点.这无疑给我们一个Req ...
- Alamofire源码解读系列(二)之错误处理(AFError)
本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...
随机推荐
- Eclipse和debug的一些快捷键
F5单步调试进入函数内部. F6单步调试不进入函数内部,如果装了金山词霸2006则要把“取词开关”的快捷键改成其他的. F7由函数内部返回到调用处. F8一直执行到下一个断点. F11 这个好 ...
- iOS 之 深复制、浅复制
深复制不仅复制对象本身,对象持有的属性对象也做了复制. 浅复制之复制对象本身,不对里面的属性进行复制.
- JavaScript 扯几句单线程相关
JavaScript 扯几句单线程相关 众所周知,Javascript是单线程执行的,这也就是说:JavaScript在同一个时间上只能处理一件事.他不像C,Java等这些多 线程的,可以开不同的线程 ...
- 使用DatePickerDialog、TimePickerDialog
DatePickerDialog与TimerPicker的功能比较简单,用户也简单,只要如下两步即可. ①通过new关键字创建DatePickerDialog.TimePickerDialog实例,调 ...
- Could not execute auto check for display colors using command /usr/bin/xdpyinfo.(
Steps to resolve this issue: 1) login into root user( su -l root) 2) execute this command : xhost +S ...
- Web应用中监听者的通知顺序按照DD中的定义顺序
Web应用中监听者的通知顺序按照DD中的定义顺序: XML: <?xml version="1.0" encoding="UTF-8"?> < ...
- Spring MVC URL的映射问题 ;Spring MVC 跳转与iframe包含地址问题
/login/login.html 进行form提交,登录之后的页面位于/main/frame.jsp; 这样的controller中的地址需要映射成/main/login.do,然后在control ...
- .md即markdown文件的基本常用编写语法(图文并茂)
序言: 很久没有写博客了,感觉只要是不写博客,人就很变得很懒,学的知识点感觉还是记不住,渐渐地让我明白,看的越多,懂的越少(你这话不是有毛病吗?应该是看的越多,懂的越多才对),此话怎讲,当你在茫茫的前 ...
- 使用数字签名实现数据库记录防篡改(Java实现)
本文大纲 一.提出问题 二.数字签名 三.实现步骤 四.参考代码 五.后记 六.参考资料 一.提出问题 最近在做一个项目,需要对一个现成的产品的数据库进行操作,增加额外的功能.为此,需要对该产品对数据 ...
- KB奇遇记(6):搞笑的ERP项目团队
早在我们来之前,KB公司这边就已经组建了ERP项目组了,当时IT就只有一个人,属网管出身.而关键用户分两种类型:专职关键用户和兼职关键用户.专职关键用户组织结构上已经调动到信息部,常驻在项目组里工作, ...