如何写出最简洁优雅的网络封装 Moya + RxSwift

前言


  • Why Moya ?

Alamofire可能是iOS Swift中最常用的HTTP networking library,用Alamofire可以抽象出NSURLSession和其中很多繁琐的细节,让你可以很方便地写出类似"APIManager"这种专门管理网络请求的类。

我们可以看一些例子,例子中用的JSONPlaceholder是一个免费的测试用的REST API:

//GET request
let postEndpoint: String = "http://jsonplaceholder.typicode.com/posts/1"
Alamofire.request(.GET, postEndpoint) 
  .responseJSON { response in
    //do something with response
  }

//POST request
let postsEndpoint: String = "http://jsonplaceholder.typicode.com/posts"
let newPost = ["title": "title", "body": "body", "userId": 1]
Alamofire.request(.POST, postsEndpoint, parameters: newPost, encoding: .JSON)
  .responseJSON { response in
    //do something with response
  }

对于每个请求,你必须提供一个String类型的URL和一个HTTP请求方法,像.GET,如果你有很多请求需要完成,那会让代码显得不那么容易阅读,维护和测试。
解决这些问题的办法就是利用Swift enum的特性给Alamofire添加一个router,这就是Moya。

  • What is Moya ?

Moya是一个基于Alamofire的Networking library,并且添加了对于ReactiveCocoa和RxSwift的接口支持,大大简化了开发过程,是Reactive Functional Programming的网络层首选。
Github上的官方介绍罗列了Moya的一些特点:

  • 编译的时候会检查API endpoint
  • 可以用枚举值清楚地定义很多endpoint
  • 增加了stubResponse类型,大大方便了unit testing

正文


正文首先介绍如何使用 Moya,第二步为 Moya 添加 RxSwift, 然后再加入数据层的映射(Model Mapping),最后在这个简单的例子中加入MVVM。一步一步地循序渐进,希望对大家有帮助。

Moya

首先创建一个 enum 来枚举你所有的 API targets。你可以把所有关于这个API的信息放在这个枚举类型中。

enum MyAPI {
    case Show
    case Create(title: String, body: String, userId: Int)
}

这个枚举类型用来在编译的阶段给每个target提供具体的信息,每个枚举的值必须有发送http request需要的基本参数,像url,method,parameters等等。这些要求被定义在一个叫做TargetType的协议中,在使用过程过我们的枚举类型需要服从这个协议。通常我们把这一部分的代码写在枚举类型的扩展里。

extension MyAPI: TargetType {
    var baseURL: URL {
        return URL(string: "http://jsonplaceholder.typicode.com")!
    }
    
    var path: String {
        switch self {
        case .Show:
            return "/posts"
        case .Create(_, _, _):
            return "/posts"
        }
    }
    
    var method: Moya.Method {
        switch self {
        case .Show:
            return .GET
        case .Create(_, _, _):
            return .POST
        }
    }
    
    var parameters: [String: Any]? {
        switch self {
        case .Show:
            return nil
        case .Create(let title, let body, let userId):
            return ["title": title, "body": body, "userId": userId]
        }
    }
    
    var sampleData: Data {
        switch self {
        case .Show:
            return "[{\\"userId\\": \\"1\\", \\"Title\\": \\"Title String\\", \\"Body\\": \\"Body String\\"}]".data(using: String.Encoding.utf8)!
        case .Create(_, _, _):
            return "Create post successfully".data(using: String.Encoding.utf8)!
        }
    }
    
    var task: Task {
        return .request
    }
}

Moya的使用非常简单,通过TargetType协议定义好每个target之后,就可以直接使用Moya开始发送网络请求了。

let provider = MoyaProvider<MyAPI>()
        provider.request(.Show) { result in
            // do something with result
        }

+ RxSwift

Moya本身已经是一个使用起来非常方便,能够写出非常简洁优雅的代码的网络封装库,但是让Moya变得更加强大的原因之一还因为它对于Functional Reactive Programming的扩展,具体说就是对于RxSwift和ReactiveCocoa的扩展,通过与这两个库的结合,能让Moya变得更加强大。我选择RxSwift的原因有两个,一个是RxSwift的库相对来说比较轻量级,语法更新相对来说比较少,我之前用过ReactiveCocoa,一些大版本的更新需求重写很多代码,第二个更重要的原因是因为RxSwift背后有整个ReactiveX的支持,里面包括Java,JS,.Net, Swift,Scala,它们内部都用了ReactiveX的逻辑思想,这意味着你一旦学会了其中的一个,以后可以很快的上手ReactiveX中的其他语言。

在我之前的几篇文章中已经写了RxSwift的一些简单上手的教程,不太熟悉RxSwift的朋友大家可以看一看,有个大致的了解。Moya提供了非常方面的RxSwift扩展:

let provider = RxMoyaProvider<MyAPI>()
provider.request(.Show)
    .filterSuccessfulStatusCodes()
    .mapJSON()
    .subscribe(onNext: { (json) in
        //do something with posts
        print(json)
     })
     .addDisposableTo(disposeBag)
  1. RxMoyaProvider是MoyaProvider的子类,是对RxSwift的扩展
  2. filterSuccessfulStatusCodes() 是Moya为RxSwift提供的扩展方法,顾名思义,可以得到成功成功地网络请求,忽略其他的
  3. mapJSON() 也是Moya RxSwift的扩展方法,可以把返回的数据解析成 JSON 格式
  4. subscribe 是一个RxSwift的方法,对经过一层一层处理的 Observable 订阅一个 onNext 的 observer,一旦得到 JSON 格式的数据,就会经行相应的处理
  5. addDisposableTo(disposeBag) 是 RxSwift 的一个自动内存处理机制,跟 ARC 有点类似,会自动清理不需要的对象。

运行程序,我们会得到下列的数据,网络请求的代码原来可以写得如此简洁优雅:

[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\\nsuscipit recusandae consequuntur expedita et cum\\nreprehenderit molestiae ut ut quas totam\\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
  {
    "userId": 1,
    "id": 3,
    "title": "ea molestias quasi exercitationem repellat qui ipsa sit aut",
    "body": "et iusto sed quo iure\\nvoluptatem occaecati omnis eligendi aut ad\\nvoluptatem doloribus vel accusantium quis pariatur\\nmolestiae porro eius odio et labore et velit aut"
  },
  {
    "userId": 1,
    "id": 4,
    "title": "eum et est occaecati",
    "body": "ullam et saepe reiciendis voluptatem adipisci\\nsit amet autem assumenda provident rerum culpa\\nquis hic commodi nesciunt rem tenetur doloremque ipsam iure\\nquis sunt voluptatem rerum illo velit"
  }, ...

+ Model Mapping

在实际应用过程中网络请求往往紧密连接着数据层(Model),具体地说,在我们的这个例子中,一般我们需要建立一个 Post 类用来统一管理数据,类里面有 id, title, body 等信息,然后把得到的一个个 post 的 JSON 数据映射到 Post 类,也就是数据层(Model)。

我之前最常用 SwiftyJSON 这个库来提取 JSON 中的各种信息,它是 Swift 中最常用的处理 JSON 的第三方库,但是在更新到了 Xcode 8 和 Swift 3 之后,这个库一直都没有更新,所以我使用了另一个 Github 上也有数千个star的库,叫做 ObjectMapper

利用 ObjectMapper,创建 Post 类:

class Post: Mappable {
    var id: Int?
    var title: String?
    var body: String?
    var userId: Int?
    
    
    required init?(map: Map) {
    }
    
    func mapping(map: Map) {
        id <- map["id"]
        title <- map["title"]
        body <- map["body"]
        userId <- map["userId"]
    }
}

详细的 ObjectMapper 教程可以查看它的 Github 主页,我在这里只做简单的介绍。

使用 ObjectMapper ,需要让自己的 Model 类使用 Mappable 协议,这个协议包括两个方法:

required init?(map: Map) {}

func mapping(map: Map) {}

mapping 方法中,用 <- 操作符来处理和映射你的 JSON 数据。

数据类建立好之后,我们还需要为 RxSwift 中的 Observable 写一个简单的扩展方法 mapObject,利用我们写好的 Post 类,一步就把 JSON 数据映射成一个个 post。

可以创建一个名为 Observable+ObjectMapper.swift 的文件:

import Foundation
import RxSwift
import Moya
import ObjectMapper

extension Observable {
    func mapObject<T: Mappable>(type: T.Type) -> Observable<T> {
        return self.map { response in
            //if response is a dictionary, then use ObjectMapper to map the dictionary
            //if not throw an error
            guard let dict = response as? [String: Any] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            
            return Mapper<T>().map(JSON: dict)!
        }
    }
    
    func mapArray<T: Mappable>(type: T.Type) -> Observable<[T]> {
        return self.map { response in
            //if response is an array of dictionaries, then use ObjectMapper to map the dictionary
            //if not, throw an error
            guard let array = response as? [Any] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            
            guard let dicts = array as? [[String: Any]] else {
                throw RxSwiftMoyaError.ParseJSONError
            }
            
            return Mapper<T>().mapArray(JSONArray: dicts)!
        }
    }
}

enum RxSwiftMoyaError: String {
    case ParseJSONError
    case OtherError
}

extension RxSwiftMoyaError: Swift.Error { }
  1. mapObject 方法处理单个对象,mapArray 方法处理对象数组。
  2. 如果传进来的数据 response 是一个 dictionary,那么就利用 ObjectMapper 的 map 方法映射这些数据,这个方法会调用你之前在 mapping 方法里面定义的逻辑。
  3. 如果 response 不是一个 dictionary, 那么就抛出一个错误。
  4. 在底部自定义了简单的 Error,继承了 Swift 的 Error 类,在实际应用过程中可以根据需要提供自己想要的 Error。

运行下面的程序:

let provider = RxMoyaProvider<MyAPI>()
provider.request(.Show)
    .filterSuccessfulStatusCodes()
    .mapJSON()
    .mapArray(type: Post.self)
    .subscribe(onNext: { (posts: [Post]) in
        //do something with posts
        print(posts.count)
    })
    .addDisposableTo(disposeBag)

provider.request(.Create(title: "Title 1", body: "Body 1", userId: 1))
    .mapJSON()
    .mapObject(type: Post.self)
    .subscribe(onNext: { (post: Post) in
        //do something with post
        print(post.title!)
    })
    .addDisposableTo(disposeBag)

得到结果:

100
Title 1

+ MVVM

MVVM(Model-View-ViewModel)可以把数据的处理逻辑放到 ViewModel 从而大大减轻了 ViewController 的负担,是 RxSwift 中最常用的架构逻辑。

这个例子中我们可以把从网络请求得到数据的步骤写到 ViewModel 文件里:

import Foundation
import RxSwift
import Moya

class ViewModel {
    private let provider = RxMoyaProvider<MyAPI>()
    
    func getPosts() -> Observable<[Post]> {
        return provider.request(.Show)
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .mapArray(type: Post.self)
    }
    
    func createPost(title: String, body: String, userId: Int) -> Observable<Post> {
        return provider.request(.Create(title: title, body: body, userId: userId))
            .mapJSON()
            .mapObject(type: Post.self)
    }

然后在 ViewController 中调用 ViewModel 的方法:

import UIKit
import RxSwift

class ViewController: UIViewController {
    
    let disposeBag = DisposeBag()
    let viewModel  = ViewModel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        viewModel.getPosts()
            .subscribe(onNext: { (posts: [Post]) in
                //do something with posts
                print(posts.count)
            })
            .addDisposableTo(disposeBag)
        
        viewModel.createPost(title: "Title 1", body: "Body 1", userId: 1)
            .subscribe(onNext: { (post: Post) in
                //do something with post
                print(post.title!)
            })
            .addDisposableTo(disposeBag)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
}

文中这个例子的完整项目放在了Github,大家可以下载参考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容