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