【Swift】Alamofire x Codable 綺麗にAPI接続するためのテンプレート

G'day mate、しょーさんです。

梅雨に入りましたね。
バイクに乗れない週末が続くのでリフレッシュできなくて辛いです。
そんな日は、API接続のお勉強をしよう。

今日やっていくことは

API接続する際のリクエストを綺麗に作っていくために行う下準備

というところです。

個人アプリでAPIを1つだけ叩く場合などは、ただつらつらリクエストからレスポンスまでツラツラすればいいですが、業務になってくると様々なpathの数だけリクエストを作成せねばなりません。
それを共通化できるところは共通化して、楽にAPIを追加してしけるようにリクエストの共通フレームを準備すれば後々追加していく時も楽ですよね。
その共通フレーム部分を作成していきます。

前置きが長くなってしまったがやっていこう!
genericsやtypealiasをふんだんに使っていくぞい。

前提

前提として、以下のような形のレスポンスに対応します。

{
    "head":{
        "code":0,
        "message":""
    },
    "body":{
        "name": "John",
        "studend_id": 123,
        "department": "programming"
    }
}

body = nil の場合も想定します。

{
    "head":{
        "code":1,
        "message":"プロパティちゃいますねん"
    },
}

API接続にはAlamofireを使ってみよう。
(導入フローはやりません)

ベースを作成

まずはベースとなるRequestを作成していきます。
これはこれからstruct で各APIリクエストをCodable(厳密にはencodeする必要がないのでDecodableを使用)で記述する際のベースになります。

import Alamofire

protocol Request {
    associatedtype Body

    var method: HTTPMethod { get }
    var baseURL: String { get }
    var path: String { get }

    var headers: [String: String] { get }
    var parameters: [String: Any]? { get }

    func request<req: Request>(_ req: req, completion: ((Response<req.Body>) -> ())?)
    func decode(from data: Data) throws -> Response<Body>
}
  • associatedtype Body : 後のDecodableで書き出した構造体で実装します
  • method : Alamofireに実装されている .get, .post などのHTTPMethodを使用します
  • baseURL : https://foo.jp/api 等の固定URLに使用します
  • path : それぞれのAPIパスを準拠させるようにします
  • headers : リクエストヘッダーをセットします
  • parameters : リクエストパラメータをセットします
  • func request : リクエストする際に使用するメソッド
  • fun decode : レスポンスをデコードする際に使用するメソッド

この protocol Request を struct FooRequest に準拠させることで、Codable書き出しを楽にしていきます。
まずはこのプロトコルの実装を整えていこう

固定値を整える

Protocol Extension を使用して、固定値を使用するフィールドにはデフォルト値を与えていきます。
この場合、

  • baseURL
  • headers

はどのリクエストでも固定で添付する値なので、デフォルト値を与えてしまいます。

extension Request {
    var baseURL: String { return "https://foo.jp/api" }
    var headers: [String: String] { return [
       "API_KEY": "123456789",
        "APP_KEY": "1234ABCD"
    ] }
    var parameters: [String: Any]? { return nil }
}
  • baseURL: 固定URLをデフォルト値として付与
  • headers : 静的なheader値が必要な場合はデフォルト値を付与
  • parameters : リクエストでパラメータが付与な場合はnilを付与するように設定

パラメータは各APIで動的に付与していくのでここでは実装しません。
次はリクエストメソッドの方を整えていきます。

リクエストメソッドを実装する

request(_:)は、BodyがDecodableに準拠している場合にのみ使用したいので、protocol extension に where を付与してデフォルト実装をします。
引数のRequestにはGenericsを使用していきます。

extension Request where Body: Decodable {
    func request<req: Request>(_ req: req, completion: ((Response<req.Body>) -> ())?) {
        Alamofire.request(req.baseURL + req.path,
                                    method: req.method,
                                    parameters: req.parameters,
                                    encoding: JSONEncoding.default,
                                    headers: req.headers)
            .response { response in

            }
    }
}

Genericsにすることにより、後に実装するdecode(_:)がreq.decode(_:)として使用できるようになります。
reqには これから作成する struct FooRequest: Request を与えてやることにより、構造体が持っているフィールドをAlamofireに渡すことができます。
上記で記述した固定値と、構造体側で設定する動的な値がここで付与されることになりますね。

そしてcompletionを設定した先には何やらResponse<req.Body>型が返るようになっていますね。
では作成しましょう。

public struct Response<Body> {
    public struct Head {
        public let code: String?
        public let message: String?
    }
    public let head: Head
    public let body: Body
}

前提で記述したJSONのように、headは上記のように、BodyはAPIごとに動的に変わりますね。
なので、Protocol Requestで記述したassociatedtype Body、つまりそれぞれのリクエストで実装するBodyがここに収まるようになるわけです。
associatedtypeってこのようにAPIレスポンスごとに型を変えたいときに便利ですね。

リクエストはこのような感じです。

次は返ってきたレスポンスをデコードする処理を書いていくぞい。

レスポンスをデコードする

.response { response in 以下に記述していきます。
まずはレスポンスからdataを取得します。

guard let data = response.data else { return }

次はこのdataをデコードしていきます。

do {
         let model = try req.decode(from: data)
         completion?(model)
} catch let error {
         print("error decode json \(error)")
}

func decode(from data: Data) throws -> Response<Body> {
    let response = try JSONDecoder().decode(DecodableResponse<Body>.self, from: data)
    return Response<Body>(head: .init(code: response.head.code.description,
                                                        message: response.head.message),
                                        body: response.body)
    }

catch 文には必ずerrorを記述しましょう。
ここにerrorがあるとないとではデバッグが全然違いますから!
ここで出力できるようにすると、デコード時に何がエラーになってるか吐き出してくれるので、、
これをやらなかったあまりに私は沢山時間を溶かしてしましました、、

デコードする際の型はDecodableResponseという物を別途作成します。

struct DecodableResponse<Body: Decodable>: Decodable {
    struct Head: Decodable {
        let code: Int
        let message: String

        private enum CodingKeys: String, CodingKey {
            case code = "code", message = "message"
        }
    }
    let head: Head
    let body: Body

    private enum CodingKeys: CodingKey {
        case head, body
    }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        head = try c.decode(Head.self, forKey: .head)
        body = try c.decode(Body.self, forKey: .body)
    }
}

先ほど実装したResponseのように、headは固定値、bodyには動的に入るようにGenericsで設定します。

これでレスポンスはこの形に全て当てはまるように、さらにbodyはそれぞれのAPIに応じて変わるようにデコードされます。

それではAPIレスポンスのDecodableの書き出しを行っていこう。

APIレスポンスを実装する

あとはAPIパスに応じてstruct FooRequestL: Requestを追加していくだけ。

import Alamofire

struct AppSettingDataRequest: Request {
    var method: HTTPMethod { return .post}
    var path: String { return "/barbar"}

    init() {}
}

extension AppSettingDataRequest {
    struct Body: Decodable {
        let name: String
        let studentId: Int
        let department: String
    }
}

extension AppSettingDataRequest.Body {
    enum CodingKeys: String, CodingKey {
        case name = "name"
        case studentId = "student_id"
        case department = "department"
    }
}

  • method : APIごとに変わるHTTPMethodは構造体で記述
  • path : APIごとに変わるAPIパスは構造体で記述
  • parameters : リクエストに必要ない場合は記述しない(デフォルト値はnilを設定してある)
  • Body : APIごとに変わるBodyは構造体でDecodableに準拠されて記述

これをAPIごとに追加していくだけ、ファイルも綺麗になりますね。
リクエストにパラメータが必要な場合は

var parameters: [String : Any]? {
        return [
            "hoge": hoge,
        ]
        .cleaned
}

let hoge: String

init(hoge: String) {
    self.hoge = hoge
}

と記述します。
こうすることで、

FooRequest(hoge: hoge)

といった風に動的にパラメータを付与して使用でき、
.cleanedメソッドは上記の型を辞書型に整えてくれるメソッドで、以下を先ほどのRequest.swiftファイルなどに記述しておくとセットで使用できます。

extension Dictionary where Key == String, Value == Any? {
    var cleaned: [Key: Any] { return clean(self) }
}

func clean<Key, Value>(_ dict: [Key: Value?]) -> [Key: Value] {
    var result: [Key: Value] = [:]
    for (key, value) in dict {
        if let value = value {
            result[key] = value
        }
    }
    return result
}

ちなみにBodyが空のレスポンスなどもあると思います。
その場合はtypealiasが無いよと怒られてしますので、空のbodyを実装すればいいです。
Requestファイルに以下を追加しておきます。

struct EmptyBody: Decodable {}

もしBodyが無い場合は以下のように付与してあげればおkです。

import Alamofire

typealias Body = EmptyBody

struct FooRequest: Request {
    var method: HTTPMethod { return .post}
    var path: String { return "/barbar"}

    init() {}
}

最後にこれらを実行するメソッドを作成していく〜。

実行メソッドの記述

func sendApi<Req: Request>(_ req: Req, completion: @escaping (Response<Req.Body>) -> Void) {
    req.request(req) { response in
        completion(response)
    }
}

// 使い方
sendApi(FooRequest()) { response in
     print(response)
}
// とか
let hoge = "hogehoge"
sendApi(BarRequest(hoge: hoge)) { response in
     print(response)
}

実行メソッドによって渡されたRequest構造体が、メソッドないでrequestメソッドを呼んでいます。
そしたら先ほど作成したリクエストメソッドに適当な値が当てはめられることとなります。

フルコード

import Foundation
import Alamofire

struct EmptyBody: Decodable {}

protocol Request {
    associatedtype Body

    var method: HTTPMethod { get }
    var baseURL: String { get }
    var path: String { get }

    var headers: [String: String] { get }
    var parameters: [String: Any]? { get }

    func request<req: Request>(_ req: req, completion: ((Response<req.Body>) -> ())?)
    func decode(from data: Data) throws -> Response<Body>
}

struct Response<Body> {
    public struct Head {
        public let code: String?
        public let message: String?
    }
    public let head: Head
    public let body: Body
}

extension Request {
    var baseURL: String { return "https://foo.jp/api" }
    var headers: [String: String] { return [
       "API_KEY": "123456789",
        "APP_KEY": "1234ABCD"
    ] }
    var parameters: [String: Any]? { return nil }
}

struct DecodableResponse<Body: Decodable>: Decodable {
    struct Head: Decodable {
        let code: Int
        let message: String

        private enum CodingKeys: String, CodingKey {
            case code = "code", message = "message"
        }
    }
    let head: Head
    let body: Body

    private enum CodingKeys: CodingKey {
        case head, body
    }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        head = try c.decode(Head.self, forKey: .head)
        body = try c.decode(Body.self, forKey: .body)
    }
}

extension Request where Body: Decodable {
    func request<req: Request>(_ req: req, completion: ((Response<req.Body>) -> ())?) {
        let req = Alamofire.request(req.baseURL + req.path,
                                    method: req.method,
                                    parameters: req.parameters,
                                    encoding: JSONEncoding.default,
                                    headers: req.headers)
            .response { response in
                guard let data = response.data else { return }

                do {
                    let model = try req.decode(from: data)
                    completion?(model)
                } catch let error {
                    print("error decode json \(error)")
                }
            }

        debugPrint(req)
    }
    func decode(from data: Data) throws -> Response<Body> {
        let response = try JSONDecoder().decode(DecodableResponse<Body>.self, from: data)
        return Response<Body>(head: .init(code: response.head.code.description,
                                          message: response.head.message),
                              body: response.body)
    }
}

// Use this for params
extension Dictionary where Key == String, Value == Any? {
    var cleaned: [Key: Any] { return clean(self) }
}

func clean<Key, Value>(_ dict: [Key: Value?]) -> [Key: Value] {
    var result: [Key: Value] = [:]
    for (key, value) in dict {
        if let value = value {
            result[key] = value
        }
    }
    return result
}

func sendApi<Req: Request>(_ req: Req, completion: @escaping (Response<Req.Body>) -> Void) {
    req.request(req) { response in
        completion(response)
    }
}

はい、良きですね。

終わりに

APIを追加していく必要がある、複数ある場合はこのようなベースを用意しておくと変化に強いんじゃ無いでしょうか。
あと綺麗にかける気がするかな?

もしよかったら実装してみてください。

以上だ!長かったな!
お疲れ様!
Catch ya!