iOSDC Japan 2019公式アプリで採用しているAPI通信周りの実装

こんにちは。iOSエンジニアの[@h1d3mun3](https://twitter.com/h1d3mun3)です。

皆さん、今年9月5日から9月7日まで、早稲田大学で開催された「[iOSDC Japan 2019](https://iosdc.jp/2019/)」にはご来場いただけましたでしょうか。

エンジニアが主役のお祭りとして、毎年多くの方にご来場いただいております。

iOSDC Japan で公式アプリを出させて頂く機会に恵まれました。

[DC Japan](https://apps.apple.com/jp/app/dc-japan/id1470400910)

無事リリースすることもでき、Opt-In Onlyながら、会期期間中のクラッシュが0件を達成することができ、大変良かったと思っております。

アプリで一番難儀する場所といえば多分API通信周りなのではないでしょうか。

ここでは、iOSDC Japan 2019公式アプリで採用したAPI通信周りの知見を共有することにより、将来API通信周りの実装で悩む人の一助となることを期待しています。

## 1. はじめに - iOSDC Japan公式アプリのアーキテクチャ

iOSDC Japan公式アプリ (以下本アプリ)では、Kickstarter式のMVVMアーキテクチャを採用しています。

1つの画面を

- ViewController
- ViewModel (Inputs / Outputsで切り分け)

の2つのファイルで構成しています。

-  `ViewModel`が `Outputs` で `Observable` を公開している
-  `ViewController` 側で `OutPuts` を `subscribe` する
-  `ViewController` が `ViewModel` の `inputs` の処理を呼ぶ
、というイメージです。

いわゆる 「ビジネスロジック」はすべて `ViewModel` 側に持たせており、 `ViewController` は表示を行うだけ、という状態です。

非同期の処理が主となるため、実装には `RxSwift` を採用しています。

Kickstarter式のMVVMアーキテクチャについては下記記事によくまとまっております。
(僕もこれを読んで勉強いたしました)

[Kickstarter-iOSのViewModelの作り方がウマかった](https://qiita.com/muukii0803/items/045b12405f7acff1a9fd)

また、構造体の場合イニシャライザを書かなくても全項目イニシャライザが自動で用意されるので楽という理由で、クラスよりも構造体を多用する設計にしています。

## 2. API通信周りの実装

ここでは、タイムテーブルを取得する処理を例に上げ、本アプリがどのような仕組みで通信を行っているかを説明します。

### 2.1 HTTP通信を担当する構造体

本アプリでは下記のような構造体を宣言し、HTTP通信を担当させています。
`GET` と `POST` しかないのは単純にそれ以外が必要なかったからです。必要に応じて追加します。

``` swift
import Foundation
import RxSwift

protocol HttpProtocol {
    func get(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)>
    func post(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)>
}

struct Http {
}

extension Http: HttpProtocol {
    func get(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)> {
        var urlComponents = URLComponents(string: url)!
        urlComponents.queryItems = param?.map { URLQueryItem(name: $0, value: "\($1)") }
        var request = URLRequest(url: urlComponents.url!)
        request.timeoutInterval = TimeInterval(30)
        request.httpMethod = "GET"
        return URLSession.shared.rx
            .response(request: request)
            .asSingle()
    }

    func post(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)> {
        var request = URLRequest(url: URL(string: url)!)
        request.timeoutInterval = TimeInterval(30)
        request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"
        if let param = param {
            request.httpBody = param.percentEscaped().data(using: .utf8)
        }
        return URLSession.shared.rx
            .response(request: request)
            .asSingle()
    }
}
```

作りとしてはとても単純で、

1. `HttpProtocol` により、振る舞いを制限 (これよりテスタブルになる)
2. `HttpProtocol` を適合した構造体をつくって実際の通信をそこに埋め込む

というだけです。

通信の結果は `Single` で帰ってくるようになっており、非同期処理をするのに便利な作りにしてあります。

(Singleにするので必ず結果が帰ってくる or エラーするのどちらかにしかならないので便利)

### 2.2 実際のAPI通信コード

実際にタイムテーブルを取得してくる構造体は下記のようなコードになります。

具体的な `APIエンドポイント` などは、`APIConstants` に集約しております。

```swift
import Foundation
import RxSwift

struct TimetableRequestApi {
    let http: HttpProtocol

    func call() -> Single<TimetableAPIResponse> {
        return http.get(url: APIConstants.timetable.url, param: nil)
            .map { response -> TimetableAPIResponse in
                if response.response.statusCode != 200 { throw DCError.internalServerError }
                let decoder: JSONDecoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                decoder.dateDecodingStrategy = .iso8601
                do {
                    let response = try decoder.decode(TimetableAPIResponse.self, from: response.data)
                    return response
                } catch {
                    throw DCError.jsonParseError
                }
        }
    }
}
```

APIからのレスポンスのマッピングには `Decodable` を利用しています。

このAPIでは、 `TimetableAPIResponse` という `Decodable` に準拠したモデルにデコードしています。

### 2.3 実際の利用ケース

実際の利用では下記のように利用します。

```swift
let request = TimetableRequestApi(http: Http())

request.call()
    .subscribe(onSuccess: { [weak self] result in
        guard let self = self else { return }
        // DO SOMETHING WHAT YOU WANT
        }, onError: { error in
        // エラー処理とか
    })
    .disposed(by: disposeBag)

```

### 2.4 補足 - リクエストにidなどが必要な場合

リクエストに当たりパラメータが必要な場合(たとえばある特定のメンバーの情報が欲しいのでリクエストの際にメンバーIDが必要、などの場合)は、 `RequestAPI` クラスに情報をもたせる作りにしてあります。


## 3. おわりに

API通信周りは基本的にどのアプリでも実装する(はず)のものなので、皆様苦心されるところだと思います。

僕も苦心しておりましたが、本アプリで実装した形が **ひとまず** 僕にとってスッキリする実装でしたのでQiitaにで共有させていただきました。

こういう形があるよ！とかあると思うので、皆さま是非是非参考にしてAPI周りをきれいに実装しちゃってくださいませ〜！