はじめに
swiftで優れたAPIクライアントの1つにAPIKitがあります。
APIKitはタイプセーフでswiftライクな非常に扱いやすいライブラリです。
今回、開発しているアプリで通信周りの設計をいろいろと考えていたところAPIKitに出会い、感銘を受けました。
ざっくりと使用感を下記にまとめてみました。(調査不足な点があるかもしれません。)
APIKitの優れている点
- タイプセーフであること
- リクエストの戻り値がパース済みのモデルとして渡せること
-
RequestType
プロトコルに準拠することでAPIの動作が確定できること(場合によるが、独自実装やプロパティを保持せず、API情報だけ定義されてるため、それを見るだけでAPIの仕様がある程度把握できる) <-ここが一番好きなポイント
- ジェネリクスにより、リクエストのインターフェースが1つに収まってること
APIKitのいまひとつな点
- 通信内容のデバッグが難しい(リクエストヘッダーなどをprintしたくても、
Session
が通信を握っているのでブラックボックス化している。NSURLSessionでHookすればできるかも) - 外部ライブラリという都合上、拡張に多少抵抗を感じてしまう(extensionを使えばカバーはできるが、個人的感覚として)
- 画像などのJSONフォーマット以外の用途だとお手軽感は薄いかも(そういったのでは通信ライブラリ単体でカバーしたい)
かゆいところにはあらかた手が届いてしまうAPIKitですが、アプリやAPIの仕様によっては辛い状況に落ちいってしまうことがありました。
そこで、今回はAPIKitのインターフェースを尊重しつつ、Alamofire, SwiftTask, ObjectMapperを組み合わせてオレオレAPIクライアントを実装したいと思います。
- Alamofire - 通信ライブラリ
- SwiftTask - Promiseライブラリ
- ObjectMapper - ORマッパー
準備
環境設定
- XCode7.2 (swift 2.1)
- CocoaPods (1.0.0.beta.1)
ライブラリ(CocoaPods)
platform :ios, '8.0'
use_frameworks!
target 'PMApiClient' do
pod 'ObjectMapper', '~> 1.1.5'
pod 'ReactKit', '~> 0.12.0'
pod 'Alamofire', '~> 3.2.1'
end
今回はswift2.1での開発のため、各種ライブラリはswift2.1に対応したバージョンを使用します。
サンプルAPI
サンプルAPIとしてQiitaのitemsAPIを使ってみます。
itemsAPIは記事の一覧を新しい順で返してくれるAPIです。レスポンスは下記。
[
{
"rendered_body": "<h1>Example</h1>",
"body": "# Example",
"coediting": false,
"created_at": "2000-01-01T00:00:00+00:00",
"id": "4bd431809afb1bb99e4f",
"private": false,
"tags": [
{
"name": "Ruby",
"versions": [
"0.0.1"
]
}
],
"title": "Example title",
"updated_at": "2000-01-01T00:00:00+00:00",
"url": "https://qiita.com/yaotti/items/4bd431809afb1bb99e4f",
"user": {
"description": "Hello, world.",
"facebook_id": "yaotti",
"followees_count": 100,
"followers_count": 200,
"github_login_name": "yaotti",
"id": "yaotti",
"items_count": 300,
"linkedin_id": "yaotti",
"location": "Tokyo, Japan",
"name": "Hiroshige Umino",
"organization": "Increments Inc",
"permanent_id": 1,
"profile_image_url": "https://si0.twimg.com/profile_images/2309761038/1ijg13pfs0dg84sk2y0h_normal.jpeg",
"twitter_screen_name": "yaotti",
"website_url": "http://yaotti.hatenablog.com"
}
}
]
実装
APIクライアント
Alamofire
とSwiftTask
を組み合わせたAPIクライアントを実装します。
Alamofire
は通信のprogressが取れたり、debugDescriptionで通信内容をログに出せたりするので便利です。
import Alamofire
import SwiftTask
public enum APIError : ErrorType {
case ConnectionError(NSError)
case InvalidResponse(AnyObject?)
case ParseError(AnyObject?)
}
struct APIClient {
static func request<T : Request>(request : T) -> Task<Float, T.Response, APIError> {
let endPoint = request.baseURL.absoluteString+request.path
let params = request.parameters
let headers = request.HTTPHeaderFields
let method = Alamofire.Method(rawValue: request.method.rawValue) ?? .GET
let encoding = (method == .GET) ? ParameterEncoding.URL : ParameterEncoding.JSON
let task = Task<Float, T.Response, APIError> { progress, fulfill, reject, configure in
let req = Alamofire.request(method, endPoint, parameters: params, encoding: encoding, headers: headers)
.validate(statusCode: 200..<300)
.progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in
if totalBytesExpectedToWrite != 0 {
let prog = Float(totalBytesWritten / totalBytesExpectedToWrite)
progress(prog)
}
}
.responseJSON(completionHandler: { response in
if let error = response.result.error {
reject(.ConnectionError(error))
return
}
if let object = response.result.value, URLResponse = response.response {
guard let model = request.responseFromObject(object, URLResponse: URLResponse) else {
reject(.ParseError(object))
return
}
fulfill(model)
}else {
reject(.InvalidResponse(nil))
}
})
print(req)
print(req.debugDescription)
}
return task
}
}
Request
APIKitのRequestType
のようなプロトコルを作成します。Request
と命名しました。
ここはリクエストのベースとなる部分で、それぞれのAPIリクエストの定義はこのRequest
プロトコルを継承して実装していきます。
処理が共通している部分はextensionに記述しておくと実装がスッキリします。
特殊な処理が必要になった場合はそれぞれのクラスで上書きしてしまえば良いです。
import ObjectMapper
public enum Method: String {
case OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE, TRACE, CONNECT
}
protocol Request {
typealias Response
var baseURL : NSURL { get }
var method : Method { get }
var path : String { get }
var parameters : [String : AnyObject] { get }
var HTTPHeaderFields : [String : String] { get }
func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response?
func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType?
}
extension Request {
var baseURL : NSURL {
return NSURL(string: "http://qiita.com/api/v2")!
}
var HTTPHeaderFields : [String : String] {
return [:]
}
func errorFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> ErrorType? {
return nil
}
}
extension Request where Response:Mappable {
func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
print(self.decode(object.description))
guard let model = Mapper<Response>().map(object) else{
return nil
}
return model
}
}
次にitemsAPIの定義をしていきます。
structにしておくとイニシャライザーが自動生成されるので便利です。
このAPIはレスポンスがArrayなのでresponseFromObject
を上書きします。
import UIKit
import ObjectMapper
struct ItemsRequest : Request {
//MARK: Params
var page : Int = 1
var perPage : Int = 10
//MARK: Protocol
typealias ResponseComponent = QiitaItem
typealias Response = [ResponseComponent]
var method : Method {
return .GET
}
var path : String {
return "/items"
}
var parameters : [String : AnyObject] {
return [
"page" : self.page,
"per_page" : self.perPage
]
}
func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
guard let model = Mapper<ResponseComponent>().mapArray(object) else{
return nil
}
return model
}
}
Model
itemのモデルを作成します。
パラメータは適当に見繕って追加しています。
import ObjectMapper
struct QiitaItem : Mappable {
var id : String = ""
var title : String = ""
var url : String = ""
var user : QiitaUser?
var createdAt : String = ""
var updatedAt : String = ""
init?(_ map: Map) { }
mutating func mapping(map: Map) {
id <- map["id"]
title <- map["title"]
url <- map["url"]
user <- map["user"]
createdAt <- map["created_at"]
updatedAt <- map["updated_at"]
}
}
struct QiitaUser: Mappable {
var id : String = ""
var name : String = ""
var organization : String = ""
var profileImageUrl : String = ""
var description : String = ""
var followeesCount : Int = 0
var followersCount : Int = 0
init?(_ map: Map) { }
mutating func mapping(map: Map) {
id <- map["id"]
name <- map["name"]
organization <- map["organization"]
profileImageUrl <- map["profile_image_url"]
description <- map["description"]
followeesCount <- map["followees_count"]
followersCount <- map["followers_count"]
}
}
動作
Request
プロトコルにに準拠したオブジェクトをrequest内に入れると、resultはRequest.Response
となります。
この場合、resultの型はItemsRequest
で定義した[QiitaItem]
となります。
//MARK: - Private method
private func requestApi() {
let request = ItemsRequest(page: 1, perPage: 10)
APIClient.request(request).success{ result in
print(result)
self.items = result
self.tableView.reloadData()
}.failure{ error in
print(error)
}
}
}
successやfailureなどそれぞれの戻り値はTask
クラスなので、メソッドチェーンで処理を続けて書くことができます。
クロージャー地獄にならない点がSwiftTask
の良い点です。
下記のように続けて書くことができます。
APIClient.request(request).progress{ progress in
print(progress)
}.success{ result in
print(result)
self.items = result
self.tableView.reloadData()
}.failure{ error in
print(error)
}.then { _ in
print("done")
}
記事の一覧が取得できました!
終わりに
APIKit
はRequestType
プロトコルに準拠したAPI定義をボコボコ追加してくだけでAPI周りの実装ができ、かつ非常にスッキリとしたコードが書けるため、とても優れた設計だと思っています。
今回はそれを参考にしてオレオレAPIクライアントを実装してみました。
アプリのビジネスロジックや特殊なAPI仕様、クライアントの無茶振りな独自仕様などによって、修正や拡張を強いられるパターンがありますが、この場合Request
部分を拡張することで簡単に機能追加が可能になります。
APIClient
部分をすこしいじればキャッシュ機構も手軽に組み込めます。
ソースコードは下記githubにあります。
PMApiClient - github