Swift API通信の綺麗なまとめ方

# はじめに

使う時に手間がかかったり**あまり整理されてないコード**を業務でよく見かけます。
「文句を垂れるくらいなら良いコードを自分で示さないと!」と言う気持ちからまとめてみました。
今回ライブラリは使いません　**良くも悪くもSwiftらしいコード**で書いてみました。

- 人的ミスを**コンパイルエラーで検知**できる
- 設計書を読み込まなくても人やAPI毎の**書き方の差異が出ない**

以上を心がけています。細かなコメントは最後に書きます。

# アプリ毎に 1つ 作成
ほとんどの場合はそのままコピペでOKです。

```HTTPClient.swift
import Foundation

enum HTTPMethod: String {
    case GET
    case POST
    case PUT
    case PATCH
    case DELETE
}
protocol JSONSerializable: Codable {
    init(jsonData: Data)
    var jsonData: Data { get }
    static var dateFormatter: DateFormatter? { get }
}
extension JSONSerializable {
    init(jsonData: Data) {
        let decoder = JSONDecoder()
        if let formatter = Self.dateFormatter {
            decoder.dateDecodingStrategy = .formatted(formatter)
        }
        self = try! decoder.decode(Self.self, from: jsonData)
    }
    var jsonData: Data {
        return try! JSONEncoder().encode(self)
    }
    static var dateFormatter: DateFormatter? {
        return nil
    }
}
protocol RequestBody: JSONSerializable {
    // 設計として各リクエストに必ず定義させたい項目があればここに書く
    // <例> let timeoutSec: Int { get }
}
protocol ResponseBody: JSONSerializable {
    // 設計として各レスポンスに必ず定義させたい項目があればここに書く
    // <例> let message: String { get }
}
protocol API {
    static var urlPath: String { get }
    static var httpMethod: HTTPMethod { get }
    associatedtype Request: RequestBody
    associatedtype Response: ResponseBody
}
extension API {
    static var API: Self.Type { return Self.self }
}
protocol HTTPClient {
    typealias CustomHeader = (value: String, field: String)
    var baseURL: String { get }
    var headers: [CustomHeader] { get }
}
extension HTTPClient {
    /// ヘッダーに付加したい情報
    var headers: [CustomHeader] {
        return [(value: "application/json", field: "Content-Type")]
    }
    /// 送信
    /// - Parameters:
    ///   - apiRequest: リクエスト
    ///   - onSuccess: レスポンス取得成功時の処理
    ///   - onError: 個別エラー処理
    func send<T: API>(_ requestBody: T.Request,
                      to api: T.Type,
                      onReceive: @escaping (_ response: T.Response) -> Void,
                      onCatch: @escaping (Error) -> Void) {
        
        var urlRequest = URLRequest(url: URL(string: baseURL + T.urlPath)!)
        urlRequest.httpMethod = T.httpMethod.rawValue
        // GET 以外の場合のみ body を設定
        if T.httpMethod != .GET {
            urlRequest.httpBody = requestBody.jsonData
        }
        // ヘッダー属性を付加
        for header in headers {
            urlRequest.addValue(header.value, forHTTPHeaderField: header.field)
        }
        let task = URLSession.shared.dataTask(with: urlRequest, completionHandler: { data, response, error in
            if let error = error {
                onCatch(error)
            }
            else {
                let responseBody = T.Response(jsonData: data!)
                onReceive(responseBody)
            }
        })
        // 実行
        task.resume()
    }
}
```

# 通信先のベースURL 毎に 1つ 作成

```MyService``` : 対応するWebサービスを識別できる名前
```baseURL```: URLのうちAPIのパス分岐直前までの部分

```_MyService_Client.swift
/// MyServiceの部分には、対応するWebサービスを識別できる名前を入れる
struct <#MyService#>Client: HTTPClient {
    let baseURL = <#"http://0.0.0.0:8000/api/v1"#>
}
```

# API毎に 1つ 作成

```MyAPI``` : 対応するAPI名
```urlPath``` : APIまでのパス
```httpMethod``` : HTTPメソッド
```my_param: Type``` : bodyに含めるパラメーター

- フィールド名の命名規則は、**API側に合わせる**ルールにしても問題ないはずです
  - <例> myParam ではなく my_param にする
  - どうしてもずらしたい場合は ```enum Key``` を使いましょう 

``` _MyAPI_.swift
enum <#MyAPI#>: API {
    static var urlPath = <#"/path/to"#> // パスURL
    static var httpMethod = <#HTTPMethod.POST#> // HTTPメソッド
    
    struct Request: RequestBody { // リクエストパラメーター
        let <#my_param1: Type#>
        let <#my_param2: Type#>
        // ...
    }
    struct Response: ResponseBody { // レスポンスパラメーター
        let <#my_param1: Type#>
        let <#my_param2: Type#>
        // ...
    }
    /* Date型を含む場合は以下のようにフォーマット指定 */
//  static var dateFormatter: DateFormatter? {
//      let formatter = DateFormatter()
//      formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
//      return formatter
//  }
}
```

# 使用例

ある果物のデータを保存して、登録された ```ID``` と ```日時``` が返されるようなAPIがあるとします

- **サービス名**: GreenStore
- **API名**: save-fruit
- **URL** : "http://0.0.0.0:8000/api/v1/food/fruits"
- **HTTPメソッド** : POST
- **果物名** : りんご
- **価格** : 120 (円)
- 登録されるID : 99
- 登録日時: 2020/12/31 23:59:59 

### コード

```GreenStoreClient.swift
/* グリーンストア */
struct GreenStoreClient: HTTPClient { // サービス名
    let baseURL = "http://0.0.0.0:8000/api/v1" // ベースURL
}
```

```SaveFruit.swift
/* 果物データ保存 */
enum SaveFruit: API {
    static var urlPath = "/food/fruits" // パスURL
    static var httpMethod = HTTPMethod.POST // POST

    struct Request: RequestBody {
        let name: String // 果物名
        let price: Int // 価格
    }
    struct Response: ResponseBody {
        let fruit_id: String // ID
        let created_at: Date // 登録日時
    }

    static var dateFormatter: DateFormatter? {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" // 日時のJSONフォーマット
        return formatter
    }
}
```

```Sample.swift
/* 通信実行 */
    ...

    let request = SaveFruit.Request(name: "りんご", // 果物名: りんご
                                    price: 120) // 価格: 120
        
    // リクエストを送信
    GreenStoreClient().send(request, to: SaveFruit.API, onReceive: {
        response in
        // レスポンス取得成功
        print(response.fruit_id, // ID: 99
              response.created_at) // 登録日時: 2020-12/31T23:59:59
        
    }, onCatch: {
        error in
        print(error.localizedDescription) // エラー出力
    })

    ...
```

### 細かいこだわりポイント

#### - コピペのままでは使えないコードにはプレースホルダーを入れておく
  - Xcodeでコードの中に **<#プレースホルダー#>** と書くと、お馴染みのプレースホルダーが作れます。
  - 意外と知らない上級者の方も多いかも?!


#### - 名前空間の表現は**プレフィックスではなくenum**を使う
  -  個人的によく業務で見かける命名として```XXXRequest``` , ```XXXResponse``` の **ペアを作らせる** ルールがありますがあまり好ましくないと思っています。
  -  **プレフィックス**によってこの **2つの関係性を認知** しているのは人間だけです。
  -  片方だけ ```YYYRequest``` に変更して、もう一方は ```XXXResponse``` のまま残っていても気付けないことがあります。単純に数カ所書き換える手間もあります。

#### - コール方法を ```send()``` にした理由
  - よくライブラリなんかで ```HTTP.get("https://...")```のような書き方がありますが, **URL**-**HTTPMethod**-**API**-**パラメータ**の紐付けはメソッド実行よりも前に**定義の時点で行たい**理由から採用しませんでした。
  - ただ、別のメソッドを用意してラップすれば紐付けはいくらでも可能なので、こちらは否定する意図は全くありません。多くのサーバーサイドフレームワークのルーティングとも書き味が似ていて使いやすいと思います。

# さいごに

- すべて机上で書いたので、抜けや漏れになっている観点があれば教えてください。
- 逆にこの書き方ではよく分からなくて教えて欲しいことがあればどうぞご質問ください。
- 私もまだまだ未熟者ですので、否定的なアドバイスでも大丈夫です。よろしくお願いします。
- Swift5.5 で **async/await** が来たら書き換えます！