iOS 【如何寫出最簡潔優雅的網路封裝 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)
- RxMoyaProvider是MoyaProvider的子類,是對RxSwift的擴充套件
filterSuccessfulStatusCodes()
是Moya為RxSwift提供的擴充套件方法,顧名思義,可以得到成功成功地網路請求,忽略其他的mapJSON()
也是Moya RxSwift的擴充套件方法,可以把返回的資料解析成 JSON 格式subscribe
是一個RxSwift的方法,對經過一層一層處理的 Observable 訂閱一個onNext
的 observer,一旦得到 JSON 格式的資料,就會經行相應的處理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 { }
mapObject
方法處理單個物件,mapArray
方法處理物件陣列。- 如果傳進來的資料
response
是一個dictionary
,那麼就利用 ObjectMapper 的map
方法對映這些資料,這個方法會呼叫你之前在mapping
方法裡面定義的邏輯。 - 如果
response
不是一個dictionary
, 那麼就丟擲一個錯誤。 - 在底部自定義了簡單的 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,大家可以下載參考。