LoginSignup
1
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-05-08

はじめに

使う時に手間がかかったりあまり整理されてないコードを業務でよく見かけます。
「文句を垂れるくらいなら良いコードを自分で示さないと!」と言う気持ちからまとめてみました。
今回ライブラリは使いません 良くも悪くも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 が来たら書き換えます!
1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3