こんにちは。iOSエンジニアの@h1d3mun3です。
皆さん、今年9月5日から9月7日まで、早稲田大学で開催された「iOSDC Japan 2019」にはご来場いただけましたでしょうか。
エンジニアが主役のお祭りとして、毎年多くの方にご来場いただいております。
iOSDC Japan で公式アプリを出させて頂く機会に恵まれました。
無事リリースすることもでき、Opt-In Onlyながら、会期期間中のクラッシュが0件を達成することができ、大変良かったと思っております。
アプリで一番難儀する場所といえば多分API通信周りなのではないでしょうか。
ここでは、iOSDC Japan 2019公式アプリで採用したAPI通信周りの知見を共有することにより、将来API通信周りの実装で悩む人の一助となることを期待しています。
1. はじめに - iOSDC Japan公式アプリのアーキテクチャ
iOSDC Japan公式アプリ (以下本アプリ)では、Kickstarter式のMVVMアーキテクチャを採用しています。
1つの画面を
- ViewController
- ViewModel (Inputs / Outputsで切り分け)
の2つのファイルで構成しています。
-
ViewModel
がOutputs
でObservable
を公開している -
ViewController
側でOutPuts
をsubscribe
する -
ViewController
がViewModel
のinputs
の処理を呼ぶ 、というイメージです。
いわゆる 「ビジネスロジック」はすべて ViewModel
側に持たせており、 ViewController
は表示を行うだけ、という状態です。
非同期の処理が主となるため、実装には RxSwift
を採用しています。
Kickstarter式のMVVMアーキテクチャについては下記記事によくまとまっております。
(僕もこれを読んで勉強いたしました)
Kickstarter-iOSのViewModelの作り方がウマかった
また、構造体の場合イニシャライザを書かなくても全項目イニシャライザが自動で用意されるので楽という理由で、クラスよりも構造体を多用する設計にしています。
2. API通信周りの実装
ここでは、タイムテーブルを取得する処理を例に上げ、本アプリがどのような仕組みで通信を行っているかを説明します。
2.1 HTTP通信を担当する構造体
本アプリでは下記のような構造体を宣言し、HTTP通信を担当させています。
GET
と POST
しかないのは単純にそれ以外が必要なかったからです。必要に応じて追加します。
import Foundation
import RxSwift
protocol HttpProtocol {
func get(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)>
func post(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)>
}
struct Http {
}
extension Http: HttpProtocol {
func get(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)> {
var urlComponents = URLComponents(string: url)!
urlComponents.queryItems = param?.map { URLQueryItem(name: $0, value: "\($1)") }
var request = URLRequest(url: urlComponents.url!)
request.timeoutInterval = TimeInterval(30)
request.httpMethod = "GET"
return URLSession.shared.rx
.response(request: request)
.asSingle()
}
func post(url: String, param: [String: Any]?) -> Single<(response: HTTPURLResponse, data: Data)> {
var request = URLRequest(url: URL(string: url)!)
request.timeoutInterval = TimeInterval(30)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
if let param = param {
request.httpBody = param.percentEscaped().data(using: .utf8)
}
return URLSession.shared.rx
.response(request: request)
.asSingle()
}
}
作りとしてはとても単純で、
-
HttpProtocol
により、振る舞いを制限 (これよりテスタブルになる) -
HttpProtocol
を適合した構造体をつくって実際の通信をそこに埋め込む
というだけです。
通信の結果は Single
で帰ってくるようになっており、非同期処理をするのに便利な作りにしてあります。
(Singleにするので必ず結果が帰ってくる or エラーするのどちらかにしかならないので便利)
2.2 実際のAPI通信コード
実際にタイムテーブルを取得してくる構造体は下記のようなコードになります。
具体的な APIエンドポイント
などは、APIConstants
に集約しております。
import Foundation
import RxSwift
struct TimetableRequestApi {
let http: HttpProtocol
func call() -> Single<TimetableAPIResponse> {
return http.get(url: APIConstants.timetable.url, param: nil)
.map { response -> TimetableAPIResponse in
if response.response.statusCode != 200 { throw DCError.internalServerError }
let decoder: JSONDecoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
do {
let response = try decoder.decode(TimetableAPIResponse.self, from: response.data)
return response
} catch {
throw DCError.jsonParseError
}
}
}
}
APIからのレスポンスのマッピングには Decodable
を利用しています。
このAPIでは、 TimetableAPIResponse
という Decodable
に準拠したモデルにデコードしています。
2.3 実際の利用ケース
実際の利用では下記のように利用します。
let request = TimetableRequestApi(http: Http())
request.call()
.subscribe(onSuccess: { [weak self] result in
guard let self = self else { return }
// DO SOMETHING WHAT YOU WANT
}, onError: { error in
// エラー処理とか
})
.disposed(by: disposeBag)
2.4 補足 - リクエストにidなどが必要な場合
リクエストに当たりパラメータが必要な場合(たとえばある特定のメンバーの情報が欲しいのでリクエストの際にメンバーIDが必要、などの場合)は、 RequestAPI
クラスに情報をもたせる作りにしてあります。
3. おわりに
API通信周りは基本的にどのアプリでも実装する(はず)のものなので、皆様苦心されるところだと思います。
僕も苦心しておりましたが、本アプリで実装した形が ひとまず 僕にとってスッキリする実装でしたのでQiitaにで共有させていただきました。
こういう形があるよ!とかあると思うので、皆さま是非是非参考にしてAPI周りをきれいに実装しちゃってくださいませ〜!