[Swift]ObjectMapper+RxSwiftを実装した備忘録

**2019/8/13 続編を書きました
[[Swift] URLSession+Combine+CodableでAPIクライアントを作る](https://qiita.com/H_Crane/items/4778d7951cd0e1611f5b)**

# 前書き
``objective-c``+``swift3`` という混在するプロジェクトでコーディングを行っている。
近い将来アプリのフルリニューアルを行うことが決定しており、レガシーコードの書き換える中で、通信系がブラックボックス化されており依存度が高いものであるため、新しく作り直すことになった。

``swift4``に移行したかったが、``objective-c``のコードをすべて``swift``化してから行いたかったので、一旦現状のまま実装できるものとして``ObjectMapper``を採用した（Realmとの相性を考えて）。また、それに伴い``Rx``の導入も行なった。

この手の記事は多いので、個人的な備忘録としてここに記録する。

### 現状

``Alamofire`` + ``Curry`` + ``Argo``

### 予定

``Alamofire`` + ``RxSwift`` + ``ObjectMapper``（AlamofireObjectMapper） 

# 導入
実際に、プロジェクトに導入する手順で行っていく。


## 1. インストール
ライブラリの導入は``Carthage``を使用。（``Cocoapods``,``SPM``など導入できるのであれば何でもおk）

```
github "Alamofire/Alamofire" ~> 4.4
github "ReactiveX/RxSwift"
github "Hearst-DD/ObjectMapper" ~> 2.2
github "tristanhimmelman/AlamofireObjectMapper" ~> 4.0
```
※ObjectMapperは最新のものだとエラーになるかもしれないので、あえて低いものを指定

## 2. ネットワークプロトコルの作成

コメントアウトで説明してあります。

```swift
import Alamofire
import ObjectMapper
import AlamofireObjectMapper

// MARK: - Base API Protocol
protocol BaseAPIProtocol {
    associatedtype ResponseType
    
    var method: HTTPMethod { get }
    var baseURL: URL { get }
    var path: String { get }
    var headers: HTTPHeaders? { get }

}

extension BaseAPIProtocol {
    var baseURL: URL {
        return try! "https://~~~".asURL()
    }
    var headers: HTTPHeaders? {
        return nil // 必要であれば個々に設定
    }
}

// MARK: - BaseRequestProtocol
protocol BaseRequestProtocol: BaseAPIProtocol, URLRequestConvertible {
    var parameters: Parameters? { get }
    var encoding: URLEncoding { get }
}

extension BaseRequestProtocol {
    var encoding: URLEncoding {
        // parameter の変換の仕方を設定 
        // defaultの場合、get→quertString、post→httpBodyとよしなに行ってくれる
        return URLEncoding.default
    }

    func asURLRequest() throws -> URLRequest {
        // requestごとの　pathを設定
        var urlRequest = URLRequest(url: baseURL.appendingPathComponent(path))

        // requestごとの　methodを設定(get/post/delete etc...)
        urlRequest.httpMethod = method.rawValue

        // headersを設定
        urlRequest.allHTTPHeaderFields = headers

        // timeout時間を設定
        urlRequest.timeoutInterval = TimeInterval(30)
        
        // requestごとの　parameterを設定
        if let params = parameters {
            urlRequest = try encoding.encode(urlRequest, with: params)
        }

        return urlRequest
    }

}
```
upload用のプロトコルを作成できるように``BaseAPIProtocol``で切り出しています。

## 3. ネットワークマネージャーを作成

``Alamofire``に``RxSwift``を盛り込む。
また、シングルトン（もどき、厳密には違う）で作成しています。

長いので解説はちょっとあれですが、、、

```swift
import UIKit
import Alamofire
import ObjectMapper
import AlamofireObjectMapper
import RxSwift

// MARK: - API Manager
struct APIManager {
    
    // MARK: - Static Variables    
    private static let successRange = 200..<300
    private static let contentType = ["application/json"]

    // MARK: - APICallProtocol Methods
    
    /// API呼び出しメソッド
    static func call<T, V>(_ request: T, _ disposeBag: DisposeBag,
                           onNext: @escaping (V) -> Void, onError: @escaping (Error) -> Void)
        where T : BaseRequestProtocol, V == T.ResponseType, T.ResponseType : Mappable {
        
            _ = observe(request)
                .observeOn(MainScheduler.instance)
                .subscribe(onNext: { onNext($0) },
                           onError: { onError($0) })
                .disposed(by: disposeBag)
    }

    /// RxでAPIを呼び出すメソッド ※下部に追記あり
    static func observe<T, V>(_ request: T) -> Observable<V>
        where T: BaseRequestProtocol, V: Mappable, T.ResponseType == V {
            
            return Observable<V>.create { observer in
                let calling = callForJson(request) { response in
                    switch response {
                    case .success(let result): observer.onNext(result as! V)
                    case .failure(let error): observer.onError(error)
                    }
                    observer.onCompleted()
                }

                return Disposables.create() { calling.cancel() }
            }
    }
    
    /// Json 形式で取得するメソッド
    static func callForJson<T, V>(_ request: T, completion: @escaping (APIResult) -> Void) -> DataRequest
        where T: BaseRequestProtocol, V: Mappable, T.ResponseType == V {

            return customAlamofire(request).responseJSON { response in
                    switch response.result {
                    case .success(let result): completion(.success(mappingJson(request, result as! Parameters)))
                    case .failure(let error): completion(.failure(error))
                    }

            }
    }
    
    /// Alamofireカスタマイズメソッド
    static func customAlamofire<T>(_ request: T) -> DataRequest
        where T: BaseRequestProtocol {
        
            return Alamofire
                .request(request)
                .validate(statusCode: successRange)
                .validate(contentType: contentType)
    }
    
    /// Jsonをレスポンスモデルに mappingするメソッド
    static func mappingJson<T, V>(_ request: T, _ result: Parameters) -> V
        where T: BaseRequestProtocol, V: Mappable, T.ResponseType == V {
    
        // 必ずmappingされるので「！」を使用
        return Mapper<V>().map(JSON: result)!
    }

}

/* APIレスポンスの結果分岐
 
 success: ObjectMapperに対応したレスポンスモデルを返す
 failure: サーバーからのエラーログを返す
 
 */

// MARK: - ResultType
enum APIResult {
    case success(Mappable)
    case failure(Error)
}
```

## 4. レスポンスモデルの作成
サーバーから取得できる値を``swift``で扱える形にする必要がある。そのためのモデルを作成する。
前提として、サーバーからはJson形式で取得されるものとする。
以下、レスポンスのJson。今回は階層構造のものを用意した。

```json
{
    "data": [
        {
            "title": "タイトル", 
            "description": "説明文", 
            "icon": "https://xxx.jpg",
            "date": "2017-01-01 00:00:00",  
            "id": "00000", 
        },
    ], 
    "result": true
}
```

上記を``ObjectMapper``に対応したレスポンスモデルにすると

```swift
import ObjectMapper

struct xxxResponse: Mappable {

    var data: [xxxDataModel]?
    var result: Bool?

    init?(map: Map) { }

    mutating func mapping(map: Map) {
        data <- map["data"]
        result <- map["result"]
    }

}

struct xxxDataModel: Mappable {

    var title: String?
    var description: String?
    var icon: URL?
    var date: Date?
    var id: Int?
    
    init?(map: Map) { }
    
    mutating func mapping(map: Map) {
        title <- map["title"]
        description <- map["description"]  
        icon <- (map["icon"], URLTransform()) // String -> URL 変換
        date <- (map["date"], ISO8601DateTransform()) // String -> Data 変換
        id <- (map["id"], TransformOf<Int, String> // String -> Int 変換
                          (fromJSON: { Int($0!) },toJSON: { $0.map { String($0) } }))
    }
    
}
```
階層構造のものは別のstructで定義されている。
``map["~~"]``でセットする際に、必要な型に変換できる。(ObjectMapperが用意してくれている)

## 5. リクエストの作成

必要なリクエストパラメータが

```
[
  "keyword": String,
  "ids": [Int]
]
```

の場合、

```swift
import Alamofire
import ObjectMapper

// MARK: - Request
enum xxxRequest: BaseRequestProtocol {
    typealias ResponseType = xxxResponse    

    case post(keyword: String, ids: [Int])
    
    var method: HTTPMethod {
        switch self {
        case .post: return .post
        }
    }
    
    var path: String {
        return "xxx/yyy/zzz" /// 個々に設定する
    }
    
    var parameters: Parameters? {
        switch self {
        case .post(let keyword, let ids):
            return [
                "keyword": keyword,
                "ids": ids
            ]
        }
    }
    
}
```

## 実用例

```swift
class ViewController: UIViewController {
     
   private let disposeBag = DisposeBag()
     
   private func callForXXX() {
      let request = xxxRequest.post(keyword: "キーワード", ids: [00000, 11111, 10101])
      APIManager.call(request, disposeBag, onNext: onNext, onError: onError)
   }  
     
   private func onNext(with result: Mappable) {
      // do something
   }
     
   private func onError() {
      // error handling
   }
     
}
```

通信結果を一元管理できるようにしようか迷ったが、同じ通信でも処理をわけたいことも考えられたので、引数として成功時・失敗時のメソッドを渡す仕組みにした。

追記
上記のネットワークマネジャーに関して
``onCompleted``で特に処理を行っていないので、Success/Failureいずれかだけをかえす、
``Rx``の``Single``に書き換えました。以下になります。 

```swift
static func observe<T, V>(_ request: T) -> Single<V>
   where T: BaseRequestProtocol, V: Mappable, T.ResponseType == V {
            
      return Single<V>.create { observer in
            let calling = callForJson(request) { response in
                  switch response {
                     case .success(let result): observer(.success(result as! V))
                     case .failure(let error): observer(.error(error))
                  }
            }

            return Disposables.create() { calling.cancel() }
   }
}
```

# 後書き
結局自分でブラックボックスのようなコードを作ってしまった気がする笑

レスポンスのJSON形式がJSONObjectの場合しか対応していないので（関わっているプロジェクトがその形式のため）、他で使用する場合を考えて少しだけ汎用的にすべきだったかと。

# 参考
[[2017年版]RxSwift + Alamofire + ObjectMapper + RealmのSwift実装について](https://qiita.com/syou007/items/8f27e7f0d03b9cfcff6c)
[Swiftを使ったAPIクライアントの実装方法](http://tech.unifa-e.com/entry/2017/01/11/153752)
[HTTP通信部分にRxSwiftを使ってみる](https://dev.classmethod.jp/smartphone/use-rxswift-for-http-networking/)