Edited at

Alamofire + SwiftTask + ObjectMapperでAPIKitのようなAPIClientを実装する

More than 1 year has passed since last update.


はじめに

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クライアントを実装したいと思います。


準備


環境設定


  • 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クライアント

AlamofireSwiftTaskを組み合わせたAPIクライアントを実装します。

Alamofireは通信のprogressが取れたり、debugDescriptionで通信内容をログに出せたりするので便利です。


APIClient.swift

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に記述しておくと実装がスッキリします。

特殊な処理が必要になった場合はそれぞれのクラスで上書きしてしまえば良いです。


Request.swift

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を上書きします。


ItemsRequest.swift

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のモデルを作成します。

パラメータは適当に見繕って追加しています。


QiitaItem.swift

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]となります。


ViewController.swift

//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")
}

記事の一覧が取得できました!


終わりに

APIKitRequestTypeプロトコルに準拠したAPI定義をボコボコ追加してくだけでAPI周りの実装ができ、かつ非常にスッキリとしたコードが書けるため、とても優れた設計だと思っています。

今回はそれを参考にしてオレオレAPIクライアントを実装してみました。

アプリのビジネスロジックや特殊なAPI仕様、クライアントの無茶振りな独自仕様などによって、修正や拡張を強いられるパターンがありますが、この場合Request部分を拡張することで簡単に機能追加が可能になります。

APIClient部分をすこしいじればキャッシュ機構も手軽に組み込めます。

ソースコードは下記githubにあります。

PMApiClient - github


参考資料