[Swift] タイプセーフな軽量HTTPクライアント「APIKit」を試してみた

logo_swift_400x400
84件のシェア(ちょっぴり話題の記事)

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 APIKitとは

Yosuke Ishikawa氏 作成のAPIKitは、比較的新しいHTTPクライアントです。

https://github.com/ishkawa/APIKit

Swiftは、タイプセーフな言語ですが、APIKitは、このタイプセーフの利点を最大限に発揮していると思います。

動作条件は次のとおりです。

  • Swift 2.1
  • Mac OS 10.9 or later
  • iOS 8.0 or later
  • watchOS 2.0 or later
  • tvOS 9.0 or later

また、特徴としては、次のようなものが挙げられます。

  • エンドポイントごとにクラスを定義
  • パラメータなどを型で明確に制限できる(Enumも利用可能)
  • レスポンスが、成功・失敗の2値となる
  • レスポンスの型を定義することができ、アンラップが不要(エラー時はNSError)

それでは、実際に使用してみたようすを以下に紹介させて下さい。

2 サンプルのWebAPI

動作確認のためのWebAPIとして、お天気Webサービス(Livedoor Weather Web Service / LWWS)を使用しました。

こちらは、現在全国142カ所の今日・明日・あさっての天気予報・予想気温と都道府県の天気概況情報を提供しているもので、無料で利用することができます。

002

使用方法としては、例えば「札幌」を取得する場合、次のようにパラメータを与えてアクセスします。

基本URL + 札幌のID(016010)
http://weather.livedoor.com/forecast/webservice/json/v1?city=016010

3 サンプルプロジェクトの作成

Xcodeで「File」-「New」-「Project」とたどり「iOS」-「Application」 から「Single View Application」を選択し新しいプロジェクトを作成します。(ここでは、名前を「APIKitSample」としました)

また、iOS9におけるATS(App Transport Security)の対応を省略するため、「iOS Develoyment Target 」を 「8.4」 にして、iOS8で動作確認できるようにしました。

004

4 APIKitのインストール

APIKitは、CocoaPodsでインストール可能になっています。

$ cat Podfile
platform :ios, '8.0'
use_frameworks!

target 'APIKitSample' do
    pod 'APIKit', '~> 1.1.2'
end
$
$ pod install
Installing APIKit (1.1.2)
Installing Result (1.0.1)
・・・
$ open APIKitSample.xcworkspace

2015.12.23現在の最新は、1.1.2となっていました。

001

5 取得データの型定義

「お天気Webサービス」で取得できるJSONデータは次のような感じです。

{
   //・・・省略・・・

   "title" : "福岡県 久留米 の天気",
   "description" : {
      "text" : " 九州北部地方は、高気圧に覆われて晴れています。\n\n 29日は、九州北部地方では・・・“,
      "publicTime" : "2013-01-29T10:37:00+0900"
   },
   "forecasts" : [
      {
         "dateLabel" : "今日",
         "telop" : "晴のち曇",

         //・・・省略・・・

      },
      {
         "dateLabel" : "明日",
         "telop" : "晴れ",

        //・・・省略・・・

      },

//・・・省略・・・

}

このデータの中から利用したい項目だけ抽出してプロパティを定義し、init()で上記のJSONをDictionary型で受け取って初期化できるように実装します。

struct WeatherData {
    var title: String = "" // タイトル
    var description: String = "" // 説明
    var today:String = "" // 今日の天気
    var tomorrow: String = “”// 明日の天気
    
    init?(dictionary: [String: AnyObject]) {
        guard let title = dictionary["title"] as? String else {
            return nil
        }

        guard let description = dictionary["description"]?["text"] as? String else {
            return nil
        }

        guard let array = dictionary["forecasts"] as? NSArray else {
            return nil
        }
        for a in array {
            if let dateLabel = a["dateLabel"] as? String {
                if dateLabel == "今日" {
                    guard let today = a["telop"] as? String else {
                        return nil
                    }
                    self.today = today
                } else if dateLabel == "明日"{
                    guard let tomorrow = a["telop"] as? String else {
                        return nil
                    }
                    self.tomorrow = tomorrow
                }
            }
        }
        self.title = title
        self.description = description
    }
}

6 RequestType プロトコル

APIKitでは、エンドポイントごとにRequestTypeプロトコルを実装したクラスを定義します。

そして、RequestTypeプロトコルは、次の5つのパラメータを必須とします。

  • typealias Response // レスポンスの型
  • var baseURL: NSURL // ベースURL
  • var method: Method // メソッド
  • var path: String // リソース
  • func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? // 受信後のデコード処理

最初に、RequestTypeプロトタイプのクラスを定義し、extensionで「ベースURL」を定義します。

protocol WeatherRequestType : RequestType {
    
}

extension WeatherRequestType {
    var baseURL:NSURL {
        return NSURL(string: "http://weather.livedoor.com/forecast/webservice/json")!
    }
}

続いて、上記のクラスを継承した形で、最終的なアクセス用のクラスを設計します。そして、ここで、「ベースURL」以外の必須項目を定義しています。

struct GetWeatherRequest: WeatherRequestType {
    // 成功時の値をWeatherData型で返す
    typealias Response = WeatherData

    // メソッドはGET
    var method: HTTPMethod {
        return .GET
    }
    
    // リソース
    var path: String {
        return "/v1"
    }
    // 付加するパラメータ(オプション)
    var parameters: [String: AnyObject] {
        return [
            "city": "016010",
        ]
    }
    // 受信完了時のデコード処理
    func responseFromObject(object: AnyObject, URLResponse: NSHTTPURLResponse) -> Response? {
        // 受信データを[String:AnyObject]のDictionary型にキャストする
        guard let dictionary = object as? [String: AnyObject] else {
            return nil
        }
        // Dictionaryを使用してWeather型のデータを初期化する        
        guard let weatherData = WeatherData(dictionary: dictionary) else {
            return nil
        }
        // 初期化したWeather型のデータを返す
        return weatherData
    }
}

上記のように、RequestTypeプロトコルを2段階に継承したのは、複数のエンドポイントがある場合に、共通部分を共有するためです。(上の例では、エンドポイントが一つなので、分けたことによる利点はありません)

例えば、URLの最後の「v1」が、「v2」となるエンドポイント用のクラスを新たに設計する場合、WeatherRequestTypeから定義することで共通となるベースURLが共有できることになります。複数のエンドポイントを定義する中で、共通となる部分は、この要領で基底の方に押し込むといいでしょう。

7 最終的な利用

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // エンドポイント用のクラスを生成する
        let request = GetWeatherRequest()
        // WebAPIへのアクセス
        Session.sendRequest(request) { result in
            switch result {
            case .Success(let weatherData): // 成功時は、Weather型のデータが取得できる
                print("title: \(weatherData.title)")
                print("description: \(weatherData.description)")
                print("today: \(weatherData.today)")
                print("tomorrow: \(weatherData.tomorrow)")
                
            case .Failure(let error): // 失敗した場合、NSError型となる
                print("error: \(error)")
            }
        }
        return true
    }

次の図は、実行している様子です。Session.sendRequestでresultがSeccessとなり、Weatherクラスでデータ取得できていることを確認できます。

003

8 その他

(1) ヘッダ追加(HTTPHeaderFields)

ヘッダを追加する場合は、HTTPHeaderFieldsプロパティを定義します。

var HTTPHeaderFields: [String: String] {
    return ["x-other": "xxxxxx"]
    //複数の場合
    //return ["x-other1": "xxxx" ,"x-other2": "zzz"]
}

(2) データ送信(RequestBodyBuilder)

任意のデータを送信したい場合は、requestBodyBuilderをオーバーライドします。

RequestBodyBuilderは、JSON(デフォルト)、URL、Customの3種類が定義可能であり、Customを選択すると、ヘッダ情報のContentTypeと、Bodyを生成するメソッドを自由に定義できます。

下記の例では、ContentTypeを「text/plain」に設定し、「Hello」という文字列をデータとして送信しています。

    var requestBodyBuilder: RequestBodyBuilder {
        return .Custom(contentTypeHeader: "text/plain",
                       buildBodyFromObject: { o in
                            let str = "Hello\r\n"
                            return str.dataUsingEncoding(NSUTF8StringEncoding)!
                       })
    }

実行時のデータをキャプチャした様子です。 005


参考:https://github.com/ishkawa/APIKit/blob/master/APIKit/RequestBodyBuilder.swift

(3) 受信パーサー(ResponseBodyParser)

RequestTypeは、デフォルトで受信したデータをJSON形式としてDictionary型にパースします。そして、このパースに失敗した場合、その時点でエラーとなります。

ResponseBodyDeserializationError(Error Domain=NSCocoaErrorDomain Code=3840 
 (JSON text did not start with array or object and option to allow fragments not set.)

もし、JSON形式以外のデータを扱う場合は、responseBodyParserをオーバーライドする必要があります。そして、この要領は、先の「(2) データ送信」と同じになります。


参考:https://github.com/ishkawa/APIKit/blob/master/APIKit/ResponseBodyParser.swift

9 まとめ

今回は、APIKitを使用してみましたが、予想以上に厳格に型で縛ることが可能であると感じました。エンドポイント単位で利用可能なパラメータなどをEnumで定義すれば、ほとんど、そのままでWebAPIのドキュメントになってしまうと思います。

APIKitは、まだ新しいライブラリであり、まだまだ仕様の変更もあると思いますが、タイプセーフの利点追求はその目的とされているようですので本当に楽しみです。

github サンプルコード(https://github.com/furuya02/APIKitSample)

10 参考資料


https://github.com/ishkawa/APIKit
#potatotips でAPIKitを紹介してきた
SwiftらしいAPIクライアント APIKitを使う-

AWS Cloud Roadshow 2017 福岡