[Swift]Alamofire+RxSwift+CodableでAPIクライアントを作る

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

**2020/1/28 続編を書きました
[Alamofire+RxSwiftでUpload APIクライアントを作る](https://qiita.com/H_Crane/items/c5e271079f30824df643)**


**追記**
Decode部分をprotocolに入れることでメソッドを減らしました。
（プロトコルなので、継承先で柔軟に上書けるようになった）


# 前置き
担当プロジェクトで、ようやく
``Objective-c``+``Swift3.2``　→　``Swift4.0``

への書き換えも済み、それなら**Codable**を使いたい！と思い行ってみた。

半年前に [ObjectMapper+RxSwiftを実装した備忘録](https://qiita.com/H_Crane/items/ac3b372d969e4ec1714e) 書いたが、これを**Codable**に対応したものにした。
（基本的には上記内容と同じです。Decode部分が若干かわったくらい）

## 導入にあたって

ちょろちょろこういうの
- [Swift4 + Moya + RxSwift + Codableで作るAPIクライアント](https://qiita.com/kouheiszk/items/46e9a233d9bb227c3b1d)
- [Moya+RxSwift+CodableでAPI通信](https://qiita.com/negi0205/items/73341f21511affb064ad)

はあったんですが、なんかあんまり綺麗じゃないと思ってしまい、、（ディスリじゃないですよ！）

あと``Moya``のメリットが、実感できなかったので``Alamofire``でやりました。

# 導入
実際に、プロジェクトに導入する手順で行っていく。
通信には``Alamofire``を使用します。


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

```
github "Alamofire/Alamofire" ~> 4.1
github "ReactiveX/RxSwift" == 4.3.1
```

``Linked Frameworks and Library``にもライブラリ追加を忘れずに。

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

[この記事](https://qiita.com/H_Crane/items/ac3b372d969e4ec1714e)ではただコードを載せただけだったので、若干細かく説明していきたいと思います。

### ①定義ファイルの作成
APIで使用するURLやPathを一元管理するためにenumで定義し管理していきます。
（このやり方が好きでない人は、リクエストごとに直にpathを書いていってください）

今回は例として``signIn``, ``signUp``というものを定義しています。

```swift
// MARK: - Constants
enum APIConstants {
    case signIn
    case signUp
    case xxx
        ,
        ,

    // MARK: Public Variables
    public var path: String {
        switch self {
        case .signIn: return "user_sign_in"
        case .signUp: return "user_sign_up"
        case .xxx: return "xxx"
                ,
                ,
        }
    }


    // MARK: Public Static Variables
    public static var baseURL = "https://~~~"

    public static var header: HTTPHeaders? {
        // 必要ならば個々に設定してください
        return [
            "Accept-Encoding": ~~~, 
            "Accept-Language": ~~~,
            "User-Agent": "~~~~"
        ]
    }
}
```

### ②リクエストの基底プロトコルを作成

requestを作成するための雛形``protocol``を作成します。
``Alamofire``を使用するので、それに準拠するように作っていきます。

```swift
import Alamofire

// MARK: - Base API Protocol
protocol BaseAPIProtocol {
    associatedtype ResponseType // レスポンスの型
    
    var method: HTTPMethod { get } // get,post,delete などなど
    var baseURL: URL { get } // APIの共通呼び出し元。 ex "https://~~~"
    var path: String { get } // リクエストごとのパス
    var headers: HTTPHeaders? { get } // ヘッダー情報
    var decode: (Data) throws -> ResponseType { get } // デコードの仕方
}

extension BaseAPIProtocol {

    var baseURL: URL { // 先ほど上で定義したもの。
        // 絶対にあることがある保証されているので「try！」を使用している
        return try! APIConstants.baseURL.asURL() 
    }

    var headers: HTTPHeaders? { // 先ほど上で定義したもの。なければ「return nil」でok
        return APIConstants.header 
    }

    var decode: (Data) throws -> ResponseType { // decoderはあとで用意するので注意
        return { try JSONDecoder.decoder.decode(ResponseType.self, from: $0) }
    }
}
```

この基底プロトコルを元に
- データを取得する
- アップロードする
- ダウンロードする

など場合分けして、それぞれが基底プロトコルに準じたものを作成していくと、コード分割ができ綺麗に作っていくことができるかと思います。
今回は**APIを叩いてデータを取ってくる**だけの想定で話を進めます。以下、普通の通信を行う場合のプロトコル。

``Alamofire``に準ずるため``URLRequestConvertible``を継承しています。
（リクエストするときに``URLRequestConvertible``を継承したものを渡す必要があるので）

```swift
// 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
    }

}
```

ここまでは[この記事](https://qiita.com/H_Crane/items/ac3b372d969e4ec1714e)とほぼ同じです。

## 3.リクエスト・レスポンスの作成

基底プロトコルを作ったので、実際にそれに適応するリクエストとレスポンスを作成していきます。

例を用意。APIを叩くと以下のレスポンス（Json）が返ってくるとする。(あえて階層構造なものにしました)

```json
{
    "data": [
        {
            "title": "タイトル", 
            "screen_name": "トップ", 
            "id": "00000", 
        },
        {
            "title": "タイトル", 
            "screen_name": "トップ", 
            "id": "00000", 
        },
            ,
            ,
    ], 
    "result": true
}
```

### ①レスポンスの作成

上記のJsonを対応させていきたいと思います。
また``Codable``に準拠させます。

```swift
import Alamofire

// MARK: - Response
struct xxxResponse: Codable {
    var data: [xxxViewModel]?
    var result: Bool = false

    // 特に型変換などがない場合は、init省略可なので省略
}

// MARK: - Model
struct xxxViewModel: Codable {
    
    let title: String?
    let screenName: String?
    let id: Int?
    
    enum Keys: String, CodingKey {
        case title
        case screenName
        case id
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Keys.self)
        
        title = try container.decode(String.self, forKey: .title)
        screenName = try container.decode(String.self, forKey: .screenName)
        id = Int(try container.decode(String.self, forKey: .id))
    }
}
```

### ②リクエストの作成

先ほど作成した``BaseRequestProtocol``に準拠させます。
今回はgetの場合を例に。

```swift
import Alamofire

// MARK: - Request
enum xxxRequest: BaseRequestProtocol {
    typealias ResponseType = xxxResponse
    
    case get
    
    var method: HTTPMethod {
        switch self {
        case .get: return .get
        }
    }
    
    var path: String { // 先ほど定enumで定義したもの
        return APIConstants.xxx.path
    }
    
    var parameters: Parameters? { // サーバーになにか送る必要がある場合は、こちらでセットする
        return nil
    }
    
}
```

ここまでで、下準備は完成です。

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

### ①Resultタイプの作成

通信成功時は``Codable``に準拠したものが返ってくるように設定します。

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

``Swift4.1``から``Codable``のキーに関して、 **SnakeCace ⇆ CamelCace**の変換がデフォルトでやりやすくなったので、Extensionで拡張してデフォルトで付与しています。

また、それ用のデコーダーを先に定義しておきます。
（使用先でハードコードしていただいても問題ないです。個人的には記述が長くなるのが好きではないだけなので）

```swift
// MARK: - JSONDecoder Extension
extension JSONDecoder {
    
    convenience init(type: JSONDecoder.KeyDecodingStrategy) {
        self.init()
        self.keyDecodingStrategy = type
    }
    
    static let decoder: JSONDecoder = JSONDecoder(type: .convertFromSnakeCase)
}
```

### ②通信部分の作成

強制キャストしているところがありますが、それは**必ずなることが担保されている**ので行なっています。
（自分は基本的には強制キャストしません。ただ**不必要なnilチェックでコードが汚くなる**のは嫌いなので）

```swift
import UIKit
import Alamofire

// MARK: - API Cliant
struct APICliant {

    // MARK: Private Static Variables
    private static let successRange = 200..<400
    private static let contentType = ["application/json"]


    // MARK: Static Methods

    // 実際に呼び出すのはこれだけ。（rxを隠蔽化しているだけなので、observeでも大丈夫）
    static func call<T, V>(_ request: T, _ disposeBag: DisposeBag, onSuccess: @escaping (V) -> Void, onError: @escaping (Error) -> Void)
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
                
            _ = observe(request)
                .observeOn(MainScheduler.instance) // mainを指定してしまっているので消しても良い
                .subscribe(onSuccess: { onSuccess($0) },
                           onError: { onError($0) })
                .disposed(by: disposeBag)
    }
      
    // RxSwiftを導入している部分。成功/失敗いずれかしか返らないSingleにしてある。
    static func observe<T, V>(_ request: T) -> Single<V>
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
                
            return Single<V>.create { observer in
                let calling = callForData(request) { response in
                    switch response {
                    //※ 既にsuccessしているので「as! V」で強制キャストしている（できる）
                    case .success(let result): observer(.success(result as! V))
                    case .failure(let error): observer(.error(error))
                    }
                }
                    
                return Disposables.create() { calling.cancel() }
            }
    }
        
    // Alamofire呼び出し部分
    private static func callForData<T, V>(_ request: T, completion: @escaping (APIResult) -> Void) -> DataRequest
        where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
                
            return request(request).responseJSON { response in
                switch response.result.flatMap(request.decode) {
                case .success(let data): completion(.success(data))
                case .failure(let error): completion(.failure(error))
                }
            }
    }

    // Alamofireのメソッドのみ切り出した部分
    private static func request<T>(_ request: T) -> DataRequest
        where T: BaseRequestProtocol {
        
            return Alamofire
                .request(request)
                .validate(statusCode: successRange)
                .validate(contentType: contentType)
    }
}


```

ちょっといきなりかなりの量だったかもしれませんが、、、笑
使用する時は**かなり綺麗にかけます**。

##**使用例**

```swift 
let disposeBag = DisposeBag()

APICliant.call(xxxRequest.get, disposeBag, onSuccess: {
  // do some thing

}, onError: {
  // error handling

})
```

たったのこれだけです！

自前で``do``, ``map``でレスポンスを加工したり、``observeOn``などでQueueを変えたい場合は、``observe``の方を呼び出すと良いでしょう。

```.swift
let disposeBag = DisposeBag()

APICliant.observe(request)
    .observeOn(MainScheduler.instance)
    .subscribe(onSuccess: { response in
        print("onSuccess")
    }, onError: { error in
        print("onError")
    })
    .disposed(by: disposeBag)
```

### 複数APIの呼び出し

``Observable``に変換して``combineLatest``でくっつけることで実現可能です。

以下に例を用意しておきます。（型は存在しないものを使用しているので、各自のものにしてください）

```.swift
let disposeBag = DisposeBag()
let relay = PublishRelay<Void>() 

func call(onNext: @escaping (XXXYYY) -> Void) {
    // API １つ目
    let xxxObservable: Observable<XXX> = relay.flatMapLatest { [weak self] _ in
        let request = XXXRequest()     
        return APICliant.observe(request)
    }

    // API 2つ目
    let yyyObservable: Observable<YYY> = relay.flatMapLatest { [weak self] _ in
        let request = YYYRequest()     
        return APICliant.observe(request)
    }

    Observable
        .combineLatest(xxxObservable, yyyObservable) { 
            // ２つのデータを加工する処理（'XXXYYY'の型になるように変換）
        }
        .subscribe(onNext: onNext)
        .disposed(by: disposeBag)

    relay.accept(())
}
```

リクエストに引数が必要な場合は、callする関数なりに追加して渡してください。
（また、今回は成功時の処理しかやっていませんが、必要に応じてエラー処理も各自追加してください）

# あとがき
~~個人的にはまだ``Object Mapper``の方が便利かなぁと思ってしまいます笑~~
だいぶCodableが流行ってきており、使い勝手もよくなってきたような気がしています。

型変換が不便なのと、Codable準拠のレスポンス変数に対して**didSetが使えない**のが個人的には辛いですね。
（→レスポンス受け取った瞬間に**didSetで何かする**ことが**できなくなった**(didSetが呼ばれない)ので）

#### PS
プロジェクトのクラス名をまんま使用するわけにいかず、一部リネームして表記しているので動かなかったらすいません、、、
というより、誤字脱字あればお願いいたします笑