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


[iOS Advent Calendar](http://qiita.com/advent-calendar/2015/ios)の13日目を担当します[@giginet](https://twitter.com/giginet/)です。

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

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

- Xcode7.2
- Swift 2.1.1
- Carthage 0.11.0

# 今回使用するAPI

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

![Screen Shot 2015-12-13 at 01.48.00.png](https://qiita-image-store.s3.amazonaws.com/0/10009/37335ca9-9a76-0781-c5d3-ea33d441213f.png)

[完全自動で、自分のプログラミング「作業ログ」を収集して可視化する「WakaTime」が素晴らしい件！ | シェアしたくなる最新のWebサービス・ITニュース情報をチェック！ APPGIGA!!（アプギガ）](http://plus.appgiga.jp/masatolan/2015/08/18/56671/)

もちろん、題材とするWeb APIはご自分の好きなサービスのものを使うことができます。
練習用であれば、[GitHubのAPI](https://developer.github.com/)などがオススメです。

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

[WakaTime API v1](https://wakatime.com/developers)

# 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](https://qiita-image-store.s3.amazonaws.com/0/10009/3f1fdcad-92a0-2fb4-d8fb-eff481b505a2.png "Screen Shot 2015-12-13 at 01.42.16.png")


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

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

# APIKitとObjectMapper

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

[ishkawa/APIKit](https://github.com/ishkawa/APIKit)
[Hearst-DD/ObjectMapper](https://github.com/Hearst-DD/ObjectMapper)

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

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

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

```shell
$ brew install carthage
```

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

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

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

```shell
$ 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](https://qiita-image-store.s3.amazonaws.com/0/10009/7a769e4e-238d-8fd1-0132-2098f3ca9b78.png "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](https://qiita-image-store.s3.amazonaws.com/0/10009/67739eb4-f597-fe8c-51c1-479f9fcf4231.png "Screen Shot 2015-12-13 at 01.42.58.png")


# モデルを定義する

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

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

WakaTimeのAPIでは、[Stats](https://wakatime.com/developers#stats)というAPIを叩くと、自分が1週間に書いた言語の秒数などを取得することができます。

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

```json
{
    "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から簡単にモデルを生成することができます。

```swift: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の値を取得できます。

```swift: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](https://github.com/Hearst-DD/ObjectMapper#objectmapper)


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

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

# Requestを実装する

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

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

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

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

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

[ishkawa/APIKit](https://github.com/ishkawa/APIKit)

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

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

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

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

```swift
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によって定義された抽象的な型であり、あとでこのプロトコルを実装した構造体で具体型にオーバーロードすることができます。

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

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

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


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

```
public extension WakaTimeRequestType where Self.Response: Mappable
```

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

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

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

[Swift 2でのAPIKit + Himotoki](http://blog.ishkawa.org/2015/07/29/1438165961/)

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

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

[WakaTime API v1](https://wakatime.com/developers#authentication)

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

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

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


```swift
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であれば、`typealias`で`Response`の型を差し替えるだけでリクエストを実装することができます。

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

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

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

```swift: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](https://wakatime.com/developers#stats)のエンドポイントを叩くとこのように、ここ1週間で自分の書いた言語の秒数を取得することができました。

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

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

## 実装例

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

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

[giginet/WakaTimeAPIClient](https://github.com/giginet/WakaTimeAPIClient)

## まとめ

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

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

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

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

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

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

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

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

```swift
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
    }
}
```

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

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

この場合は`Response`が

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

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

```swift

struct User: Mappable {
    // 略
}

public UserRequestType: BaseRequestType {
    typealias = [User]
}
```