1. 程式人生 > >iOS 【如何寫出最簡潔優雅的網路封裝 Moya + RxSwift】

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)
  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,大家可以下載參考。