背景
Alamofireパイセンはとても便利なHTTP通信ライブラリだけど、responseが文字列で返ってくるのでちょっと辛い。ジェネリックスを利用して既にオブジェクトになっている状態でコールバックを実行できるようにしたいし、それに対してメソッドチェーンでゴニョゴニョ出来るようにしたい。
Androidでいう所のRetrofit+RxJavaに近しいことが出来ればいいなぁ、と。
Alamofireのレスポンスをオブジェクトで受け取る
公式のreadmeにAdvancedなトピックとして書いてある。
Alamofire/Alamofire
公式を更に掘り下げて説明してくれているこのGistが、とてもわかりやすかった。英語です。
Alamofire JSON Serialization of Objects and Collections
メソッドチェーンはSwiftTaskで
こちらを参考にさせてもらった。
SwiftTask、PromiseKit、Boltsを比較する(2015年3月版)
SwiftTask(Promise拡張)を使う
json-schemaからSwiftのAPIクライアントを自動生成するツールを作った
SwiftTaskを使って、Alamofireでジェネリックなオブジェクトを取得すること自体をTaskに落とす。で、落としたTaskをあとはPromise的なメソッドチェーンでよしなにやるというシンプルなものを目指した。
書いたコード
正直な所、SwiftTaskに対する理解があやふやなので、本人はなんとなく出来た感があります。とりあえず、書いたものを全部晒しておきます。はい。
import Foundation
import Alamofire
import SwiftTask
@objc public protocol ResponseObjectSerializable {
init(response: NSHTTPURLResponse, representation: AnyObject)
}
extension Alamofire.Request {
public func responseObject<T: ResponseObjectSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, T?, NSError?) -> Void) -> Self {
let serializer: Serializer = { (request, response, data) in
let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
if response != nil && JSON != nil {
return (T(response: response!, representation: JSON!), nil)
} else {
return (nil, serializationError)
}
}
return response(serializer: serializer, completionHandler: { (request, response, object, error) in
completionHandler(request, response, object as? T, error)
})
}
}
@objc public protocol ResponseCollectionSerializable {
static func collection(#response: NSHTTPURLResponse, representation: AnyObject) -> [Self]
}
extension Alamofire.Request {
public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
let serializer: Serializer = { (request, response, data) in
let JSONSerializer = Request.JSONResponseSerializer(options: .AllowFragments)
let (JSON: AnyObject?, serializationError) = JSONSerializer(request, response, data)
if response != nil && JSON != nil {
return (T.collection(response: response!, representation: JSON!), nil)
} else {
return (nil, serializationError)
}
}
return response(serializer: serializer, completionHandler: { (request, response, object, error) in
completionHandler(request, response, object as? [T], error)
})
}
}
//適当にTask定義
typealias ApiTask = Task<Int,AnyObject,NSError>
class ApiService {
private func makeRequest<T: ResponseCollectionSerializable>(req:URLRequestConvertible) -> Task<ApiTask, [T], NSError> {
let task = Task<ApiTask, [T], NSError> { _,fulfill, reject,_ in
let alam = Alamofire.request(req)
alam.responseCollection { (request, response, collection: [T]?, error) in
if let error = error {
reject(error)
return
}
if let collection = collection {
fulfill(collection)
return
}
}
}
return task
}
private func makeRequest<T: ResponseObjectSerializable>(req:URLRequestConvertible) -> Task<ApiTask,T, NSError> {
let task = Task<ApiTask,T, NSError> { _,fulfill, reject,_ in
let alam = Alamofire.request(req)
alam.responseObject{ (request, response, data: T?, error) in
if let error = error {
reject(error)
return
}
if let data = data {
fulfill(data)
return
}
}
}
return task
}
func getCompany() -> Task<ApiTask,Corporation, NSError> {
return makeRequest(Router.Company)
}
enum Router: URLRequestConvertible {
static let baseURLString = "http://gothedistance.sakura.ne.jp"
case Search(query: String)
case Company
var URLRequest: NSURLRequest {
let (path: String, parameters: [String: AnyObject]?) = {
switch self {
case .Search(let query):
return ("/search", ["q": query])
case .Company():
return ("/company.json",nil)
}
}()
let URL = NSURL(string: Router.baseURLString)!
let URLRequest = NSURLRequest(URL: URL.URLByAppendingPathComponent(path))
let encoding = Alamofire.ParameterEncoding.URL
return encoding.encode(URLRequest, parameters: parameters).0
}
}
}
- extensionで定義しているのは、Alamofireのレスポンスをオブジェクトでゲットするためのコード。公式に書いてある内容そのまま。
- ApiServiceクラスはSwiftTaskを使って、Alamofireの通信部分をTaskとして管理しているもの。SwiftTaskはTaskクラスが引数を3つとる。公式では第1引数にprogressのtypealiasを作っていたが、めんどくなってIntにした。
- 第2引数はalamofireで取得するオブジェクト、第3引数はエラーのオブジェクト。のはず。
- AlamofireはURLRequestConvertibleを使って、自分好みのEnumを定義できる。method,parameter,encoding...などがスッキリまとめられるのが気持ちよかった。これも公式に書いてある内容、ほぼそのままです。
今回利用したJSONファイル
{
"name": "(有) エフケーコーポレーション",
"address": "東京都大田区仲六郷1-51-11 1F",
"zipcode": "144-0055",
"tel": "03-3733-6143",
"fax": "03-3737-5479",
"id": 100
}
JSONをマッピングする対象となるクラスのコード
final class Corporation: ResponseObjectSerializable {
let name: String
let zipcode: String
let address: String
let tel: String
let fax: String
let id: Int
@objc required init(response: NSHTTPURLResponse, representation: AnyObject) {
self.name = representation.valueForKeyPath("name") as! String
self.zipcode = representation.valueForKeyPath("zipcode") as! String
self.address = representation.valueForKeyPath("address") as! String
self.tel = representation.valueForKeyPath("tel") as! String
self.fax = representation.valueForKeyPath("fax") as! String
self.id = representation.valueForKeyPath("id") as! Int
}
}
ResponseObjectSerializableを継承して、initにあるようにチマチマとJSONのキーとマッピングすると、JSONをオブジェクトに変換してくれる。
使い方
ViewControllerでこんな感じで書きます。よく考えたらgetCompanyはclass funcにしたほうが良いかも。
override func viewDidLoad() {
super.viewDidLoad()
let api = ApiService()
api.getCompany().success {(company: Corporation) -> Void in
println(company.tel) //03-3733-6143
}.failure{(error: NSError?, isCancelled: Bool) -> Void in
println(error)
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
Podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '8.0'
use_frameworks!
target 'xxx' do
pod 'Alamofire', '~> 1.2'
pod 'ReactKit'
end
target 'xxxTests' do
end
嬉しい事
- コールバックをネストする必要がない。thenやsuccessつなげていくだけ。
- Alamofireで取得したJSONを型があるオブジェクトで取得できる。
今後の課題
- デフォルトの処理を差し込めるといいかも。
- success/failureのfailureの処理を共通化したい。
- ローディング表示なんかもTaskで拾えると尚よさげ。でもVCのインスタンスが必要?
ほんまにこれでええんかな? 何かの役に立てば幸いです。