マネーフォワードさんとネタが被りましたね...
僕も今月中旬からAPI通信用クライアントの実装に取り組んでおりまして、こんな感じでええんちゃうかなぁという形ができたので、晒しておきます。
##利用ライブラリ
- Alamofire(通信)
- ObjectMapper(JSON⇔オブジェクト変換)
- SwiftTask(メソッドチェーン)
##API通信でよくあるやつ
- API通信の戻り値をオブジェクトにしたい。
- 成功と失敗のハンドラを自動で登録したい。
- コールバック地獄はだるいからメソッドチェーンで処理したいし、チェーンした時のクロージャで全く別の型を返却したい。
- ヘッダに認証用tokenなんかも入れたい。
こんなもんでしょ、きっと。
###通信⇔オブジェクト変換
Alamofireのrequestメソッドの拡張で、AlamofireObjectMapperというライブラリがあります。僕はこれを利用しています。全体で100行も無いので、見たほうが早いかな.. うっすいラッパーだから。
この中にresponseObjectとresponseArrayというメソッドが定義されており、responseJSONしたjsonを自動でオブジェクトにパースしてくれます。
###メソッドチェーンでの処理
SwiftTaskというライブラリを利用しています。
https://github.com/ReactKit/SwiftTask
Taskという単位で機能を作り、そのTaskで行っている内容が「実行中」「成功」「失敗」だった場合に得られる値の型を定義することでメソッドチェーンで処理することが出来ます。あるクロージャが成功したらその成功した時のデータを引き継いで別の違う型を返して処理を連結することも出来、コールバック地獄から開放されます。JavascriptのPromiseというライブラリに近いようです。
##実際に書いたコード
###モデル
モデル定義はObjectMapperの流儀に従い、Mappableプロトコルを実装するだけ。Realmに保存したい場合は、RealmObjectのサブクラスにして、Mappableを実装するだけです。ObjectMapperのgithubページに書いてあるのでご参照下さい。
import Foundation
import ObjectMapper
class Order: Mappable {
var id:Int = 0 //注文ID
var name:String = "" //顧客名
var ordersum:Int = 0 //注文総数
var subtotal:Int = 0 //合計
var bikou:String = "" //備考
required init(){}
required init?(_ map: Map){}
func mapping(map: Map) {
id <- map["id"]
name <- map["name"]
ordersum <- map["ordersum"]
subtotal <- map["subtotal"]
bikou <- map["bikou"]
}
}
###通信→後処理
SwiftTaskで通信用のTaskを作ります。これが9割です。findOrderメソッドの中でTaskを作っています。
host名やコンテキストパスなんかは適当に文字列で定義してしまいましたが、マネーフォワードさんのようにRequestProtocol的なものに抽象化するのがお作法かも。僕は小規模なアプリなので、これだけで充分だった。
import Alamofire
import SwiftTask
class ApiService {
static let hostname:String = "http://xxxx/"
enum Path : String {
case OrderSearch = "order_search"
}
class func makeUrl(path:Path) -> String { return hostname + path.rawValue }
class func findOrder(param:[String : AnyObject]) -> Task<Void,[Order]?,ErrorType> {
/**
* ここがTaskを作る部分です
* Taskクラスはinitメソッドで、処理中、成功、失敗時の処理に受けるデータ型を指定します。
* 処理中は何も受け取らないようにしたので、Voidにしました。怒られそう。
* 成功時にはOrderモデルの配列を受け取ります。空っぽの場合はnilです。
* 失敗時にはErrorTypeオブジェクトを受け取ります。空の場合はnilです。
*/
let task:Task<Void,[Order]?,ErrorType> = Task { _,fulfill,reject,configure in
Alamofire.request(Alamofire.Method.GET,
ApiService.makeUrl(.OrderSearch),
parameters: param)
.validate()
.responseArray { (response:[Order]?, error: ErrorType?) in
if let error = error {
reject(error)
return
}
fulfill(response)
}
}
return task
}
###APIを使うコード
let param:[String: AnyObject] = [
"name" : clientNameTxt.text!
]
ApiService.findOrder(param).success{ data in
if let data = data {
//UITableViewをrefresh!
self.order_data = data
self.tableView.reloadData()
} else {
//検索結果がなかった時
}
}.failure{ error,cancel in
//something is technically wrong...
}
###TokenをHTTPヘッダに常に入れたい
class func setApiToken(token:String) {
Alamofire.Manager.sharedInstance.session.configuration.HTTPAdditionalHeaders =
["Authorization": token]
}
AlamofireはSingletonパターンで常に同じインスタンスを返してくれますので、上記だけでリクエスト時のHTTPヘッダーに「常に」Authroizationヘッダがつけられます。
###UI処理の共通化でちょっと悩む
API呼び出す時ってローディングウインドウを出したり、レスポンス帰ってきたらそのウインドウを閉じたり、メッセージを表示したい場合はアラートなんかを上げたりしたいですよね。
さっきのfindOrderのメソッドでこんな感じで強引にアラートをあげようしたら、uiviewcontroller whose view is not in the window hierarchy
と怒られた。UIViewControllerのインスタンスだけを都合よく利用することは出来ません。
let alert = UIAlertController(title: "abc", message: "def", preferredStyle: .Alert)
UIViewController().presentViewController(alert, animated: true, completion: nil)
Apiを呼び出す時に参照しているUIViewControllerのインスタンスを渡せば出来るんですけど、皆さんこういう時ってどうされているんだろう。例外を投げてGlobal Exception Handler的なもので捕まえているのかな? それが出来れば最高なんだけど、もしGood Ideaがあればご教示頂けると嬉しいです。