Swift4 + Moya + RxSwift + Codableで作るAPIクライアント

# Moyaとは

MoyaはAlamofireのラッパーで、iOSアプリを作る時に作成されるAPI通信周りの処理をまるっと引き受けてくれるやつです。触ろうと思えばAlamofireを直接さわれるので、Moyaを使って出来ないことは無いです。Alamofireを利用しているならぜひ使いたいライブラリですね。
また、公式にRxSwiftに対応しているため、独自にRxSwift用にレスポンスの変換処理を書く必要がないのも嬉しいです。Moyaに限らずAPI通信周りを担ってくれるほとんどのライブラリはEitherの形でレスポンスが返ってきます。それを自分たちでRxに変換するコードをわざわざ書いているのであれば、Moyaを使って必要な開発にリソースを割いた方がいいです。

# Moyaのインストール

Cartfileに`Moya/Moya`を追加します。

```ruby
github "Moya/Moya"
```

その後、`carthage bootstrap --platform ios`　を実行し、`Build Phases`にビルド済みのframeworkの追加とRun Scriptの設定を行いましょう。

![スクリーンショット 2018-03-02 11.29.16.png](https://qiita-image-store.s3.amazonaws.com/0/20406/5478ef8f-1140-76d7-c78d-b7e2ef5ab329.png)

![スクリーンショット 2018-03-02 11.24.21.png](https://qiita-image-store.s3.amazonaws.com/0/20406/213db7c8-a09b-d692-fbf1-c7d4b8004c18.png)

RxSwiftと一緒に利用したいので`RxMoya.framework`と`RxSwift.framework`も追加しています。

# 基本的な使い方

TargetTypeというプロトコルに準拠する形で接続先やパラメータなどを書いていきます。

```swift:Api.swift
import Moya

// URL用に文字列をエスケープするためのやつで直接的には関係ありません
extension String {
    var urlEscaped: String {
        return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }
}

enum GitHub {
    case userProfile(String)
}

extension GitHub: TargetType {
    var baseURL: URL { return URL(string: "https://api.github.com")! }
    var path: String {
        switch self {
        case .userProfile(let name):
            return "/users/\(name.urlEscaped)"
        }
    }
    var method: Moya.Method {
        return .get
    }
    var task: Task {
        switch self {
        case .userProfile(_):
            return .requestPlain
        }
    }
    var headers: [String : String]? { return nil }

    // テストの時などに、実際にAPIを叩かずにローカルのjsonファイルを読み込める
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "samples", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
}
```

```swift:Profile.swift
import Foundation

struct Profile: Codable {
    let login: String
    let url: URL
    let name: String?
    let email: String?
}
```

参考: https://github.com/Moya/Moya/blob/master/docs/Examples/Basic.md
参考: https://github.com/Moya/Moya/blob/master/docs/Targets.md


呼び出しは以下のように行います。

```swift
let disposeBag = DisposeBag()

let provider = MoyaProvider<GitHub>()
provider.rx.request(.userProfile("kouheiszk"))
    .filterSuccessfulStatusCodes()
    .map(Profile.self)
    .subscribe(onSuccess: { (profile) in
        print(profile)
    }) { (error) in
        print(error)
}
.disposed(by: disposeBag)
```

`provider`に対して`rx`を付けて`request`を叩くことでレスポンスをsubscribeできるようになります。
`map(Profile.self)`のところで、レスポンスのJSONをProfileにマッピングしています。ProfileはCodableに準拠していれば特に何も考える必要がありません。
とても簡単ですね。

# 応用的な使い方

基本的な使い方で書いた様なAPIのリクエスト先をcaseで記述する方法だと、リクエスト先が増えた際にcaseが増え、TargetTypeが要求する`var path`や`var method`などの中のswitch-caseも増えていきます。2〜3個ならまだ大丈夫ですが、それ以上にcaseが増えると定義が分散して読み辛いコードが出来上がることが容易に想像できます。
caseで分岐しない、API仕様書をそのまま写した様な書き方をしたいという欲求が出てくると思います。

実はMoyaではこのAPI仕様書をそのまま写した様な書き方ができるので、APIのリクエスト先が複数になる場合ははじめからこの書き方にしておいた方がいいです。

## caseではなくstructでAPIを定義

新たにに`TargetType`に準拠する`GitHubApiTargetType`というprotocolを定義します。`baseURL`など各APIで共通の項目はextensionで定義しておきましょう。

```swift
protocol GitHubApiTargetType: TargetType {
}

extension GitHubApiTargetType {
    var baseURL: URL { return URL(string: "https://api.github.com")! }
    var headers: [String : String]? { return nil }
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "samples", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
}
```

enumのcaseを削除し、`GitHubApiTargetType`に準拠したstructを定義してきます。

```swift
enum GitHub {
    struct GetUserProfile: GitHubApiTargetType {
        var method: Method { return .get }
        var path: String { return "/users/\(name.urlEscaped)" }
        var task: Task { return .requestPlain }
        let name: String

        init(name: String) {
            self.name = name
        }
    }
}
```

APIの定義がまとまり見やすくなりました。
呼び出しはほとんど変わらず、以下のようになります。

```swift
let disposeBag = DisposeBag()

let provider = MoyaProvider<GitHub.GetUserProfile>()
provider.rx.request(GitHub.GetUserProfile(name: "kouheiszk"))
    .filterSuccessfulStatusCodes()
    .map(Profile.self)
    .subscribe(onSuccess: { (profile) in
        print(profile)
    }) { (error) in
        print(error)
}
.disposed(by: disposeBag)
```

## 共通のProviderを利用する

APIの定義は見やすくなったのですが、今度はproviderを各APIリクエスト毎に都度生成しなくてはいけなくなってしまいました。

```swift
GitHub.GetUserRepositories(name: "kouheiszk")
```

のようなAPIリクエストを行いたいと思った時に、

```swift
let provider = MoyaProvider<GitHub.GetUserRepositories>()
provider.rx.request(GitHub.GetUserRepositories(name: "kouheiszk"))
```

の様に新たにProviderを作る必要があるということです。
理想としては共通のProviderを利用して、さらにはそれすら隠蔽して

```swift
Api.request(GitHub.GetUserProfile(name: "kouheiszk"))
```

```swift
Api.request(GitHub.GetUserRepositories(name: "kouheiszk"))
```

のように、リクエストしたいですよね。

## 型消去

それぞれの型に対応したProvider。そうです型消去です。型消去をしていきましょう。
`AnyGitHubApiTargetType`を作成し、`MoyaProvider<AnyGitHubApiTargetType>()`とするイメージです。

しかし早まってはいけません。こういうことはもっと前に考えついている人がいるはずです。
Moyaのドキュメントを見てみるとやはりありました。どうやら以下のようにすれば出来るみたいです。

```swift
let provider = MoyaProvider<MultiTarget>()
let target = MultiTarget(GitHub.GetUserRepositories(name: "kouheiszk"))
provider.rx.request(target)
```

参考：https://github.com/Moya/Moya/blob/master/docs/Examples/MultiTarget.md

これをベースに、理想の呼び出し方を実現させてみましょう。
Codableなレスポンスの型はリクエスト毎に異なるはずなので、`GitHubApiTargetType`にこのレスポンスの型を記述するようにします。

```swift
protocol GitHubApiTargetType: TargetType {
    associatedtype Response: Codable
}

struct GetUserProfile: GitHubApiTargetType {
    typealias Response = Profile

    ...
}
```

リクエストを行うApiクラスは以下のようになります。Providerが開放されると、APIのリクエスト結果をSubscribeできなくなってしまうので、ここではシングルトンでProviderが開放されないようにしています。

```swift
class Api {
    static let shared = Api()
    private let provider = MoyaProvider<MultiTarget>()

    func request<R>(_ request: R) -> Single<R.Response> where R: GitHubApiTargetType {
        let target = MultiTarget(request)
        return provider.rx.request(target)
            .filterSuccessfulStatusCodes()
            .map(R.Response.self)
    }
}
```

呼び出しは以下のようになります。

```swift
let disposeBag = DisposeBag()

Api.shared.request(GitHub.GetUserProfile(name: "kouheiszk"))
    .subscribe(onSuccess: { (profile) in
        print(profile)
    }) { (error) in
        print(error)
}
.disposed(by: disposeBag)
```

とてもシンプルになりました。

# 完成

最後に作成した`Api.swift`の内容を全て貼っておきます。各APIはリクエストの種類毎に`Api+User.swift`の様にファイル分割するといいと思います。

```swift
//
//  Api.swift
//  MoyaSample
//
//  Created by Kouhei Suzuki on 2018/03/02.
//  Copyright © 2018年 Kouhei Suzuki. All rights reserved.
//

import Moya
import RxSwift

extension String {
    var urlEscaped: String {
        return addingPercentEncoding(withAllowedCharacters: .urlHostAllowed)!
    }
}

class Api {
    static let shared = Api()
    private let provider = MoyaProvider<MultiTarget>()

    func request<R>(_ request: R) -> Single<R.Response> where R: GitHubApiTargetType {
        let target = MultiTarget(request)
        return provider.rx.request(target)
            .filterSuccessfulStatusCodes()
            .map(R.Response.self)
    }
}

protocol GitHubApiTargetType: TargetType {
    associatedtype Response: Codable
}

extension GitHubApiTargetType {
    var baseURL: URL { return URL(string: "https://api.github.com")! }
    var headers: [String : String]? { return nil }
    var sampleData: Data {
        let path = Bundle.main.path(forResource: "samples", ofType: "json")!
        return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile()
    }
}

enum GitHub {
    struct GetUserProfile: GitHubApiTargetType {
        typealias Response = Profile

        var method: Method { return .get }
        var path: String { return "/users/\(name.urlEscaped)" }
        var task: Task { return .requestPlain }
        let name: String

        init(name: String) {
            self.name = name
        }
    }
}
```