はじめに
こんにちは。ENDoDoです。
このたび、32歳になって、初めてエンジニアとして転職しました。
iOSアプリのSwiftエンジニアになりました。
仕事ではこれまで8年間ずっと企画系のディレクターやアナリストをやってました。
転職したのが先月なので、エンジニアの社歴としてはちょうど1ヶ月になります。
プライベートでも趣味でアプリは作ったりしてたのですが、とにかく動けばいいマインドで作っていたので、今思うととても酷いコードを書いていたと思います。
※この酷いコードについては後日公開したいと思います。
転職を機に勉強を始めたので、どうせなら今後同じようなところで悩む人の少しもの手助けになればと思い、Qiitaも始めてみることにしました。
過去に2記事ほど書いてるのは黒歴史です。
第一弾:AlamofireでAPIClientを作ってみた
今回は第一弾として、AlamofireでAPIClientを作ってみました。
AlamofireはAPIリクエスト用のオープンソースのライブラリとして、とても有名です。
多くの企業でも広く採用されているライブラリと認識しています。
僕も昔からずっとお世話になっています。
APIClient
さて、実装したAPIClientのソースコードはこんな感じです。
import Alamofire
import Foundation
class APIClient {
static var shared = APIClient()
private init() {}
func call<T: RequestProtocol>(request: T, success: @escaping (T.Response) -> Void, failure: @escaping () -> Void) {
let baseUrl = request.baseUrl
let path = request.path
let requestUrl = baseUrl + path
let method = request.method
let encoding = request.encoding
let parameters = request.parameters
let headers = request.headers
Alamofire.request(requestUrl,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)
.validate(statusCode: 200..<300)
.responseJSON { response in
switch response.result {
case .success(_):
do {
guard let data = response.data else { return }
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(T.Response.self, from: data)
success(result)
} catch {
print("suceess catch")
failure()
}
case .failure(_):
print("failure")
failure()
}
}
}
}
クラスについてはインスタンス化するメリットがないので、シングルトン・パターンで実装しています。
メソッド部分はcallになりますが、以下のように記述していただければ呼び出せます。
APIClient.shared.call(request, success, failure) ※()内は引数になります。
request部分はジェネリクスでT型の型変数を使用しました。
事前にRequestProtocolを定義しておくことで、それを継承しているもののみ利用可という制約条件を設けています。
success、failureについてはクロージャによるコールバックを行なっています。
これによって呼び出し元で処理が行えるので、例えばMVP構成で組む時などは、Model側でAPIClient連携をさせて、一方でレスポンスデータについてはPresenter側で処理させるということが可能になります。
Jsonデータのデコード処理については、Swift4.1から標準機能として搭載されたCodableを使っています。
この登場によって、Struct内でいちいち、スネークケースからキャメルケースに変換する必要がなくなったのは熱いですね!
バリデーションは200番台のみ許可しています。
try catchについてもざっくりとは実装していますが、厳密じゃないのでご容赦ください。
RequestProtocol
RequestProtocolについては以下のようにしました。
import Alamofire
import Foundation
protocol RequestProtocol {
associatedtype Response: ResponseProtocol
var baseUrl: String { get }
var path: String { get }
var method: Alamofire.HTTPMethod { get }
var encoding: Alamofire.ParameterEncoding { get }
var parameters: Alamofire.Parameters? { get }
var headers: Alamofire.HTTPHeaders? { get }
}
extension RequestProtocol {
var baseUrl: String {
return Env.baseUrl
}
var encoding: Alamofire.ParameterEncoding {
return JSONEncoding.default
}
var parameters: Alamofire.Parameters? {
return nil
}
var headers: Alamofire.HTTPHeaders? {
return nil
}
}
Model側でのリクエスト用の構造体で、このprotocolを継承して使います。
struct HogeRequest: RequestProtocol { ~
こんな感じ。
associatedtype については ResponseProtocolと紐づけていますが、
可読性がよくなる?ぐらいのメリットしか感じていないです。
この辺り、詳しい方教えていただけましたら幸いです。
また何度も同じ値が入る箇所については、Protocolをextensionすることでデフォルト値を設定できるようにしました。
ドメインなど、文字列は環境変数用のファイルを別途作成し、構造体として一元管理しています。(ここではEnvと置いています。内容は割愛させていただきますが、static変数で静的に参照できるようにしています。)
encodingについてはJSONをよく使うのでJSONEncodingを一旦設定していますが、柔軟に置き換えていただければと思います。
parametesやheadersについても同様に必要があれば、連想配列で定義してください。
例えば、以下のように使えます。
var headers: Alamofire.HTTPHeaders? {
let headersArr = [
"deviceName": "iPhone8",
"osType": "iOS"
]
return headersArr
}
ResponseProtocol
ResponseProtocolについては以下のようにしました。
import Foundation
protocol ResponseProtocol: Decodable {}
といっても今回はデコーダブルの定義しかしていません。
RequestProtocol同様に、Response用のStructに継承させてあげてください。
ちょっとややこしいのですが、このStructが APIClientのsuccess時のクロージャ部分の T.Response と対応しています。
Decodableしかないので、そのままDecodableを継承してもらっていいような気もしますが、associatedtypeでRequestProtocolと連携させていますので、こっちの方が可読性が良いかと思います。
所感
ジェネリクスやプロトコル、エクステンションなど、Swiftの基本機能を一通り理解するのは色々面倒だったのでずっと避けてきましたが、ちゃんと活用してあげることですっきりとしたコードが書けるもんだなぁと身を以て実感できました。
また、個人的にディレクターやマーケターは目に見えて成長を感じづらいイメージがあり、一方でエンジニアはできなかったことができるようになる、わからなかったことがわかるようになるなど、成長を実感しやすい気がしました。
次回も気が向いたら書こうと思います。MVPの話とか考えてます。
ご意見とかいただけたら嬉しいです。