LINE Creators Studio担当の@ukitakaです。タイトルのハードルの高さに驚きつつ、前篇に引き続きLINE Creators Studio iOSアプリでのAPI周りの実装の話をします。主に APIKit というライブラリを中心とした話になります。
Proxyパターンで必要なパラメータを後から設定する
リクエストヘッダに認可トークンを埋め込んで〜のような処理はよくあると思いますが、みなさんどう設定していますか?
APIKitではRequest
にheaderFields
というプロパティがあり、これを設定することでHTTPヘッダの設定ができます。
struct MyRequest: Request {
var headerFields: [String: String] { ... }
}
トークンのような場合、ほとんどすべてのRequest
の実装にこの設定を書く必要がでてきてしまいます。
struct MyRequest: Request {
let token: String
var headerFields: [String: String] {
["Authorization" : "Bearer " + token]
}
}
もちろんどこかに保存されているtokenをグローバルに参照すればRequestのProtocol Extensionとして書くことはできなくはないですが、テストの時に毎回グローバルな設定を書き換える必要があったり、リクエストによって他のヘッダーを設定したい時などを考えると少し微妙な実装です。
extension Request {
var headerFields: [String: String] {
["Authorization" : "Bearer " + token] // 別のところにあるtokenを参照
}
}
いくつか方法はあると思いますが、今回は一番実装が綺麗で他にも応用がしやすいProxy パターンを使った方法を採用して実装を行いました。
まずはRequestProxy
というprotocolを用意します。
protocol RequestProxy: APIKit.Request {
associatedtype Request: APIKit.Request
var request: Request { get }
}
extension RequestProxy {
var baseURL: URL {
return request.baseURL
}
var headerFields: [String : String] {
return request.headerFields
}
// 以下省略。
// すべてのpropertyとメソッドについて
// 同様にrequestへと処理を渡す
}
RequestProxy
自身がRequest
として振舞うことができ、また元のrequest
をpropertyとして保持していて、デフォルトでは元のrequest
の処理を呼び出すだけです。
そして、これを実装した AuthorizedRequest という型を作ります。
struct AuthorizedRequest<R: APIKit.Request>: RequestProxy {
typealias Request = R
typealias Response = R.Response
let request: R
let token: String
var headerFields: [String : String] {
var h = request.headerFields
h["Authorization"] = "Bearer " + token
return h
}
}
こうすることで例えばAPIClient
みたいなリクエストを担当するクラスがあったとして、そこでヘッダーにtoken
を差し込むことができるようになります。
final class APIClient {
let token: String
init(token: String) {
self.token = token
}
func send<Request: APIKit.Request>(request: Request) -> Observable<Request.Response> {
// tokenを設定
let request = AuthorizedRequest(request: request, token: token)
// requestを使ってリクエストを投げる
// (略)
}
}
こんな感じです。
また、LINE Creators Studioではサーバーを切り替えたりテストを書いたりするために、baseURL
を後から差し替えたいという話があったので、同様の方法で対応しています。
struct BaseURLInjectableRequest<R: APIKit.Request>: RequestProxy {
typealias Request = R
typealias Response = R.Response
let request: Request
let baseURL: URL
init(request: Request, baseURL: URL) {
self.request = request
self.baseURL = baseURL
}
}
もちろん上記のAuthorizedRequest
と組み合わせることも容易です。
APIKit.RequestとHimotoki.Decodableをうまく組み合わせる
APIKitのRequest
の実装では、JSONオブジェクトからResponse 型への変換をするために func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response
を実装する必要がありますが、うまく記述すればこの実装を毎回書く必要がなくなります。
APIKit + Himotokiという組み合わせは(特にSwift3までの日本のiOS開発界隈では) よく見かけますし、APIKit作者のIshkawa氏のBlogでも紹介されているので、すでに同じことをやってる方も多いのではないかなと思います。
要はこういうことです。
extension Request where Response: Decodable {
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
return try decodeValue(object)
}
}
LINE Creators Studioではこれに Array への対応と Void への対応を加えて少しだけ便利にしてあります。
// EがDecodableで、ResponseがArray<E>のとき
// (正確にはArrayだけでなくRangeReplaceableCollectionのサブタイプであればなんでも)
extension Request where Response: RangeReplaceableCollection, Response.Iterator.Element: Decodable {
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
let array: [Response.Iterator.Element] = try decodeArray(object)
return Response(array)
}
}
// ResponseがVoidのとき
// == での制約はSwift3.1から
extension Request where Response == Void {
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
return ()
}
}
( Conditional conformancesが入ればもう少し簡単に記述できますが、Swift4ではまだ入りませんでしたね。。)
ページネーションを型で表現する
LINE Creators Studioの一部のAPIでは、ページネーションに対応しています。
何ページ目を読むのか、次のページがあるのか、などいろいろ考慮すべきことがあって面倒なページネーションの処理ですが、今回は適切に型を定義して実装を行いました。
長くなってしまうので、ここでは簡単な紹介にとどめておきます。
まず、ページネーションに対応したAPIのレスポンスの抽象化として以下のような型を作りました。
indirect enum PaginatedResponse<Request: APIKit.Request> {
case hasNext(Request.Response, AnyRequest<PaginatedResponse<Request>>)
case done(Request.Response)
}
TypeErasureが入っているのでちょっとだけ分かりづらいですが、簡単に言うと 「次がある場合は、いまのレスポンス結果と次のページのリクエスト」「これで終わりの場合はレスポンスのみ」を表現する型です。
次にページ付きリクエストのためのPaginatedRequest
という protocol を作ります。
protocol PaginatedRequest: RequestProxy {
// レスポンスから次のページのリクエストを作る
func nextRequest(from response: Request.Response, urlResponse: HTTPURLResponse) throws -> Self?
}
そしてこれの具体的な実装を用意します。
今回はページの情報はヘッダーに埋め込むことにしたので、headerFields
を実装してあげます。
struct LCSPaginatedRequest<R: APIKit.Request>: PaginatedRequest {
...
// ペジネーションリクエスト時に必要なページの設定
var headerFields: [String : String] {
var h = request.headerFields
h["page"] = "(page)"
return h
}
// 次のリクエストの作成
func nextRequest(from response: Request.Response, urlResponse: HTTPURLResponse) throws -> LCSPaginatedRequest? {
guard let page = urlResponse.allHeaderFields["next_page"] as? Int else {
return nil
}
return LCSPaginatedRequest(request: request, page: page)
}
}
あとはページングに対応したRequest
をLCSPaginatedRequest
にした上で使ってあげれば、良い感じに動きます。
LCSPaginatedRequest(request: request)
まとめ
型の話というよりかはAPIKit周辺のテクニックの紹介になってしまいました。
他にもRxSwiftと組み合わせるとか、Error型をもたせてステータスコードが4xxとか5xxのときにはその型にマッピングするとか、紹介しきれなかったものもいくつかありますがLINE Creators Studioではこんな感じで実装を行なっているという雰囲気が掴んでもらえたかなと思います。