AlamofireでGenericにModelオブジェクトを取得する

2015年3月11日
ios / swift /

※この記事のコードはXcode 6.3 beta(Swift 1.2)で試しています

Swiftいいですね!

これまでSwiftの案件を2つほどやってきたのですが、どちらも開発スタートが2014年7月だったため新しめのSwiftライブラリもリスクが高そうで、利用できるライブラリはある程度限定されてしまいました。 例えば、Alamofire のInitial Commitも2014/7/31だったりと。。。

今となっては(2015年3月)Swift公開から早9ヶ月が経過しており、ライブラリの選択肢もだいぶ広がりました。 また、まだSwiftのライブラリを管理する環境もだいぶ整ってきました(ちょうど本日3/11にCocoaPodsのDynamic Framework対応版が公開されました!)。

ということで、3月からはじめる新案件ではAlamofireの採用を決め、APIアクセスまわりのインターフェースをいろいろと検討してみました。 やはりSwiftを使うからには、Genericsを使ってModelオブジェクトに変換された状態のレスポンスを受け取れるインターフェースになっていて欲しいですよね!

※基本的にはAlamofireのREADMEに書かれている話です

ふつうにJSONを取得するインターフェース

まず、普通にJSONを取得するインターフェースですが、

Alamofire.request(.GET, "https://api.github.com/users")
    .validate()
    .responseJSON { [unowned self] (_, _, JSON, error) in
        if let error = error {
            self.textView?.text = "\(error)"
        } else if let JSON = JSON {
            // ここでJSONをパースしてModelに変換する
            // これは擬似的なコードです
            if let dicts = JSON as? NSArray {
                var users = [User]()
                for dict = dicts {
                    if let user = User(dict: dict) {
                        users.append(user)
                    }
                }
                self.textView?.text = "\(users)"
            }
        }
    }

といったかんじでresponseJSONNSArray or NSDictionaryにシリアライズ後の情報をModelオブジェクトに変換して利用するというのがよくある使い方ではないかと思います。

Genericなインターフェース

ただ、せっかくSwiftを使っているので(正確にはObjective-Cでもこうしたいですが…)Modelオブジェクトへの変換までを裏(APIクライアント側)でやってしまいたいところです。

例えば、こんなかんじで。

Alamofire.request(.GET, "https://api.github.com/users")
    .validate()
    .responseCollection { [unowned self] (_, _, users: [User]?, error) in
        if let error = error {
            self.textView?.text = "\(error)"
        } else if let users = users {
            self.textView?.text = "\(users)"
        }
    }

あら素敵!

Objective-C時代にもAPIクライアントにParserを渡してModelオブジェクトの状態で返してもらうようなことはやっていましたが、ここまですっきりしたインターフェースでこれが実現できるのはSwiftならではですね!

Alamofireの拡張部分

(AlamofireのREADMEどおりですが)Alamofireを拡張してresponseCollectionというModelオブジェクトへの変換までやってくれる用のメソッドの実装はこんなかんじです。

新しく作るResponseCollectionSerializableというJSON->Modelオブジェクトのシリアライズ用のprotocolに対応したクラスであれば、全てこのインターフェースで取得できるようになります。Generics偉い!

import SwiftyJSON

extension Alamofire.Request {
    public func responseCollection<T: ResponseCollectionSerializable>(completionHandler: (NSURLRequest, NSHTTPURLResponse?, [T]?, NSError?) -> Void) -> Self {
        let serializer: Serializer = { (request, response, data) in
            if let response = response, data = data {
                let json = JSON(data: data)
                if let objects: [T] = T.collection(response: response, json: json) {
                    return (objects as? AnyObject, nil)
                }
            }
            return (nil, NSError()) //< TODO: 期待されないレスポンスだった場合のエラーをここで返す
        }

        return response(serializer: serializer, completionHandler: { (request, response, object, error) in
            completionHandler(request, response, object as? [T], error)
        })
    }
}

SerializerはAlamofireが具備しているシリアライズ部分のカスタマイズ用の型です。これをresponseメソッドに渡すことでシリアライズ部分を柔軟に拡張可能です。

Model側の実装

(これもほぼREADMEどおりですが)最後に、JSONからのシリアライズにModelを対応させるための実装(ResponseCollectionSerializable)です。

SwifthJSON を使ってます

import SwiftyJSON

public protocol ResponseCollectionSerializable {
    init?(json: JSON)
    static func collection<T: ResponseCollectionSerializable>(#response: NSHTTPURLResponse, json: JSON) -> [T]?
}

class Model: ResponseCollectionSerializable {
    var identifier: String!

    required init?(json: JSON) {
        if let identifier = json["id"].int {
            self.identifier = String(identifier)
        } else {
            return nil
        }
    }

    class func collection<T: ResponseCollectionSerializable>(#response: NSHTTPURLResponse, json: JSON) -> [T]? {
        var items = [T]()
        for (k, j) in json {
            if let item = T(json: j) {
                items.append(item)
            }
        }
        return items
    }
}

class User: Model {
    var name: String!

    required init?(json: JSON) {
        super.init(json: json)
        if let name = json["login"].string {
            self.name = name
        } else {
            return nil
        }
    }
}

このあと

SwiftTaskも使いつつ、このあたりの実装をAPIクライアントとしてまとめておきたい。

Related Entries
Latest Entries