この記事ははてなエンジニアアドベントカレンダー2014の16日目です。昨日はid:nobuokaによる「【Retrofit を読む】 利用者が定義したインターフェイスに実装を提供する Java ライブラリの作り方 【リフクレション】」でした。
こんにちは。はてなアプリケーションエンジニアのid:cockscombです。
Webと連携するスマートフォンアプリを開発するとき、Web APIを抽象化したAPIクライアントを作ることがよくあります。これはWeb APIのエンドポイントとメソッドを紐付け、パラメータに名前をつけて、返ってくるJSONのレスポンスを何らかのクラスに当てはめ型付けする、といったようなものになります。
Swiftのモダンな言語機能を利用して、このAPIクライアントを書きましたので、以下に詳解します。例としてGitHubのStatus APIを取り上げています。
またネットワーク通信部分にAFNetworking、JSONからモデルクラスへの対応付けにMantleを使いました。
enumでエンドポイントを列挙する
enum Endpoint { case Status case LastMessage case Messages func request<T: MTLModel where T: MTLJSONSerializing>(manager: AFHTTPSessionManager, parameters: [String: String]?, handler:(response: Response<T>) -> Void) -> NSURLSessionDataTask { let success: ((NSURLSessionDataTask!, AnyObject!) -> Void) = { handler(response: Response<T>.parse($1)) } let failure: ((NSURLSessionDataTask!, NSError!) -> Void) = { handler(response: Response.Error($1)) } switch (self) { case .Status: return manager.GET("/api/status.json", parameters: parameters, success: success, failure: failure) case .LastMessage: return manager.GET("/api/last-message.json", parameters: parameters, success: success, failure: failure) case .Messages: return manager.GET("/api/messages.json", parameters: parameters, success: success, failure: failure) } } }
enum Endpoint
でエンドポイントを列挙APIのエンドポイントはSwiftのenum
を利用して列挙します。Swiftのswitch文ではenumの全てのケースを網羅しないとコンパイルエラーになりますから、実装の漏れを防ぐこともできます。
上の例にはありませんが、共用型のenumとすることでエンドポイントのパスに文字列などを埋め込むことも簡単です。
enum Endpoint { case User(name: String) func request<T: MTLModel where T: MTLJSONSerializing>(manager: AFHTTPSessionManager, parameters: [String: String]?, handler:(response: Response<T>) -> Void) -> NSURLSessionDataTask { ... switch (self) { case let .User(name): return manager.GET("/api/\(name)/status", parameters: parameters, success: success, failure: failure) } } }
ジェネリクスを使ってレスポンスに型付けする
public enum Response<T: MTLModel where T: MTLJSONSerializing> { case One(@autoclosure() -> T) case Many(@autoclosure() -> [T]) case Error(NSError?) static func parse<T: MTLModel where T: MTLJSONSerializing>(JSON: AnyObject) -> Response<T> { var error: NSError? if let array = JSON as? [AnyObject] { if let xs = MTLJSONAdapter.modelsOfClass(T.self, fromJSONArray: array, error: &error) as? [T] { return .Many(xs) } } else if let object = JSON as? [NSObject: AnyObject] { if let x = MTLJSONAdapter.modelOfClass(T.self, fromJSONDictionary: object, error: &error) as? T { return .One(x) } } return .Error(error) } }
APIからの結果を共用型のenum Response
で表します。JSONのオブジェクト、JSONの配列、エラー、の3つのパターンを共用型で表します。
@autoclosure() -> T
などとなっているのは、T
や[T]
にした場合に現在のSwift (Swift 1.1) のコンパイラが"unimplemented IR generation feature non-fixed multi-payload enum layout"というエラーを出すので、そのワークアラウンドです。将来のSwiftで解消されればもっと素直に書けるでしょう。
ここでstatic func parse<T: MTLModel where T: MTLJSONSerializing>(JSON: AnyObject) -> Response<T>
という関数に着目します。これはMantleを利用してJSONから具体的なクラスのインスタンスを作るための関数ですが、具体的なクラスを書かずに型パラメータとして<T: MTLModel where T: MTLJSONSerializing>
という風にTを設定しています。
func testStatus() { let expectation = expectationWithDescription("Status") client.status { (response) -> Void in switch (response) { case .One(let status): XCTAssertEqual(status().status!, "good", "Status is good") default: XCTFail("Response must have one status") } expectation.fulfill() } waitForExpectationsWithTimeout(10, handler: { (error) -> Void in }) }
テストコードで結果を取得しているところをみると、switch
で分岐して実際の結果を得ています。少し冗長に見えますが、例えばenum Response
に関数を追加することで、一般的なケースでより簡単に書けるようにできるかもしれません。また@autoclosure()
しているためにstatus()
となっているところがあります。
APIクライアント
public class APIClient { let manager = AFHTTPSessionManager(baseURL: NSURL(string: "https://status.github.com/")!) public func status(handler: (response: Response<Status>) -> Void) { Endpoint.Status.request(manager, parameters: nil, handler: handler) } public func lastMessage(handler: (response: Response<Message>) -> Void) { Endpoint.LastMessage.request(manager, parameters: nil, handler: handler) } public func messages(handler: (response: Response<Message>) -> Void) { Endpoint.Messages.request(manager, parameters: nil, handler: handler) } }
APIClient
の実装これらの実装を利用することで、APIClient
は上記のように簡単に書くことができます。JSONから変換されるクラスは、それぞれのhandler
に型パラメータとして指定されています。func lastMessage
でもfunc messages
でも同じMessage
クラスを型パラメータとして渡していますが、実際にはそれぞれResponse.One<Message>
とResponse.Many<Message>
が返ってきます。
こうしてできあがったAPIクライアントの実装をGitHubで公開しています。
APIClient.swiftとAPIClientTests.swiftのふたつのファイルを見ると、おおよそどういった感じになっているか掴めるかと思います。
高機能になったenumやジェネリクスといったSwiftのモダンな言語機能を利用することで、Objective-Cよりもスマートに実装できます。Swiftの高い表現力でかっこいいコードを書いていきたいものですね。
スマートなおしらせ
本記事の内容は、筆者とid:yashigani_wが日常的なディスカッションを重ねた結果として生み出されました。私たちといっしょにSwiftでスマートにプログラミングすることに興味をお持ちの方は、こちらからエントリーください。
明日はid:astjさんです。