68
65

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-05-16

はじめに

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

screen1.png

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

終わりに

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

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

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

参考資料

68
65
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
65

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?