Quantcast
Channel: クックパッド開発者ブログ
Viewing all articles
Browse latest Browse all 726

API クライアントを書きつつ Swift らしいコードを考える

$
0
0

こんにちは、技術部モバイル基盤グループの茂呂(@slightair)です。

クックパッドは Garageという RESTful Web API 開発を楽にする Rails のためのライブラリを作り、内部通信やモバイルアプリケーションのためのAPIサーバの開発に利用しています。

過去の Garage の紹介記事はこちらです。

この Garage を使って実装された Web API を iOS アプリから気軽に呼べるように、 Swift で Garage のクライアントを実装してみました。

この記事では、GarageClientSwiftの紹介をしつつ、これを作りながら Swift らしいコードってどんなコードなんだろうと考えたことをつらつらと書いていきたいと思います。

Garage

Garage は RESTful Web API 開発のためのライブラリです。OSSとして公開しています。 https://github.com/cookpad/garage

今回はクライアントサイドの話をしたいので Garage 自体の説明は過去の記事にまかせます。

記事で紹介されているサンプル実装を使ってクライアントの開発・動作確認を行います。 手元で動作を確認しながら読みたい場合は、リポジトリからコードをチェックアウトして動かしてください。 https://github.com/taiki45/garage-example

クライアントアプリケーションの動作確認時には、サーバアプリケーションのアクセストークンが必要になるので、過去の記事の手順にしたがって取得してください。

GarageClientSwift

GarageClientSwift はその名の通り、GarageClient の Swift による実装です。 https://github.com/slightair/GarageClientSwift

GarageClientSwift は僕が趣味でなんとなく書いたものなので、クックパッドのアプリでもうバッチリ使っているぜ!…というわけではありません。ただ基本的な機能はそろっているのではないかと思います。

GarageClientSwift は HimotokiAPIKitというSwiftのライブラリに依存しています。 これらのライブラリについては後述します。

GarageClientSwift の使い方

GarageClientSwift は Carthageでプロジェクトに導入できます。 詳しくは README.mdを読んでください。 この記事では GarageClientSwift 1.1.0 の実装を使った例を出します。

GarageClientSwift の workspace に Demo.playgroundを同梱しているので、コードを触りながら動作を確認したければこれを利用できるでしょう。 Demo.playgroundを動かす際は一度 GarageClient iOSの scheme でビルドしてから playground ファイルを開いてください。

この節で説明するものは、この playground ファイルに記述されているものです。

リソースのモデルを定義する

Web API Client を使うということは、なんらかのリソースを取得したいと考えているはずです。 ここでは User リソースを取得することを考えます。 以下のように User構造体を定義します。 リソースモデルは Himotoki の Decodableに準拠するようにします。

structUser:Decodable {
    letid:Intletname:Stringletemail:Stringstaticfuncdecode(e:Extractor) throws ->User {
        return try User(
            id:e<|"id",
            name:e<|"name",
            email:e<|"email"
        )
    }
}

リクエストを定義する

次にリソースを得るためにどのようなリクエストを投げるか定義します。 /usersに GET リクエストを送信してユーザーの一覧を取得しましょう。 このようなリクエストを表現する構造体を定義します。

structGetUsersRequest:GarageRequestType {
    typealiasResource= [User]

    varmethod:HTTPMethod {
        return .GET
    }

    varpath:String {
        return"/users"
    }

    varqueryParameters:[String: AnyObject]? {
        return [
            "per_page":1,
            "page":2,
        ]
    }
}

なんとなくやりたいことがわかると思います。 APIKitを知っている人はそのまんまだと感じていると思います。

Garage の設定を定義する

次にGarageアプリケーションへ接続するための情報を用意します。 GarageConfigurationTypeというプロトコルがあるので、それに準拠する構造体かクラスを定義してそのインスタンスを作ります。ここでは単純にGarageアプリケーションのベースURLとアクセストークンをただ保持している構造体を作りました。実際にはアクセストークンを認可サーバから取得してそれを返してくれるような認証・認可機能を実装したクラスになると思います。

structConfiguration:GarageConfigurationType {
    letendpoint:NSURLletaccessToken:String
}

letconfiguration= Configuration(
    endpoint:NSURL(string:"http://localhost:3000")!,
    accessToken:"YOUR ACCESS TOKEN"
)

リクエストを送信する

あとはリクエストを送信するだけです。 GarageClient のインスタンスを作って、sendRequest メソッドでリクエストを送信します。 リクエストのコールバックには Result.Success.Failureが引数に渡されるので結果に応じた処理を記述します。 .Successの場合には、取得したリソースやページングのための件数などの情報を含む GarageResponse構造体を取得できます。

letgarageClient= GarageClient(configuration:configuration)
garageClient.sendRequest(GetUserRequest()) { result inswitch result {
    case .Success(letresponse):
        debugPrint(response)

        letusers= response.resource
        debugPrint(users)
    case .Failure(leterror):
        debugPrint(error)
    }
}

以上が GarageClientSwift を使ったリクエスト送信までの流れです。

Himotoki

Himotoki はJSONをデコードしてモデルにマッピングするためのライブラリです。 https://github.com/ikesyo/Himotoki

この記事では Himotoki 2.0.1 の実装を使った例を出します。

Himotoki を使って以下の様な JSON を User構造体にマッピングするにはこのように記述します。

JSON

{"id": 2,
  "name": "bob",
  "email": "bob@example.com"
}

User.swift

structUser:Decodable {
    letid:Intletname:Stringletemail:Stringstaticfuncdecode(e:Extractor) throws ->User {
        return try User(
            id:e<|"id",
            name:e<|"name",
            email:e<|"email"
        )
    }
}

e <| "id"のような見慣れない構文が登場しますが、これは Himotoki の Extractor のためのオペレータです。JSONから指定したキーの要素を期待通りの型で取り出すための工夫です。

Himotoki の実装をのぞいてみる

<|はどのような実装になっているのか見てみましょう。

https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/Operators.swift

infix operator <| { associativity left precedence 150 }

/// - Throws: DecodeError or an arbitrary ErrorTypepublicfunc<|<T: Decodable>(e:Extractor, keyPath:KeyPath) throws ->T {
    return try e.value(keyPath)
}

Swift ではオペレータを定義することができるので、その結合の仕方と優先度、処理を定義しています。 <|は Extractor の e.value(keyPath)を呼んでいることがわかりました。

https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/Extractor.swift

privatefunc_rawValue(keyPath:KeyPath) throws ->AnyJSON? {
    guard isDictionary else {
        throw typeMismatch("Dictionary", actual:rawValue, keyPath:keyPath)
    }

    letcomponents= ArraySlice(keyPath.components)
    return valueFor(components, rawValue)
}

/// - Throws: DecodeError or an arbitrary ErrorTypepublicfuncvalue<T: Decodable>(keyPath:KeyPath) throws ->T {
    guardletrawValue= try _rawValue(keyPath) else {
        throw DecodeError.MissingKeyPath(keyPath)
    }

    do {
        return try T.decodeValue(rawValue)
    } catch letDecodeError.MissingKeyPath(missing) {
        throw DecodeError.MissingKeyPath(keyPath + missing)
    } catch letDecodeError.TypeMismatch(expected, actual, mismatched) {
        throw DecodeError.TypeMismatch(expected:expected, actual:actual, keyPath:keyPath+ mismatched)
    }
}

value メソッドはつまり、与えられた keyPath で Dictionary から要素を取り出し、返り値の型の decodeValueを呼びだして値を返しています。TypeConstraintsを使って Decodable プロトコルに準拠していることを制限に課しているので、e <| "id"の返り値が Decodable に準拠する型でないといけません。

上記のJSONの "id"要素は数値なので Int になることを期待します。Int や String のようなよく使う型に対しては Himotoki ですでに Decodable に準拠するための実装が extension で追加されています。 https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/StandardLib.swift

さて、User 構造体の decode メソッドは以下のように実装していました

staticfuncdecode(e:Extractor) throws ->User {
    return try User(
        id:e<|"id",
        name:e<|"name",
        email:e<|"email"
    )
}

User 構造体では、 id は Int、name と email は Stringと型宣言してあるので、コンパイラが e <| "id"Inte <| "name"Stringが返ると推論します。型推論がうまく働いてくれるのですっきりした記述になるわけです。

Decodable プロトコルはどのような定義になっているのでしょうか。 https://github.com/ikesyo/Himotoki/blob/2.0.1/Sources/Decodable.swift

static func decode(e: Extractor) throws -> Selfを定義していることを要求しています。 これが、先ほど見つけた decodeValueメソッドから呼ばれます。

Swift の Protocol にはデフォルト実装を Protocol extension で追加できます(Swift2 から) なので Decodable に準拠している構造体は decodeValueメソッドを実装していなくてもデフォルトの実装が使われます。

Himotoki の Decodable プロトコルに準拠していれば、JSON から作られたDictionaryを以下のようにしてモデルにマッピングできます。

letuser:User? = try? decodeValue(JSON)
letusers:[User]? = try? decodeArray(ArrayJSON)

便利ですね! 期待した型とJSONの要素の型が一致しない場合は例外が投げられマッピングに失敗します。

Himotoki は Generics と型推論、Protocol をうまく使った例だと思います。

APIKit

APIKit はリクエストとレスポンスを抽象的に表現できて使いやすいAPIクライアントライブラリです。 https://github.com/ishkawa/APIKit/

リクエストを表す構造体を定義し、それに対応するレスポンスをモデルで受け取れるのが特長です。 リクエストに渡すパラメータの型を明示できます。 リクエスト結果は、成功と失敗のどちらかの状態を表現する Result型で受け取れます。Result には成功時に目的のオブジェクトを、失敗時にエラー情報を含めることができるので、Optional な変数を用いることなくリクエスト結果を受け取ることができます。

この記事では APIKit 2.0.1 の実装を使った例を出します。

使い方を見てみましょう。 https://github.com/ishkawa/APIKit/blob/2.0.1/Documentation/GettingStarted.md

まずはリクエストを定義します。 サンプルは GitHub API の Ratelimit を取得する API を実行するようです。 RequestTypeプロトコルに準拠した RateLimitRequestとそのレスポンスを表すモデル RateLimitを定義します。

structRateLimitRequest:RequestType {
    typealiasResponse= RateLimit

    varbaseURL:NSURL {
        return NSURL(string:"https://api.github.com")!
    }

    varmethod:HTTPMethod {
        return .GET
    }

    varpath:String {
        return"/rate_limit"
    }

    funcresponseFromObject(object:AnyObject, URLResponse:NSHTTPURLResponse) throws ->Response {
        guardletdictionary= object as? [String:AnyObject],
            letrateLimit= RateLimit(dictionary:dictionary) else {
                throw ResponseError.UnexpectedObject(object)
        }

        return rateLimit
    }
}

structRateLimit {
    letlimit:Intletremaining:Intinit?(dictionary:[String: AnyObject]) {
        guardletlimit= dictionary["rate"]?["limit"] as? Int,
            letremaining= dictionary["rate"]?["limit"] as? Int else {
                returnnil
        }

        self.limit = limit
        self.remaining = remaining
    }
}

RateLimitRequest構造体には API の baseURLmethodpathなどのリクエストを構築するために必要な情報を記述します。 また、レスポンスをどのようにモデルにマッピングするかを responseFromObjectメソッドに記述します。

リクエストの定義ができたらそれを使ってリクエストを投げます。 コールバックには Result<T, Error>が渡されるのでそれに応じた処理を記述します。 .Successの場合はレスポンスをマッピングしたモデルが含まれているので、後は好きなように扱えばよいでしょう。 RateLimitRequestのレスポンスはRateLimitと定義してあるので、resultResult<RateLimit, Error>であり、.Success<RateLimit>が渡されるわけです。なので limitremainingのプロパティにアクセスできます。

letrequest= RateLimitRequest()

Session.sendRequest(request) { result inswitch result {
    case .Success(letrateLimit):
        print("limit: \(rateLimit.limit)")
        print("remaining: \(rateLimit.remaining)")

    case .Failure(leterror):
        print("error: \(error)")
    }
}

APIKit の実装をのぞいてみる

APIKit の RequestTypeプロトコルの実装を見てみましょう。 https://github.com/ishkawa/APIKit/blob/2.0.1/Sources/RequestType.swift

RequestTypeはリクエストを表現する構造体が準拠すべきプロトコルでした。 baseURLmethodpathqueryParametersheaderFieldsなどなど様々なプロパティがありますがほとんどにデフォルト実装が用意されており、オプションのパラメータはリクエストを定義する際に指定したいものだけ実装すれば良いようになっています。

受け取ったレスポンスをパースしたオブジェクトをどのようにモデルにマッピングするかを以下のメソッドに記述します。デフォルトではレスポンスに JSON を期待しています。dataParserプロパティを指定すれば JSON 以外も受け付けることができます。

funcresponseFromObject(object:AnyObject, URLResponse:NSHTTPURLResponse) throws ->Response

他にも以下の様なメソッドが宣言されています。

funcinterceptURLRequest(URLRequest:NSMutableURLRequest) throws ->NSMutableURLRequestfuncinterceptObject(object:AnyObject, URLResponse:NSHTTPURLResponse) throws ->AnyObject

これらのメソッドをリクエストの構造体に実装することで送信する URLRequest に追加の情報を付与したり、レスポンスに応じて独自のエラーを投げてエラーレスポンスを処理することができるようになっています。 これらのメソッドも必要でなければデフォルト実装が利用されるので定義を省略することができます。

APIKit は Swift の Protocol をうまく利用していると思います。

次に Sessionを見てみましょう。 https://github.com/ishkawa/APIKit/blob/2.0.1/Sources/Session.swift

Singleton の Sessionオブジェクトを持っているので、通常の利用範囲であればクラスメソッドの Session.sendRequestメソッドを使えば良いようになっていることがわかります。

sendRequestメソッドは TypeConstraints で引数 request の型 RequestRequestTypeに準拠しているべきと制約を課しています。

publicfuncsendRequest<Request: RequestType>(request:Request,
                                        callbackQueue:CallbackQueue? =nil,
                                              handler: (Result<Request.Response, SessionTaskError>) ->Void= {r in})
                                              ->SessionTaskType? {
...

RequestType には以下の様な記述がありました。

publicprotocolRequestType {
    /// The response type associated with the request type.associatedtype Response
...

これは Protocol の Associated Types という機能で定義するプロトコルに関連する型を指定できるものです。 以下のように RateLimitRequest の Response 型を typealiasキーワードで指定することができます。

structRateLimitRequest:RequestType {
    typealiasResponse= RateLimit
...

これにより先ほどの sendRequestメソッドの handler 引数にある Result<Request.Response, SessionTaskError>の記述が、RateLimitRequestの場合は Result<RateLimit, SessionTaskError>に定まるわけです。 こうして、リクエストとそれに対応するレスポンスのモデルの型を明示できるようになっています。

このようにして APIKit はリクエストとレスポンスを表現するモデルをわかりやすく定義できるように作られています。 僕のお気に入りのライブラリです。

GarageClientSwift の実装

GarageClientSwift はこれまで説明してきた Himotoki と APIKit を組み合わせて作ったライブラリです。 すでに利用例で見せたように、Himotoki を使った Decodable なリソースのモデルを用意し、APIKit のようにリクエストを表現してリクエストを送信します。

やっていることは APIKit をラップして、Garage アプリケーションの認証に必要なアクセストークンをリクエストに付与したり、Garage のレスポンスに共通で含まれるページング等の情報を持った値を表現する GarageResponseを返すようにしています。

少し工夫したところはリソースの型に User[User]のようにモデルの配列も指定できるようにしたところです。

GarageClientにふたつの sendRequestを定義しています。 https://github.com/slightair/GarageClientSwift/blob/1.1.0/Sources/GarageClient.swift

publicfuncsendRequest<R: GarageRequestType, D: Decodable where R.Resource == D>
    (request:R,
     handler: (Result<GarageResponse<D>, SessionTaskError>) ->Void= { result in })
    ->SessionTaskType? {
        letresourceRequest= RequestBuilder.buildRequest(request, configuration:configuration)
...publicfuncsendRequest<R: GarageRequestType, D: Decodable where R.Resource: CollectionType, R.Resource.Generator.Element == D>
    (request:R,
     handler: (Result<GarageResponse<[D]>, SessionTaskError>) ->Void= { result in })
    ->SessionTaskType? {
        letresourceRequest= RequestBuilder.buildRequest(request, configuration:configuration)
...

リクエストの ResourceDecodableまたは Decodableを要素に持つ CollectionTypeを受け付けています。

RequestBuilder にもふたつの buildRequestを定義しており、それぞれ SingleResourceRequestMultipleResourceRequestを作ります。 https://github.com/slightair/GarageClientSwift/blob/1.1.0/Sources/RequestBuilder.swift

structRequestBuilder {
    staticfuncbuildRequest<R: GarageRequestType, D: Decodable where R.Resource == D>
        (baseRequest:R, configuration:GarageConfigurationType) ->SingleResourceRequest<R, D> {
        return SingleResourceRequest(baseRequest:baseRequest, configuration:configuration)
    }

    staticfuncbuildRequest<R: GarageRequestType, D: Decodable where R.Resource: CollectionType, R.Resource.Generator.Element == D>
        (baseRequest:R, configuration:GarageConfigurationType) ->MultipleResourceRequest<R, D> {
        return MultipleResourceRequest(baseRequest:baseRequest, configuration:configuration)
    }
}

SingleResourceRequestMultipleResourceRequestの違いは、中で呼んでいる Himotoki のメソッドが decodeValuedecodeArrayかの違いです。 ともに ResourceRequestプロトコルに準拠しており、このプロトコルは APIKit の RequestTypeを継承しています。 前述した GarageRequestTypeは APIKit の RequestType風のプロトコルですが、実際には APIKit の sendRequestに渡す ResourceRequestRequestBuilderが作り GarageRequestTypeから値を取っていたのでした。 https://github.com/slightair/GarageClientSwift/blob/1.1.0/Sources/GarageRequestType.swift

今回のような範囲では Class の継承ではなく Protocol を使うとすっきりと書けます。 Swift の Protocol は Protocol extension によるデフォルト実装の提供が強力で、継承ができない struct であっても Protocol の組み合わせで拡張していくことができます。 このような Protocol を組み合わせていくプログラミング手法を Apple は Protocol Oriented Programming として提唱しています。

まとめ

GarageClientSwift というライブラリを紹介しつつ、このライブラリの実装に利用した Himotoki、 APIKit と GarageClientSwift 自身の実装を読み、Protocol や Generics を使った実装例の説明をしました。 Swift は新しい言語であり、おもしろい機能や新しいプログラミング手法を提供してくれます。単なる Objective-C の置き換えでアプリケーションを楽に記述するための言語とは捉えずに、 Swift の言語機能を使ってより柔軟で安全なコードを記述して素敵なアプリケーションを作りましょう。


Viewing all articles
Browse latest Browse all 726

Trending Articles