2019/8/13 続編を書きました
URLSession+Combine+CodableでAPIクライアントを作る
2020/1/28 続編を書きました
Alamofire+RxSwiftでUpload APIクライアントを作る
追記
Decode部分をprotocolに入れることでメソッドを減らしました。
(プロトコルなので、継承先で柔軟に上書けるようになった)
前置き
担当プロジェクトで、ようやく
Objective-c
+Swift3.2
→ Swift4.0
への書き換えも済み、それならCodableを使いたい!と思い行ってみた。
半年前に ObjectMapper+RxSwiftを実装した備忘録 書いたが、これをCodableに対応したものにした。
(基本的には上記内容と同じです。Decode部分が若干かわったくらい)
導入にあたって
ちょろちょろこういうの
はあったんですが、なんかあんまり綺麗じゃないと思ってしまい、、(ディスリじゃないですよ!)
あとMoya
のメリットが、実感できなかったのでAlamofire
でやりました。
導入
実際に、プロジェクトに導入する手順で行っていく。
通信にはAlamofire
を使用します。
1. ライブラリのインストール
ライブラリの導入はCarthage
を使用。
(Cocoapods
,SPM
など導入できるのであれば何でもおk)
github "Alamofire/Alamofire" ~> 4.1
github "ReactiveX/RxSwift" == 4.3.1
Linked Frameworks and Library
にもライブラリ追加を忘れずに。
2. ネットワークプロトコルの作成
この記事ではただコードを載せただけだったので、若干細かく説明していきたいと思います。
①定義ファイルの作成
APIで使用するURLやPathを一元管理するためにenumで定義し管理していきます。
(このやり方が好きでない人は、リクエストごとに直にpathを書いていってください)
今回は例としてsignIn
, signUp
というものを定義しています。
// MARK: - Constants
enum APIConstants {
case signIn
case signUp
case xxx
,
,
// MARK: Public Variables
public var path: String {
switch self {
case .signIn: return "user_sign_in"
case .signUp: return "user_sign_up"
case .xxx: return "xxx"
,
,
}
}
// MARK: Public Static Variables
public static var baseURL = "https://~~~"
public static var header: HTTPHeaders? {
// 必要ならば個々に設定してください
return [
"Accept-Encoding": ~~~,
"Accept-Language": ~~~,
"User-Agent": "~~~~"
]
}
}
②リクエストの基底プロトコルを作成
requestを作成するための雛形protocol
を作成します。
Alamofire
を使用するので、それに準拠するように作っていきます。
import Alamofire
// MARK: - Base API Protocol
protocol BaseAPIProtocol {
associatedtype ResponseType // レスポンスの型
var method: HTTPMethod { get } // get,post,delete などなど
var baseURL: URL { get } // APIの共通呼び出し元。 ex "https://~~~"
var path: String { get } // リクエストごとのパス
var headers: HTTPHeaders? { get } // ヘッダー情報
var decode: (Data) throws -> ResponseType { get } // デコードの仕方
}
extension BaseAPIProtocol {
var baseURL: URL { // 先ほど上で定義したもの。
// 絶対にあることがある保証されているので「try!」を使用している
return try! APIConstants.baseURL.asURL()
}
var headers: HTTPHeaders? { // 先ほど上で定義したもの。なければ「return nil」でok
return APIConstants.header
}
var decode: (Data) throws -> ResponseType { // decoderはあとで用意するので注意
return { try JSONDecoder.decoder.decode(ResponseType.self, from: $0) }
}
}
この基底プロトコルを元に
- データを取得する
- アップロードする
- ダウンロードする
など場合分けして、それぞれが基底プロトコルに準じたものを作成していくと、コード分割ができ綺麗に作っていくことができるかと思います。
今回はAPIを叩いてデータを取ってくるだけの想定で話を進めます。以下、普通の通信を行う場合のプロトコル。
Alamofire
に準ずるためURLRequestConvertible
を継承しています。
(リクエストするときにURLRequestConvertible
を継承したものを渡す必要があるので)
// MARK: - BaseRequestProtocol
protocol BaseRequestProtocol: BaseAPIProtocol, URLRequestConvertible {
var parameters: Parameters? { get }
var encoding: URLEncoding { get }
}
extension BaseRequestProtocol {
var encoding: URLEncoding {
// parameter の変換の仕方を設定
// defaultの場合、get→quertString、post→httpBodyとよしなに行ってくれる
return URLEncoding.default
}
func asURLRequest() throws -> URLRequest {
// requestごとの pathを設定
var urlRequest = URLRequest(url: baseURL.appendingPathComponent(path))
// requestごとの methodを設定(get/post/delete etc...)
urlRequest.httpMethod = method.rawValue
// headersを設定
urlRequest.allHTTPHeaderFields = headers
// timeout時間を設定
urlRequest.timeoutInterval = TimeInterval(30)
// requestごとの parameterを設定
if let params = parameters {
urlRequest = try encoding.encode(urlRequest, with: params)
}
return urlRequest
}
}
ここまではこの記事とほぼ同じです。
3.リクエスト・レスポンスの作成
基底プロトコルを作ったので、実際にそれに適応するリクエストとレスポンスを作成していきます。
例を用意。APIを叩くと以下のレスポンス(Json)が返ってくるとする。(あえて階層構造なものにしました)
{
"data": [
{
"title": "タイトル",
"screen_name": "トップ",
"id": "00000",
},
{
"title": "タイトル",
"screen_name": "トップ",
"id": "00000",
},
,
,
],
"result": true
}
①レスポンスの作成
上記のJsonを対応させていきたいと思います。
またCodable
に準拠させます。
import Alamofire
// MARK: - Response
struct xxxResponse: Codable {
var data: [xxxViewModel]?
var result: Bool = false
// 特に型変換などがない場合は、init省略可なので省略
}
// MARK: - Model
struct xxxViewModel: Codable {
let title: String?
let screenName: String?
let id: Int?
enum Keys: String, CodingKey {
case title
case screenName
case id
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Keys.self)
title = try container.decode(String.self, forKey: .title)
screenName = try container.decode(String.self, forKey: .screenName)
id = Int(try container.decode(String.self, forKey: .id))
}
}
②リクエストの作成
先ほど作成したBaseRequestProtocol
に準拠させます。
今回はgetの場合を例に。
import Alamofire
// MARK: - Request
enum xxxRequest: BaseRequestProtocol {
typealias ResponseType = xxxResponse
case get
var method: HTTPMethod {
switch self {
case .get: return .get
}
}
var path: String { // 先ほど定enumで定義したもの
return APIConstants.xxx.path
}
var parameters: Parameters? { // サーバーになにか送る必要がある場合は、こちらでセットする
return nil
}
}
ここまでで、下準備は完成です。
4.ネットワークマネージャーを作成
①Resultタイプの作成
通信成功時はCodable
に準拠したものが返ってくるように設定します。
// MARK: - ResultType
enum APIResult {
case success(Codable)
case failure(Error)
}
Swift4.1
からCodable
のキーに関して、 SnakeCace ⇆ CamelCaceの変換がデフォルトでやりやすくなったので、Extensionで拡張してデフォルトで付与しています。
また、それ用のデコーダーを先に定義しておきます。
(使用先でハードコードしていただいても問題ないです。個人的には記述が長くなるのが好きではないだけなので)
// MARK: - JSONDecoder Extension
extension JSONDecoder {
convenience init(type: JSONDecoder.KeyDecodingStrategy) {
self.init()
self.keyDecodingStrategy = type
}
static let decoder: JSONDecoder = JSONDecoder(type: .convertFromSnakeCase)
}
②通信部分の作成
強制キャストしているところがありますが、それは必ずなることが担保されているので行なっています。
(自分は基本的には強制キャストしません。ただ不必要なnilチェックでコードが汚くなるのは嫌いなので)
import UIKit
import Alamofire
// MARK: - API Cliant
struct APICliant {
// MARK: Private Static Variables
private static let successRange = 200..<400
private static let contentType = ["application/json"]
// MARK: Static Methods
// 実際に呼び出すのはこれだけ。(rxを隠蔽化しているだけなので、observeでも大丈夫)
static func call<T, V>(_ request: T, _ disposeBag: DisposeBag, onSuccess: @escaping (V) -> Void, onError: @escaping (Error) -> Void)
where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
_ = observe(request)
.observeOn(MainScheduler.instance) // mainを指定してしまっているので消しても良い
.subscribe(onSuccess: { onSuccess($0) },
onError: { onError($0) })
.disposed(by: disposeBag)
}
// RxSwiftを導入している部分。成功/失敗いずれかしか返らないSingleにしてある。
static func observe<T, V>(_ request: T) -> Single<V>
where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
return Single<V>.create { observer in
let calling = callForData(request) { response in
switch response {
//※ 既にsuccessしているので「as! V」で強制キャストしている(できる)
case .success(let result): observer(.success(result as! V))
case .failure(let error): observer(.error(error))
}
}
return Disposables.create() { calling.cancel() }
}
}
// Alamofire呼び出し部分
private static func callForData<T, V>(_ request: T, completion: @escaping (APIResult) -> Void) -> DataRequest
where T: BaseRequestProtocol, V: Codable, T.ResponseType == V {
return request(request).responseJSON { response in
switch response.result.flatMap(request.decode) {
case .success(let data): completion(.success(data))
case .failure(let error): completion(.failure(error))
}
}
}
// Alamofireのメソッドのみ切り出した部分
private static func request<T>(_ request: T) -> DataRequest
where T: BaseRequestProtocol {
return Alamofire
.request(request)
.validate(statusCode: successRange)
.validate(contentType: contentType)
}
}
ちょっといきなりかなりの量だったかもしれませんが、、、笑
使用する時はかなり綺麗にかけます。
##使用例
let disposeBag = DisposeBag()
APICliant.call(xxxRequest.get, disposeBag, onSuccess: {
// do some thing
}, onError: {
// error handling
})
たったのこれだけです!
自前でdo
, map
でレスポンスを加工したり、observeOn
などでQueueを変えたい場合は、observe
の方を呼び出すと良いでしょう。
let disposeBag = DisposeBag()
APICliant.observe(request)
.observeOn(MainScheduler.instance)
.subscribe(onSuccess: { response in
print("onSuccess")
}, onError: { error in
print("onError")
})
.disposed(by: disposeBag)
複数APIの呼び出し
Observable
に変換してcombineLatest
でくっつけることで実現可能です。
以下に例を用意しておきます。(型は存在しないものを使用しているので、各自のものにしてください)
let disposeBag = DisposeBag()
let relay = PublishRelay<Void>()
func call(onNext: @escaping (XXXYYY) -> Void) {
// API 1つ目
let xxxObservable: Observable<XXX> = relay.flatMapLatest { [weak self] _ in
let request = XXXRequest()
return APICliant.observe(request)
}
// API 2つ目
let yyyObservable: Observable<YYY> = relay.flatMapLatest { [weak self] _ in
let request = YYYRequest()
return APICliant.observe(request)
}
Observable
.combineLatest(xxxObservable, yyyObservable) {
// 2つのデータを加工する処理('XXXYYY'の型になるように変換)
}
.subscribe(onNext: onNext)
.disposed(by: disposeBag)
relay.accept(())
}
リクエストに引数が必要な場合は、callする関数なりに追加して渡してください。
(また、今回は成功時の処理しかやっていませんが、必要に応じてエラー処理も各自追加してください)
あとがき
個人的にはまだObject Mapper
の方が便利かなぁと思ってしまいます笑
だいぶCodableが流行ってきており、使い勝手もよくなってきたような気がしています。
型変換が不便なのと、Codable準拠のレスポンス変数に対してdidSetが使えないのが個人的には辛いですね。
(→レスポンス受け取った瞬間にdidSetで何かすることができなくなった(didSetが呼ばれない)ので)
PS
プロジェクトのクラス名をまんま使用するわけにいかず、一部リネームして表記しているので動かなかったらすいません、、、
というより、誤字脱字あればお願いいたします笑