本篇讲解Result的封装

前言

有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解。在Alamofire中,使用Response来描述请求后的结果。我们都知道Alamofire返回的数据可以经过特殊的处理,比如说序列化,那么我们应该如何在Response中获取到这些类型不同的数据呢?

假如说序列化后的数据是data,最直接的想法就是把data设置为Any类型,在实际用到的时候在进行判断,这也是最普通的一种开发思维。现在我们就要打破这种思维。我们需要封装一个对象,这个对象能够表达任何结果,这就用到了swift中的泛型。

接下来在讲解Result之后,会给出两个使用泛型的例子,第一个例子表达基本的网络封装思想,第二个表达基本的viewModel思想。

Result

  1. /// Used to represent whether a request was successful or encountered an error.
  2. ///
  3. /// - success: The request and all post processing operations were successful resulting in the serialization of the
  4. /// provided associated value.
  5. ///
  6. /// - failure: The request encountered an error resulting in a failure. The associated values are the original data
  7. /// provided by the server as well as the error that caused the failure.
  8. public enum Result<Value> {
  9. case success(Value)
  10. case failure(Error)
  11. }

关于如何描述结果,有两种可能,不是成功就是失败,因此考虑使用枚举。在Alamofire源码解读系列(二)之错误处理(AFError)这篇文章中我已经详细的讲解了枚举的使用方法。在上边的代码中,对枚举的每个子选项都做了值关联。

大家注意,泛型的写法是类似这样的:,在<和>之间声明一种类型,这个T知识象征性的,在赋值的时候,可以是任何类型。还有一种用法,看下边的代码:

  1. struct CellConfigurator<Cell> where Cell: Updatable, Cell: UITableViewCell {
  2. }

上边代码中的Cell必须符合后边给出的两个条件才行,这种用法是给泛型增加了条件限制,这种用法还有另外一种方式,看下边的代码:

  1. func send<T: Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void);

其实道理都差不多,都属于对泛型的灵活运用。

我们接着看看在Alamofire中是如何使用Result的。

  1. @discardableResult
  2. public func responseJSON(
  3. queue: DispatchQueue? = nil,
  4. options: JSONSerialization.ReadingOptions = .allowFragments,
  5. completionHandler: @escaping (DataResponse<Any>) -> Void)
  6. -> Self
  7. {
  8. return response(
  9. queue: queue,
  10. responseSerializer: DataRequest.jsonResponseSerializer(options: options),
  11. completionHandler: completionHandler
  12. )
  13. }

上边的这个函数的主要目的是把请求成功后的结果序列化为JSON,completionHandler函数的参数类型为DataResponse,其中的Any就会传递给Result,也就是Result。

那么问题来了,不是把数据解析成JSON了吗?为什么要返回Any类型呢?json本质上很类似于JavaScript中的对象和数组。JSONSerialization.jsonObject返回的类型是Any,这是因为解析后的数据有可能是数组,也有可能是字典。

字典:

  1. {
  2. "people":[
  3. {"firstName":"Brett","lastName":"McLaughlin","email":"aaaa"},
  4. {"firstName":"Jason","lastName":"Hunter","email":"bbbb"},
  5. {"firstName":"Elliotte","lastName":"Harold","email":"cccc"}
  6. ]
  7. }

数组:

  1. [
  2. "a",
  3. "b",
  4. "c"
  5. ]

当然如果不是这两种格式的数据,使用JSONSerialization.jsonObject解析会抛出异常。

到这里我们就大概对这个Result有了一定的了解,下边的代码给result添加了一些属性,主要目的是使用起来更方便:

  1. /// Returns `true` if the result is a success, `false` otherwise.
  2. public var isSuccess: Bool {
  3. switch self {
  4. case .success:
  5. return true
  6. case .failure:
  7. return false
  8. }
  9. }
  10. /// Returns `true` if the result is a failure, `false` otherwise.
  11. public var isFailure: Bool {
  12. return !isSuccess
  13. }
  14. /// Returns the associated value if the result is a success, `nil` otherwise.
  15. public var value: Value? {
  16. switch self {
  17. case .success(let value):
  18. return value
  19. case .failure:
  20. return nil
  21. }
  22. }
  23. /// Returns the associated error value if the result is a failure, `nil` otherwise.
  24. public var error: Error? {
  25. switch self {
  26. case .success:
  27. return nil
  28. case .failure(let error):
  29. return error
  30. }
  31. }

当然,为了打印更加详细的信息,使Result实现了CustomStringConvertibleCustomDebugStringConvertible协议 :

  1. // MARK: - CustomStringConvertible
  2. extension Result: CustomStringConvertible {
  3. /// The textual representation used when written to an output stream, which includes whether the result was a
  4. /// success or failure.
  5. public var description: String {
  6. switch self {
  7. case .success:
  8. return "SUCCESS"
  9. case .failure:
  10. return "FAILURE"
  11. }
  12. }
  13. }
  14. // MARK: - CustomDebugStringConvertible
  15. extension Result: CustomDebugStringConvertible {
  16. /// The debug textual representation used when written to an output stream, which includes whether the result was a
  17. /// success or failure in addition to the value or error.
  18. public var debugDescription: String {
  19. switch self {
  20. case .success(let value):
  21. return "SUCCESS: \(value)"
  22. case .failure(let error):
  23. return "FAILURE: \(error)"
  24. }
  25. }
  26. }

总起来说,Result是一个比较简单的封装。

基于泛型的网络封装

在实际的开发工作中,我们使用Alamofire发送请求,获取服务器的数据,往往会对其进行二次封装,在这里,我讲解一个封装的例子,内容来自面向协议编程与 Cocoa 的邂逅

  1. 我们需要一个协议,这个协议提供一个函数,目的是把Data转换成实现该协议的对象本身。注意我们在这时候是不知道这个对象的类型的,为了适配更多的类型,这个对象暂时设计为泛型,因此协议中的函数应该是静态函数

    1. protocol Decodable {
    2. static func parse(data: Data) -> Self?
    3. }
  2. 封装请求,同样采用协议的方式

    1. public enum JZGHTTPMethod: String {
    2. case options = "OPTIONS"
    3. case get = "GET"
    4. case head = "HEAD"
    5. case post = "POST"
    6. case put = "PUT"
    7. case patch = "PATCH"
    8. case delete = "DELETE"
    9. case trace = "TRACE"
    10. case connect = "CONNECT"
    11. }
    12. protocol Request {
    13. var path: String { get }
    14. var privateHost: String? { get }
    15. var HTTPMethod: JZGHTTPMethod { get }
    16. var timeoutInterval: TimeInterval { get }
    17. var parameter: [String: Any]? { get }
    18. associatedtype Response: Decodable
    19. }
  3. 封装发送端,同样采用协议的方式

    1. protocol Client {
    2. var host: String { get }
    3. func send<T: Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void);
    4. }
  4. 只要是实现了Client协议的对象,就有能力发送请求,在这里Alamofire是作为中间层存在的,只提供请求能力,可以随意换成其他的中间能力层

    1. struct AlamofireClient: Client {
    2. public static let `default` = { AlamofireClient() }()
    3. public enum HostType: String {
    4. case sandbox = "https://httpbin.org/post"
    5. }
    6. /// Base host URL
    7. var host: String = HostType.sandbox.rawValue
    8. func send<T : Request>(_ r: T, handler: @escaping (T.Response?, String?) -> Void) {
    9. let url = URL(string: r.privateHost ?? host.appending(r.path))!
    10. let sessionManager = Alamofire.SessionManager.default
    11. sessionManager.session.configuration.timeoutIntervalForRequest = r.timeoutInterval
    12. Alamofire.request(url, method: HTTPMethod(rawValue: r.HTTPMethod.rawValue)!,
    13. parameters: r.parameter,
    14. encoding: URLEncoding.default,
    15. headers: nil)
    16. .response { (response) in
    17. if let data = response.data, let res = T.Response.parse(data: data) {
    18. handler(res, nil)
    19. }else {
    20. handler(nil, response.error?.localizedDescription)
    21. }
    22. }
    23. }
    24. }

封装完成之后,我们来使用一下上边封装的功能:

  1. 创建一个TestRequest.swift文件,内部代码为:

    1. struct TestRequest: Request {
    2. let name: String
    3. let userId: String
    4. var path: String {
    5. return ""
    6. }
    7. var privateHost: String? {
    8. return nil
    9. }
    10. var timeoutInterval: TimeInterval {
    11. return 20.0
    12. }
    13. var HTTPMethod: JZGHTTPMethod {
    14. return .post
    15. }
    16. var parameter: [String : Any]? {
    17. return ["name" : name,
    18. "userId" : userId]
    19. }
    20. typealias Response = TestResult
    21. }
  2. 创建TestResult.swift文件,内部代码为:

    1. struct TestResult {
    2. var origin: String
    3. }
    4. extension TestResult: Decodable {
    5. static func parse(data: Data) -> TestResult? {
    6. do {
    7. let dic = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
    8. guard let dict = dic as? Dictionary<String, Any> else {
    9. return nil
    10. }
    11. return TestResult(origin: dict["origin"] as! String)
    12. }catch {
    13. return nil
    14. }
    15. }
    16. }
  3. 发送请求

    1. let request = TestRequest(name: "mama", userId: "12345");
    2. AlamofireClient.default.send(request) { (response, error) in
    3. print(response)
    4. }

对网络的基本封装就到此为止了 ,这里的Result可以是任何类型的对象,比如说User,可以通过上边的方法,直接解析成User对象。

基于泛型的cell封装

这种设计通常应用在MVVM之中,我们看下边的代码:

  1. 定义一个协议,这个协议提供一个函数,函数会提供一个参数,这个参数就是viewModel。cell只要实现了这个协议,就能够通过这个参数拿到viewModel,然后根据viewModel来配置自身控件的属性。

    1. protocol Updatable: class {
    2. associatedtype ViewData
    3. func update(viewData: ViewData)
    4. }
  2. 再定义一个协议,这个协议需要表示cell的一些信息,比如reuseIdentifier,cellClass,同时,这个协议还需要提供一个方法,赋予cell适配器更新cell的能力

    1. protocol CellConfiguratorType {
    2. var reuseIdentifier: String { get }
    3. var cellClass: AnyClass { get }
    4. func update(cell: UITableViewCell)
    5. }
  3. 创建CellConfigurator,这个CellConfigurator必须绑定一个viewData,这个viewData通过Updatable协议中的方法传递给cell

    1. struct CellConfigurator<Cell> where Cell: Updatable, Cell: UITableViewCell {
    2. let viewData: Cell.ViewData
    3. let reuseIdentifier: String = NSStringFromClass(Cell.self)
    4. let cellClass: AnyClass = Cell.self
    5. func update(cell: UITableViewCell) {
    6. if let cell = cell as? Cell {
    7. cell.update(viewData: viewData)
    8. }
    9. }
    10. }

万变不离其宗啊,我们在请求到数据之后,需要把数据转变成CellConfigurator,也就是在数组中存放的是CellConfigurator类型的数据。

看看使用示例:

  1. 创建数组

    1. let viewController = ConfigurableTableViewController(items: [
    2. CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Foo")),
    3. CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "og")!)),
    4. CellConfigurator<ImageTableViewCell>(viewData: ImageCellViewData(image: UIImage(named: "GoogleLogo")!)),
    5. CellConfigurator<TextTableViewCell>(viewData: TextCellViewData(title: "Bar")),
    6. ])
  2. 注册cell

    1. func registerCells() {
    2. for cellConfigurator in items {
    3. tableView.register(cellConfigurator.cellClass, forCellReuseIdentifier: cellConfigurator.reuseIdentifier)
    4. }
    5. }
  3. 配置cell

    1. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    2. let cellConfigurator = items[(indexPath as NSIndexPath).row]
    3. let cell = tableView.dequeueReusableCell(withIdentifier: cellConfigurator.reuseIdentifier, for: indexPath)
    4. cellConfigurator.update(cell: cell)
    5. return cell
    6. }

这个cell封装思想出自这里https://github.com/fastred/ConfigurableTableViewController

总结

上边两个例子,我解释的并不是很详细,只需要打开源码,仔细琢磨琢磨就能体会到里边的妙处,如有问题,可以留言。

在这里获取代码:https://github.com/agelessman/TTestDemo

由于知识水平有限,如有错误,还望指出

链接

Alamofire源码解读系列(一)之概述和使用 简书博客园

Alamofire源码解读系列(二)之错误处理(AFError) 简书博客园

Alamofire源码解读系列(三)之通知处理(Notification) 简书博客园

Alamofire源码解读系列(四)之参数编码(ParameterEncoding) 简书博客园

Alamofire源码解读系列(五)之结果封装(Result)的更多相关文章

  1. Alamofire源码解读系列(九)之响应封装(Response)

    本篇主要带来Alamofire中Response的解读 前言 在每篇文章的前言部分,我都会把我认为的本篇最重要的内容提前讲一下.我更想同大家分享这些顶级框架在设计和编码层次究竟有哪些过人的地方?当然, ...

  2. Alamofire源码解读系列(六)之Task代理(TaskDelegate)

    本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...

  3. Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager)

    Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 本篇主要讲解iOS开发中的网络监控 前言 在开发中,有时候我们需要获取这些信息: 手机是否联网 ...

  4. Alamofire源码解读系列(八)之安全策略(ServerTrustPolicy)

    本篇主要讲解Alamofire中安全验证代码 前言 作为开发人员,理解HTTPS的原理和应用算是一项基本技能.HTTPS目前来说是非常安全的,但仍然有大量的公司还在使用HTTP.其实HTTPS也并不是 ...

  5. Alamofire源码解读系列(十)之序列化(ResponseSerialization)

    本篇主要讲解Alamofire中如何把服务器返回的数据序列化 前言 和前边的文章不同, 在这一篇中,我想从程序的设计层次上解读ResponseSerialization这个文件.更直观的去探讨该功能是 ...

  6. Alamofire源码解读系列(十一)之多表单(MultipartFormData)

    本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...

  7. Alamofire源码解读系列(十二)之时间轴(Timeline)

    本篇带来Alamofire中关于Timeline的一些思路 前言 Timeline翻译后的意思是时间轴,可以表示一个事件从开始到结束的时间节点.时间轴的概念能够应用在很多地方,比如说微博的主页就是一个 ...

  8. Alamofire源码解读系列(十二)之请求(Request)

    本篇是Alamofire中的请求抽象层的讲解 前言 在Alamofire中,围绕着Request,设计了很多额外的特性,这也恰恰表明,Request是所有请求的基础部分和发起点.这无疑给我们一个Req ...

  9. Alamofire源码解读系列(二)之错误处理(AFError)

    本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...

随机推荐

  1. 3.1. 修改托管对象模型(Core Data 应用程序实践指南)

    托管对象模型是会变好的,有时候变化的比较小,什么添加验证规则.修改默认值.修改获取请求模板等.但是设置到结构变化,如添加.删除字段时,需要先把持久化数据区迁移到新的模型版本才行.假如没有提供迁移数据所 ...

  2. --@angularJS--指令与控制器之间较复杂的交互demo2

    1.index.html: <!DOCTYPE HTML><html ng-app="app"><head>    <title>c ...

  3. 【bzoj4198】 Noi2015—荷马史诗

    http://www.lydsy.com/JudgeOnline/problem.php?id=4198 (题目链接) 题意 一篇文章n个单词,每个出现了${w_i}$次,用k进制数代替单词,使得任意 ...

  4. Android中支持的距离单位

    px(像素):每个px对应屏幕上的一个点 dip或dp(device independent pixels,设备独立像素):一种基于屏幕密度的抽象单位.在每英寸160点的显示器上,1dip=1px.但 ...

  5. 在Express中安装XTemplate

    上一节讲了安装Express,并且生成了一个"microblog"的工程,我们的目标是在工程下安装XTemplate: 1.安装xtpl npm install xtpl xtem ...

  6. 如何使用DockerHub官方的mysql镜像

    Mysql是一个广泛使用的开源关系型数据库. 如何获取Mysql Docker镜像? docker pull mysql:5.7 如何使用这个Docker镜像? 1.启动一个Mysql Server容 ...

  7. 【转】OSX键盘快捷键

    OS X 键盘快捷键 了解有关常见 OS X 键盘快捷键的信息.键盘快捷键是通过按下键盘上的组合键来调用 OS X 功能的一种方式. 若要使用键盘快捷键或按键组合,您可以同时按修饰键和字符键.例如,同 ...

  8. HDU1392(凸包)

    Surround the Trees Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Other ...

  9. SuperSocket入门(五)-常用协议实现模版及FixedSizeReceiveFilter示例

             Socket里面的协议解析是Socket通讯程序设计中最复杂的地方,如果你的应用层协议设计或实现不佳,Socket通讯中常见的粘包,分包就难以避免.SuperSocket内置了命令行 ...

  10. .Net学习难点讨论系列17 - 线程本地变量的使用

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...