LoginSignup
40
40

More than 5 years have passed since last update.

AlamofireとSwiftTaskでジェネリックなHTTPクライアントを作りたい

Posted at

背景

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に対する理解があやふやなので、本人はなんとなく出来た感があります。とりあえず、書いたものを全部晒しておきます。はい。

ApiService.swift
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ファイル

company.json
{
  "name": "(有) エフケーコーポレーション",
  "address": "東京都大田区仲六郷1-51-11 1F",
  "zipcode": "144-0055",
  "tel": "03-3733-6143",
  "fax": "03-3737-5479",
  "id": 100
}

JSONをマッピングする対象となるクラスのコード

Model.swift
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にしたほうが良いかも。

ViewController.swift

    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

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のインスタンスが必要?

ほんまにこれでええんかな? 何かの役に立てば幸いです。

40
40
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
40