炊きたてのご飯が食べたい

定時に帰れるっていいね。自宅勤務できるっていいね。子どもと炊きたてのご飯が食べられる。アクトインディでは積極的にエンジニアを募集中です。

Swift2 Alamofire + ObjectMapper で API クライアントを作成する


Swift2.2, Xcode7

サーバーと API 通信するアプリの開発に携わることになり、色々な記事を参考 API クライアントを作成しました。

APIクライアントを作る理由

API をコールする処理を一元管理することで、共通で行う処理がとてもスムーズに行えるようになります。

  1. json を ObjectMapper でオブジェクトに変換
  2. オブジェクトが Realm オブジェクトだったら、API のレスポンスを Realm に保存
  3. エラーコードが 503 だったらメンテナンスモードだと判別して Observer (通知)を実施

などなど。今回の記事では(1)を実装したサンプルを書きます。サンプルプロジェクトはこちらです。 Github Alamofire-ObjectMapperSample

APIクライアントの作成

マネーフォワードさんの Swift2.0で作るAPI通信基盤 を参考にさせていただきました。こちらの記事では Alamofire のバージョンが 2.0 なので、現在の 3.X 系で対応したコードを記述しておきます。マネーフォワードさんの記事と異なる点は、以下の 3 点です。

  • Result<V, NSError> と Alamofire 3.0 への変更点を反映。エラーは NSError で受け取るようにしました。
  • HTTP の statusCode が 200 番台を正常と判断するように .validate(statusCode: 200..<300) を追加
  • API のレスポンスは json しか利用しないので、レスポンスデータが json であることを .validate(contentType: ["application/json"]) で明示

API クライアントとなる API.swift

import Alamofire

/*
 使用例
 API.call(Endpoint.User.Find(id: 1)) { response in
    switch response {
    case .Success(let result):
        print("success \(result)")
    case .Failure(let error):
        print("failure \(error)")
    }
 }
 */

class API {
    class func call<T: RequestProtocol, V where T.ResponseType == V>(request: T, completion: (Result<V, NSError>) -> Void) {
        Alamofire.request(request)
            .validate(statusCode: 200..<300)
            .validate(contentType: ["application/json"])
            .responseJSON { response in
                switch response.result {
                case .Success(let json):
                    completion(request.fromJson(json))
                case .Failure(let error):
                    completion(.Failure(error))
                }
        }
    }
}

API のインターフェースを定義する RequestProtocol.swift

  • baseUrl や method 、 parameters など API で共通で使うプロパティを定義します
  • インターフェースだけでなく extension で実装も記述します
import Alamofire
import ObjectMapper

protocol RequestProtocol: URLRequestConvertible {
    associatedtype ResponseType
    
    var baseURL: String { get }
    var method: Alamofire.Method { get }
    var path: String { get }
    var headers: [String: String]? { get }
    var parameters: [String: AnyObject]? { get }
    var encoding: ParameterEncoding { get }
    
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError>
}

extension RequestProtocol {
    var method: Alamofire.Method {
        return .GET
    }
    
    var baseURL: String {
        return "https://httpbin.org"
    }
    
    var headers: [String: String]? {
        return nil
    }
    
    var parameters: [String: AnyObject]? {
        return nil
    }
    
    var encoding: ParameterEncoding {
        return .URL
    }
    
    var URLRequest: NSMutableURLRequest {
        let url = "\(baseURL)\(path)"
        let encodedUrl = url.stringByAddingPercentEncodingWithAllowedCharacters(
            NSCharacterSet.URLQueryAllowedCharacterSet())
        let mutableURLRequest = NSMutableURLRequest(URL: NSURL(string: encodedUrl!)!)
        mutableURLRequest.HTTPMethod = method.rawValue
        mutableURLRequest.allHTTPHeaderFields = headers
        do {
            if let parameters = parameters {
                mutableURLRequest.HTTPBody = try NSJSONSerialization.dataWithJSONObject(
                    parameters, options: NSJSONWritingOptions())
            }
        } catch {
            // No-op
        }
        mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        return mutableURLRequest
    }
    
    /*
     NSError について
     
     1) domain は 識別子。独自で設定する際は com.company.app が通常
     2) code: エラーコード
     3) UserInfoのDictionary: エラーの概要(NSLocalizedDescriptionKey)と復旧方法(NSLocalizedRecoverySuggestionErrorKey)
     */
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError> {
        guard let value = json as? ResponseType else {
            let errorInfo = [NSLocalizedDescriptionKey: "Convert object failed" , NSLocalizedRecoverySuggestionErrorKey: "Good luck!"]
            let error = NSError(domain: "com.example.app", code: 0, userInfo: errorInfo)
            return .Failure(error)
        }
        return .Success(value)
    }
}

extension RequestProtocol where ResponseType: Mappable {
    func fromJson(json: AnyObject) -> Result<ResponseType, NSError> {
        guard let value = Mapper<ResponseType>().map(json) else {
            let errorInfo = [ NSLocalizedDescriptionKey: "Mapping object failed" , NSLocalizedRecoverySuggestionErrorKey: "Rainy days never stay." ]
            let error = NSError(domain: "com.example.app", code: 0, userInfo: errorInfo)
            return .Failure(error)
        }
        return .Success(value)
    }
}

接続先やリクエストデータを定義する Endpoint.swift

  • API のレスポンスデータは様々なオブジェクトに変換する為 typealias を利用して複数のオブジェクトに対応しています。
  • CRUD を想定して enum による実装です
import Alamofire
import ObjectMapper

class Endpoint {
    enum User: RequestProtocol {
        typealias ResponseType = UserEntity
        
        case Get
        case Find(id: Int)
        case Delete
        
        var method: Alamofire.Method {
            switch self {
            case .Get, .Find:
                return .GET
            case .Delete:
                return .DELETE
            }
        }
        
        var path: String {
            switch self {
            case .Get:
                return "/get"
            case .Find(let id):
                return "/users/\(id)"
            case .Delete:
                return "/user/delete"
            }
        }
    }
}

レスポンスデータを定義する UserEntity.swift

import ObjectMapper

class UserEntity: Mappable {
    var id: Int?
    var name: String?
    var email: String?
    var url: String?
    
    required convenience init?(_ map: Map) {
        self.init()
    }
    
    func mapping(map: Map) {
        id <- map["id"]
        name <- map["name"]
        email <- map["email"]
        url <- map["url"]
    }
}

以上で API をコールして、結果をオブジェクトに変換する API クライアントのサンプルの出来上がりです。次回は Realm への保存やメンテナンスの出し方について書きたいと思います。

この記事では Alamofire の request メソッドのみの対応になるので、画像をアップロードする時に利用する upload メソッドの API クライアントを作成はこの記事を参考にしてください。 (Swift2) Alamofire の upload 用 API クライアントを作成する