DRYな備忘録

Don't Repeat Yourself.

AlamofireとSwiftyJSONとSwiftTaskで、genericなprotocolを受ける汎用的なHTTPClientをつくりたい

問題

  • AlamofireとSwiftTaskで汎用的にレスポンスをモデルにデコードするようなHTTP Clientをつくりたい
  • 特定のAPIエンドポイントのレスポンスをデコードするロジックは、各モデルにもたせたい

SwiftJSONを受けて"何らかの"モデルにデコードするメソッドを持つProtocolを作る

import SwiftyJSON

protocol JSONMappable{
    associatedtype T
    static func mapJSON(json: SwiftyJSON.JSON) -> T
}

associatedtypegenericsとして受け取れるprotocolなので、このprotocolをimplementするclassにおいてtypealiasTを指定すれば、その型を返すメソッドの実装でこのprotocolを満たせる。

このProtocolを満たすClassを作る

Model的なもの

import SwiftyJSON

class User: NSObject, JSONMappable {
    let name: String
    let age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
        super.init()
    }

    // MARK: - JSONMappableの実装
    typealias T = User // mapJSONの返り値としてUserを実装
    static func mapJSON(json: JSON) -> T {
        return User(
            json["name"].stringValue,
            json["age"].intValue
        )
    }
}

genericなprotocolを返り値に取れるHTTPClientを作る

import Alamofire
import SwiftTask
import SwiftyJSON

class HTTPClient: NSObject {
    static func request<T:JSONMappable>(
        method: Alamofire.Method,
        url: NSURL,
        params: [String: AnyObject]?,
        headers: [String:String] = ["Content-Type": "application/json"]
    ) -> Task<(Int64, Int64, Int64), T, NSError> {
        return Task<(Int64, Int64, Int64), T, NSError> { progress, fulfill, reject, configure in
            Alamofire.request(method, url, parameters: params, headers: headers, encoding: .JSON)
                .responseJSON { response -> Void in
                    switch response.result {
                    case .Success(let value):
                        let json = JSON(rawValue: value)
                        // TはJSONMappableなので、mapJSONメソッドを持っている
                        if mapped = T.mapJSON(json!) as? T {
                            fulfill(mapped)
                        } else {
                            reject(NSError(domain: "foo", code: 123, userInfo: nil))
                        }
                        return
                    case .Failure(let error):
                        reject(error)
                        return
                    }
                }
        }
    }
}

番外: APIエンドポイントとモデルを紐付けるクラスをつくる

本論では無いけど、HTTPClientを継承してエンドポイントとモデルの関連を固定するクラスをつくってみる

class UserAPI: HTTPClient {
    static func get(id: Int) -> Task<(Int64, Int64, Int64), User, NSError> {
        let url = NSURL(string: "http://localhost:8080/user/\(id)")!
        return request(.GET, url: url, params: [:])
    }
}

ゴール: ViewControllerから使う

UserAPI.get(1)
    .success { user -> Void in
        print("user取れた", user)
    }
    .failure { error, cancelled -> Void in
        print("失敗した", error)
    }

雑感

  • 最近JSばっかりだったので、型とエディタが強力なのすごい気持ちいい

f:id:otiai10:20160717214141p:plain

DRYな備忘録