LoginSignup
211
212

More than 5 years have passed since last update.

Swiftを使ってモダンなWeb APIクライアントを爆速で開発する

Last updated at Posted at 2015-12-12

iOS Advent Calendarの13日目を担当します@giginetです。

APIクライアントを作りたいなあと言う気概になったので、APIクライアントをライブラリ化するまでの方法をご紹介します。

なお、この記事は執筆時点の最新の環境で検証しています。

  • Xcode7.2
  • Swift 2.1.1
  • Carthage 0.11.0

今回使用するAPI

今回は、APIクライアントが見当たらなかったので、WakaTimeという、エディタからデータを送り、自分のプログラミングについてのデータを集積してくれるサービスのAPIクライアントを作って、自分の1週間のコーディングを管理できるようにしてみました。

Screen Shot 2015-12-13 at 01.48.00.png

完全自動で、自分のプログラミング「作業ログ」を収集して可視化する「WakaTime」が素晴らしい件! | シェアしたくなる最新のWebサービス・ITニュース情報をチェック! APPGIGA!!(アプギガ)

もちろん、題材とするWeb APIはご自分の好きなサービスのものを使うことができます。
練習用であれば、GitHubのAPIなどがオススメです。

APIドキュメントは以下にありますので、こちらを参考にクライアントを作っていきます。

WakaTime API v1

iOSのフレームワークとプロジェクトのひな形を作る

フレームワークのプロジェクトを作る

まずAPIクライアント用のプロジェクトを作成します。
Xcodeの「File > New > Project」から、「iOS > Framework & Library > Cocoa Touch Framework」を選択します。
今回は「 WakaTimeAPIClient 」と名付けます。
その後、外部からフレームワークとして読み込むために、WakaTimeAPIClient.hというヘッダファイルを作成します。中身は空で構いません。

デモアプリプロジェクトを作る

その後、デモ用のアプリケーションを作成します。
同様に「iOS > Application > Single View Application」からアプリケーションを作成します。今回は「 DemoApplication 」というプロジェクトにします。

ワークスペースを作る

最後に、これらのプロジェクトをまとめるためにワークスペースを作りましょう。
「File > New > Workspace」からWakaTimeAPIClient.xcworkspaceを作成します。
その後、ワークスペースを開き、左側のペインに今まで作成したプロジェクトを追加します。

Screen Shot 2015-12-13 at 01.42.16.png

DemoApplicationの適当な場所からimport WakaTimeAPIClientなどを試してみて、問題なくビルドできれば、APIクライアントのひな形の完成です。

以後、このワークスペースを使って実装を進めていきます。

APIKitとObjectMapper

今回は、簡単にエンドポイントごとのAPIリクエストを作成できるお便利ライブラリの APIKitと、JSONとオブジェクトを簡単に相互変換できる ObjectMapperを使ってAPIクライアントを作ります。

ishkawa/APIKit
Hearst-DD/ObjectMapper

Carthageを使ってパッケージを導入する

これらのライブラリを利用するために、 Carthageと呼ばれるSwift製のパッケージ管理ツールを使います。

CartageはHomebrewから導入することができます。

$ brew install carthage

まず、先ほど作成したWorkspaceと同じディレクトリにCartfileを作成します。
ここでは以下のように今回使うライブラリを指定します。

Carftile
github "ishkawa/APIKit" ~> 1.0.0
github "Hearst-DD/ObjectMapper" ~> 1.0.1

その後、以下のコマンドを実行して、ライブラリを導入します。

$ carthage update --use-submodules

このコマンドを実行すると、Carthage/Build, Carthage/Checkouts以下にそれぞれ、ビルド済みのライブラリと、それぞれのプロジェクトのリポジトリが作成されます。

--use-submodulesオプションは、Carthage/Checkouts以下をgitのサブモジュールとして読み込むことができるオプションです。git管理をしている場合はライブラリの管理が簡単になるので、このオプションをつけることをオススメします。

先ほど作成したワークスペースにCarthage/Checkouts以下のxcodeprojを追加しましょう。

Screen Shot 2015-12-13 at 00.07.57.png

その後、WakaTimeAPIClientのターゲットを開き、「General > Linked Frameworks and Libraries」に依存するライブラリを追加します。

今回は以下の3つの.frameworkを追加します。これであなたのAPIクライアントからAPIKitとObjectMapperが利用できるようになりました。

Screen Shot 2015-12-13 at 01.42.58.png

モデルを定義する

ここから実際にコードを書いてみましょう。

次に、ObjectMapperを使ってモデルを定義します。

WakaTimeのAPIでは、StatsというAPIを叩くと、自分が1週間に書いた言語の秒数などを取得することができます。

レスポンスは以下のようになっています。

{
    "data": {
        "languages": [{
            "created_at": "2015-12-12T15:04:18Z",
            "id": "xxxxxxxxxxxxxx",
            "modified_at": "2015-12-12T15:04:19Z",
            "name": "Swift",
            "percent": 34.08,
            "total_seconds": 22457
        }, {
            "created_at": "2015-12-12T15:04:17Z",
            "id": "xxxxxxxxxxxxxx",
            "modified_at": "2015-12-12T15:04:18Z",
            "name": "Python",
            "percent": 33.8,
            "total_seconds": 22276
        }]
    }
}

これからわかるように、1つのStatが複数のLanguageを保持しているという構造になっています。

今回はStatモデルとLanguageモデルを実装していきましょう。

ここで利用するのが先ほど導入したObjectMapperです。

ObjectMapperに含まれるMappableというプロトコルを実装し、mappingメソッドを実装することで、渡されたJSONから簡単にモデルを生成することができます。

Language.swift
import Foundation
import ObjectMapper

public struct Language: Mappable {
    public var name: String!
    public var totalSeconds: Int!
    public var percent: Float!

    public init?(_ map: Map) {
    }

    public mutating func mapping(map: Map) {
        self.name <- map["name"]
        self.totalSeconds <- map["total_seconds"]
        self.percent <- map["persent"]
    }
}

まずLanguageモデルは上記のように実装します。

ObjectMapperでは<-という演算子が定義されており、これによって簡単にJSONの値を取得できます。

Stat.swift
import Foundation
import ObjectMapper

public struct Stat: Mappable {
    public var languages: [Language] = []

    public init?(_ map: Map) {
    }

    public mutating func mapping(map: Map) {
        self.languages <- map["languages"]
    }
}

次にlanguagesを複数持つStatモデルを定義します。

ObjectMapperではネストしたモデルの保持にも対応していて、self.languagesの型を[Language]にしておくと、
自動的にJSONの辞書を上記で定義したLanguageモデルにマッピングしてくれて便利です。

詳しくはObjectMapperのREADMEを参照してください。

ObjectMapper

今回実装したモデルは、Mapperを使うことで、以下のようにJSONオブジェクトをパースしてオブジェクトを作成できます。

let mapper = Mapper<Stat>()
if let object = mapper.map(jsonObject) {
    print(object)
}

Requestを実装する

次にエンドポイントを叩くリクエストを実装してみましょう。

基底となるプロトコルを定義する

public protocol WakaTimeRequestType: RequestType {
    var apiKey: String? { get set }
}

APIKitではRequestTypeを継承したプロトコルを作り、それをさらに実装することで,
エンドポイントごとのリクエストを作成していきます。

詳しくはAPIKitのREADMEを読むと良いでしょう。

ishkawa/APIKit

今回は、全てのリクエストが認証用のAPIキーを持つはずなので、基底のプロトコルにapiKeyを定義しています。

プロトコル・エクステンションと型制約を使って基底となる実装を定義する

次にプロトコル・エクステンションと呼ばれるSwiftの機能を使って、基底となるAPIリクエストを実装します。

プロトコル・エクステンションは、プロトコルに対してデフォルトの実装を与えることができる機能です。

public extension WakaTimeRequestType where Self.Response: Mappable {
    public var method: HTTPMethod {
        return .GET
    }

    public var baseURL: NSURL {
        return NSURL(string: "https://wakatime.com/api/v1/")!
    }

    public func configureURLRequest(URLRequest: NSMutableURLRequest) throws -> NSMutableURLRequest {
        guard let apiKey = self.apiKey else {
            throw WakaTimeAPIClientError.APIKeyNotDefined
        }
        guard let data = apiKey.dataUsingEncoding(NSUTF8StringEncoding) else {
            throw WakaTimeAPIClientError.AuthenticationFailure
        }
        let encryptedKey = data.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
        URLRequest.setValue("Basic \(encryptedKey)", forHTTPHeaderField: "Authorization")
        return URLRequest
    }

    public func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
        guard let dictionary = object as? [String: AnyObject] else {
            return nil
        }

        guard let data = dictionary["data"] as? [String: AnyObject] else {
            return nil
        }

        let mapper = Mapper<Response>()
        guard let object = mapper.map(data) else {
            return nil
        }
        return object
    }
}

ここで重要なのはresponseFromObjectメソッドです

このメソッドでは、渡されたJSONオブジェクトをパースしてResponse型のオブジェクトを返しています。

Response型は、APIKitによって定義された抽象的な型であり、あとでこのプロトコルを実装した構造体で具体型にオーバーロードすることができます。

public struct StatRequest: WakaTimeRequestType {
    // 抽象的な戻り値であるResponse型をStat型にしている
    typealias Response = Stat
}

この仕組みを使うことで、基底部分の実装を共通化し、様々な型について同じ実装を使い回すことができます。

しかし、このままではResponse型として全ての型を利用することができてしまいますが、今回はObjectMapperMappableプロトコルに準拠した型のみをResponse型として受け付け、
responseFromObjectではMapperを使って、JSONからオブジェクトを生成できるようにしたいです。

そこで以下の 型制約 を使います。プロトコルの後にwhereを使って、適応可能な型を指定することで、その型制約を満たす場合のみ、このプロトコル・エクステンションが適応されます。

public extension WakaTimeRequestType where Self.Response: Mappable

このように表記することで、Response型は必ずMappableを適応した型であることが保証されるため、以下のようにMapperでパースすることが可能になります。

let mapper = Mapper<Response>()
guard let object = mapper.map(data) else {
    return nil
}

詳しくはAPIKitの作者の方が解説ブログを書いているので参考にすると良いでしょう。

Swift 2でのAPIKit + Himotoki

他に特筆すべきとして、configureURLRequestではHTTPヘッダにAPIキーを付加して認証を行っています。

認証は、もちろんAPIの仕様によって異なりますが、今回は以下のドキュメントを参考に、HTTPヘッダにBase64で暗号化したAPIキーを付与することにしています。

WakaTime API v1

エンドポイントごとにAPIリクエストを実装する

その後、基底となるResponseTypeを実装して、それぞれのエンドポイントごとにリクエストを作成します。

/users/current/stats/last_7_days/にアクセスすることで、直近7日間の自分のコーディング履歴を取得することができます。

import Foundation
import ObjectMapper

public struct StatRequest: WakaTimeRequestType {

    // Statを取得するAPI
    public typealias Response = Stat
    public var apiKey: String?

    public var path: String {
        return "/users/current/stats/last_7_days/"
    }
}

ここでの肝はtypealiasによる型の指定で、ここでMappableな型をResponseとして指定することで、先ほど定義した基底となるRequestTypeの実装を汎化して利用することができます。

あとはこれを繰り返すだけで簡単にAPIクライアントが量産できます。

同じようなAPIであれば、typealiasResponseの型を差し替えるだけでリクエストを実装することができます。

作成したRequestを利用してAPIを叩く

最後に、今作成したAPIリクエストを使用して、ここ1週間に使った言語の情報を表示してみましょう。

DemoApplicationViewControllerなどでAPIクライアントを使用するコードを書いてみます。

ViewController.swift
import UIKit
import CoreFoundation
import WakaTimeAPIClient
import APIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        var request = StatRequest()
        request.apiKey = "Your API Key"

        Session.sendRequest(request) { result in
            switch result {
            case .Success(let response):
                let stat = response
                for language in stat.languages {
                    print("\(language.name) \(language.totalSeconds)")
                }
            case .Failure(let error):
                print("error: \(error)")
            }
        }
    }
}

作成したリクエストはSession.sendRequestの引数として渡すことで、クロージャーから結果を受け取ることができます。

Statsのエンドポイントを叩くとこのように、ここ1週間で自分の書いた言語の秒数を取得することができました。

Python 29092
Swift 18638
Other 10287
INI 2832
HTML 1649
Markdown 1643

あとは、同様にして適宜必要なデータが取得できるようにモデルとリクエストを実装していきます。

実装例

実装例は以下になります。

本当はこれを使って、自分の直近のコーディング量をApple Watchなどに表示するアプリを作りたかったのですが、途中で力尽きたので一部のAPIしか実装されていません。

giginet/WakaTimeAPIClient

まとめ

今回は型制約を使った抽象的なAPIクライアントの実装サンプルをご紹介したのですが、非常に説明しづらかったので、正しく伝っていれば幸いです。

このような抽象的な型の概念は、最初に基底の実装を用意するのが大変ですが、一度理解してしまうと、あとは最小限のコードを追加することで様々なモデルやエンドポイントに対応させることができ、爆速でAPIクライアントを実装することができます。

実際に作ってみてWakaTimeのAPIはあまり綺麗ではなく、サンプルとしては不適だったなあと言うのが反省点です。

また、時間があればテストやTravis CIなどの設定についても書きたかったのですが、今回は実現できなかったので次回以降の課題にしたいです。

APIKitはシンプルながらもSwiftの特長を生かした美しい実装になっていますので、内部のコードを読むと大変勉強になると思います。

おまけ - コレクションを返すResponseType

ハマったので追記しておきます。

ResponseTypeが返すResponseを配列にしたいとき、Mappableな要素を含むコレクションという指定をする必要がありますが、このような複雑な型制約も指定することができます。

public extension WakaTimeRequestType
where Self.Response: CollectionType,
Self.Response.Generator.Element: Mappable {
    public func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
        guard let dictionary = object as? [String: AnyObject] else {
            return nil
        }

        // dataという配列が辞書を持っていて、それぞれをparseしてオブジェクトにしたい
        guard let data = dictionary["data"] as? [AnyObject] else {
            return nil
        }

        var objects: [Self.Response.Generator.Element] = []
        for objectDictionary in data {
            let mapper = Mapper<Response.Generator.Element>()
            guard let object = mapper.map(objectDictionary) else {
                continue
            }
            objects.append(object)
        }
        return objects as? Self.Response
    }
}

このとき、以下のような型制約がポイントで,で繋ぐことで複数条件を指定することができます。

public extension WakaTimeRequestType
where Self.Response: CollectionType,
Self.Response.Generator.Element: Mappable

この場合はResponse

  • CollectionTypeを実装している
  • かつ子要素がMappableを実装している

という条件であり、戻り値の型を「Mappableなオブジェクトを含むコレクション」に制約することができます。


struct User: Mappable {
    // 略
}

public UserRequestType: BaseRequestType {
    typealias = [User]
}
211
212
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
211
212