Alamofireでパラメータをenumで扱えるようにする
Alamofireといえば、言わずと知れたSwift界のHTTPクライアント。名前の由来はテキサスの花の名前らしいすね。
今回はAlamofireのリクエストパラメータをenumで扱うという話。
大前提。Stringは脆い!
Stringはすべての表現を兼ね備える万能な型です。
色は"#FFFFFF"という値で扱うこともできるし、URLも"http://blog.matsuokah.jp/"と扱うことができます。
故に、APIレスポンスの型にStringが採用される変数も多いですよね。
(数値とBool以外Stringで受け取ってクライアントでパースするのが一般的なような)
しかしながらStringを型として用いる場合、その変数には「特定の値が入っている」ことを前提として扱わなければならないので、コードの堅牢性を下げうる型とも言えます。
let color: String = "http://blog.matsuokah.jp/"
と入力できてしまうが故にエラーはランタイムで発生することでしょう。コンパイラ言語のありがたみもありません。
今回はこれをAlamofireのパラメータに当てはめて考え、キー値をenumで扱えるようにしました
Alamofireのパラメータは [String: Any]
という形式
AlamofireではAlamofire.Parameters
というエイリアスが[String:Any]
に参照しており、
Dictionaryをセットするだけでリクエストに応じて柔軟にパラメータを設定してくれる形式になっています。
dictionaryを代入するポイントはAlamofire.request
のparamters:
なので、ラップする箇所や各所のAPIサービス周辺でパラメータを用意する形式がシンプルなやり方可と思います。
APIClientにAPIのエンドポイント、パラメータ、返却する方をジェネリクスで決めてDataStoreやModelからコールするイメージです
struct SearchAPIService { let apiClient: APIClient // APIをコールするServiceのメソッドの一例 func searchUser(userName: String) -> Observable<SearchResult> { var paramter = [String: Any]() parameter["user_name"] = userName return apiClient.get(endpoint: endpoint, parameter: parameter) } }
Parameterをenum化する
import Foundation import Alamofire /// Dictionaryから新たなDictionaryを作る // MARK: - Dictionary extension Dictionary { // MARK: Public Methods public func associate<NKey, NValue>(transformer: @escaping ((Key, Value) -> (NKey, NValue))) -> Dictionary<NKey, NValue> { var dic: [NKey: NValue] = [:] forEach { let pair = transformer($0.key, $0.value) dic[pair.0] = pair.1 } return dic } } // MARK: - ParameterKey public protocol ParameterKey: Hashable { var key: String { get } } // MARK: - RequestParameter public protocol RequestParameter { associatedtype T : ParameterKey // MARK: Internal Properties var parameters: Alamofire.Parameters { get } // MARK: Private Properties var parameter: [Self.T: Any] { get set } // MARK: Internal Methods mutating func setParameter(_ key: Self.T, _ value: Any) } // MARK: - APIParameter public extension RequestParameter { public var parameters: Alamofire.Parameters { if parameter.isEmpty { return [:] } return parameter.associate { (param, any) in return (param.key, any) } } public mutating func setParameter(_ key: Self.T, _ value: Any) { parameter[key] = value } }
// MARK: - SearchParameterKeys public enum SearchParameterKeys: ParameterKey { case userName // MARK: Internal Properties public var key: String { switch self { case .userName: return "user_name" } } } // MARK: - SearchParameter public struct SearchParameter: RequestParameter { public typealias T = SearchParameterKeys public var parameter: [SearchParameterKeys : Any] = [:] }
struct SearchAPIService { let apiClient: APIClient // APIをコールするServiceのメソッドの一例 func searchUser(userName: String) -> Observable<SearchResult> { var paramter = SearchParamter() parameter.set(.userName, userName) return apiClient.get(endpoint: endpoint, parameter: parameter.parameters) } }
こうすることで、SearchAPIに対して設定できるパラメータはSearchParameterのみ。SearchParameterでセットできるキー値はSearchParameterKeysのみとなりました。
また、q
というキーに対して今はenum値q
を用いてますがquery
のような、デスクリプティブな表現もできるようになります。
Pros & Cons
Pros
- キーに設定できるパラメータが絞られるようになった
- APIServiceのインタフェースにParameterを用いることができるようになったので、引数が少なく、拡張性をもたせることができる
- キーenumはキー文字列へのaliasなのでより、説明的な表現ができる
Cons
- キー値の共通がこのままだとできないので、API毎に似たような定義が増える
まとめ
Consに関してはプロトコルで共通部分を抜き出すなどすればなんとかできそうだと思っています。
また、主観ですがAPIに対して特定のキー値があることのほうが多いので問題に当たるケースは少ないかなと楽観してます。
今回載せたコードは下記のリポジトリに載せてます。(テストは動きません無)