こんにちは、技術部モバイル基盤グループの茂呂(@slightair)です。
クックパッドは Garageという RESTful Web API 開発を楽にする Rails のためのライブラリを作り、内部通信やモバイルアプリケーションのためのAPIサーバの開発に利用しています。
過去の Garage の紹介記事はこちらです。
- RESTful Web API 開発をささえる Garage - http://techlife.cookpad.com/entry/2014/11/06/100000
- RESTful Web API 開発をささえる Garage (client 編) - http://techlife.cookpad.com/entry/2014/12/26/193802
この 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 は Himotokiと APIKitという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"
は Int
、 e <| "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 の baseURL
、 method
、 path
などのリクエストを構築するために必要な情報を記述します。
また、レスポンスをどのようにモデルにマッピングするかを responseFromObject
メソッドに記述します。
リクエストの定義ができたらそれを使ってリクエストを投げます。
コールバックには Result<T, Error>
が渡されるのでそれに応じた処理を記述します。
.Success
の場合はレスポンスをマッピングしたモデルが含まれているので、後は好きなように扱えばよいでしょう。
RateLimitRequest
のレスポンスはRateLimit
と定義してあるので、result
は Result<RateLimit, Error>
であり、.Success<RateLimit>
が渡されるわけです。なので limit
や remaining
のプロパティにアクセスできます。
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
はリクエストを表現する構造体が準拠すべきプロトコルでした。
baseURL
、 method
、 path
、 queryParameters
、 headerFields
などなど様々なプロパティがありますがほとんどにデフォルト実装が用意されており、オプションのパラメータはリクエストを定義する際に指定したいものだけ実装すれば良いようになっています。
受け取ったレスポンスをパースしたオブジェクトをどのようにモデルにマッピングするかを以下のメソッドに記述します。デフォルトではレスポンスに 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 の型 Request
に RequestType
に準拠しているべきと制約を課しています。
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) ...
リクエストの Resource
が Decodable
または Decodable
を要素に持つ CollectionType
を受け付けています。
RequestBuilder にもふたつの buildRequest
を定義しており、それぞれ SingleResourceRequest
、 MultipleResourceRequest
を作ります。
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) } }
SingleResourceRequest
と MultipleResourceRequest
の違いは、中で呼んでいる Himotoki のメソッドが decodeValue
か decodeArray
かの違いです。
ともに ResourceRequest
プロトコルに準拠しており、このプロトコルは APIKit の RequestType
を継承しています。
前述した GarageRequestType
は APIKit の RequestType
風のプロトコルですが、実際には APIKit の sendRequest
に渡す ResourceRequest
は RequestBuilder
が作り 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 の言語機能を使ってより柔軟で安全なコードを記述して素敵なアプリケーションを作りましょう。