はじめに
以前[Swift] RxSwift + Alamofire + ObjectMapper + Realmの基本的な使い方と言う記事を書いたのですが、一年経った今ではそのまま使えない上に理解に時間がかかりました。
なので、再度API通信をする方法について考えてみました。
以前の方法ではRouter
部分と呼び出される処理の部分が離れているため、APIを追加するために3箇所にコードを書く必要がありました。
これにモデルを入れると以下の4箇所になってしまいます。
- Router部分
- API部分
- API呼び出し部分
- モデル
これをもう少し端折りたくて、今回は一部の整合性チェックを捨てて簡易的に実装できるものを作ってみました。
この方法が良いかどうかは・・・わかりません。
使っているライブラリー
使用しているライブラリーは以下です。
github "ReactiveX/RxSwift"
github "Alamofire/Alamofire" ~> 4.4
github "Hearst-DD/ObjectMapper" ~> 2.2
github "tristanhimmelman/AlamofireObjectMapper" ~> 4.0
僕はcarthage
で入れましたがCocoaPods
で入れても問題ありません。
Realm
とも組み合わせれますが、このコードを汎用的に使いたかったのでRealm
への保存は業務コードに委託してます。(すなわち、この記事では取り扱ってないです。)
コード
はじめにコードと使い方を書きます。
一部変更が必要(後述)ですが、コピペすれば動きます。
import UIKit
import Alamofire
import RxSwift
import ObjectMapper
import AlamofireObjectMapper
// Routerを実装してAPIを作成ます。
public protocol Router {
// URLを返却
func url() -> String
// メソッドを返却
func method() -> Alamofire.HTTPMethod
// 実際のデータ取得処理
func request<T: Mappable>() -> Observable<[T]>
func request<T: Mappable>() -> Observable<T>
func request<T: Mappable>(parameters: Parameters?) -> Observable<[T]>
func request<T: Mappable>(parameters: Parameters?) -> Observable<T>
// エラー時の処理
func errorHandle(error: Error, statusCode: Int?)
}
// Routerの共通処理をあらかじめ定義しておく。
extension Router {
// Defaultはget処理
func method() -> Alamofire.HTTPMethod { return .get }
// 実際のリクエスト発行
func request<T: Mappable>() -> Observable<[T]> {
return self.request(parameters: nil)
}
func request<T: Mappable>(parameters: Parameters? = nil) -> Observable<[T]> {
return Observable.create { (observable: AnyObserver<[T]>) in
// リクエストを投げる。
API.createRequest(router: self, parameters: parameters).responseArray { (response: DataResponse<[T]>) in
switch response.result {
case .success(let value):
observable.on(.next(value))
observable.onCompleted()
case .failure(let error):
self.errorHandle(error: error, statusCode: response.response?.statusCode)
}
}
return Disposables.create()
}
}
func request<T: Mappable>() -> Observable<T> {
return self.request(parameters: nil)
}
func request<T: Mappable>(parameters: Parameters? = nil) -> Observable<T> {
return Observable.create { (observable: AnyObserver<T>) in
// リクエストを投げる。
API.createRequest(router: self, parameters: parameters).responseObject { (response: DataResponse<T>) in
switch response.result {
case .success(let value):
observable.on(.next(value))
observable.onCompleted()
case .failure(let error):
observable.onError(error)
self.errorHandle(error: error, statusCode: response.response?.statusCode)
}
}
return Disposables.create()
}
}
// エラー処理
func errorHandle(error: Error, statusCode: Int?) {
if statusCode == 401 {
// 共通エラー処理をここに記載します。
}
}
}
// API処理
class API {
// ホスト名
private static let Host = "http://localhost:3000/api/v1"
// 共通ヘッダー
static let CommonHeaders:HTTPHeaders = [
"Authorization": "Test",
"Version": Bundle.main.infoDictionary!["CFBundleShortVersionString"]! as! String,
"Accept": "application/json"
]
// リクエスト処理の生成
fileprivate class func createRequest(router:Router, parameters: Parameters? = nil) -> Alamofire.DataRequest {
return Alamofire.request("\(Host)\(router.url())",
method:router.method(),
parameters: parameters,
encoding: JSONEncoding.default,
headers: API.CommonHeaders).validate()
}
}
使い方
モデル
使用するモデルを作ります。
定番ですがUser
モデルとします。
import Foundation
import ObjectMapper
class User: Mappable {
var id = 0
var name = ""
required init?(map: Map) {}
// Mappable
func mapping(map: Map) {
id <- map["id"]
name <- map["name"]
}
}
ObjectMapper
を使うことで、返却されるJson
をマッピングさせます。
想定されるJson
は以下の通りです。
{
"id": 1,
"name": "hoge"
},
{
"id": 2,
"name": "foo"
}
※keyPathについては未考慮(後述)
ルーティング
ルーティング用のクラスは以下の通りです。
extension API {
enum Users: Router {
// データ一覧取得
case list
// データ一件取得
case get(Int)
// ルーティングはここで行います。
func url() -> String {
switch self {
case .list:
return "/users"
case .get(let id):
return "/users/\(id)"
}
}
// methodも上記同様に定義できます。未定義の場合はgetが使われます。
}
}
今回のケースではhttp://localhost:3000/api/v1
を共通URLとして定義しているので、以下のURLにアクセスされます。
呼び出し方
実際に呼び出す時には以下のように呼び出します。
class UsersViewController: UIViewController {
// APIの解放で使います。わからない人はこのまま実装すればOKです。
let disposeBag = DisposeBag()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// 一覧取得
// パラメータをつけたい場合はparametersに[String:String]で定義。なくても動きます。
API.Users.list.request(parameters: nil).subscribe(onNext: { (users:[User]) in
for user in users {
DebugLog("id = \(user.id)")
}
}).addDisposableTo(disposeBag)
// 一件のデータ取得
API.Users.get(1).request().subscribe(onNext: { (user:User) in
DebugLog("id = \(user.id)")
}).addDisposableTo(disposeBag)
}
}
解説
肝となっているのはAPI
クラスです。
こちらのクラスでAlamofire
のリクエストを生成してます。
共通ヘッダーなどを定義する場合はこのクラスに行います。
Router
プロトコルはやや強引に作っています。
冒頭に書いた整合性が取れない原因がT: Mappable
の部分になります。
本来ならUser
オブジェクトで受け取る必要がある箇所で、Company
を定義してしまっても落ちることなく処理が進んでいきます。
API.Users.get(1).request().subscribe(onNext: { (user:Company) in
DebugLog("id = \(user.id)")
}).addDisposableTo(disposeBag)
上記のようにクラスを誤ってもコンパイルエラーは発生しません。
Router
を隠蔽して、別のクラスを作成すればUser
意外は許可しないと言うこともできるのですが、今回はこの点を見送りました。
そして、共通処理を全てextension Router
に委ねています。
もしkeyPath
を設定して
"users": [
{
"id": 1,
"name": "hoge"
},
{
"id": 2,
"name": "foo"
}
]
このようなデータを取得した場合はこちらの処理を変更します。
API.createRequest(router: self, parameters: parameters)
.responseArray { (keyPath: "users", response: DataResponse<[T]>)
あとはRxSwift
を使って呼び出し側へ処理を返します。
return Observable.create { (observable: AnyObserver<T>) in
...
return Disposables.create()
}
※NopDisposableが無くなっていて焦った・・・。
最後に
このコードはまだ出来たばかりで実戦投入はしていません。
なので、いろいろ欠点があるかもしれませんが基本的な考え方はきっとあっているはずです(苦笑
1年前に作った時と色々変わっていたので、この記事も1年後に使えるかどうかは微妙です。
ただ、前の記事もまったく参考にならなかったわけではなく前の記事を参考にしたから1日で実装出来たわけなので来年になっても多少は使えるだろうなぁと思ってます。
本当はAlamofireObjectMapper
は使わないで実装しようと思ったのですが、うまくAlamofire
からObjectMapper
に落とせなかったので使ってしまいました。(ループ処理を書きたくなかった・・・。)
それに使ってみるとこっちの方が便利だなーと思ったので結果OKと言う結論に至りました。
僕の調べ方が悪いのか、Swiftの日本語文献はあまりないので誰か(1年後の僕)の役に立てればいいな。
では。