Swift2 Alamofire + ObjectMapper で API クライアントを作成する
Swift2.2, Xcode7
サーバーと API 通信するアプリの開発に携わることになり、色々な記事を参考 API クライアントを作成しました。
APIクライアントを作る理由
API をコールする処理を一元管理することで、共通で行う処理がとてもスムーズに行えるようになります。
- json を ObjectMapper でオブジェクトに変換
- オブジェクトが Realm オブジェクトだったら、API のレスポンスを Realm に保存
- エラーコードが 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 クライアントを作成する
- リフレッシュトークンを利用したトークンの更新についての記事はこちら ( Swift2 ) Alamofire で有効期限付きの認証用トークンを利用する( refresh token を利用した token 更新)
- Alamofire でネットワーク通信の検知を行う方法はこちら Swift2 - Alamofire のリクエストでネットワークエラーを検知する