これはなに
この記事はiOS2 Advent Calendar 2017の5日目の記事です。
今年のiOSDCで注目を集めた iOSDesignPatternSamples を参考にしてiOSのデザインパターンを勉強してる。このiOSDesignPatternSamplesは、GithubKit というライブラリを使って開発されている。
今回は、このGithubKitが面白かったので自分も真似して作ってみたという話。しかし、ただ真似するだけではコピペになるし学びが薄いので、APIをQiitaに変更して開発してみた。
先に作った感想
結論から先に言うと、とても勉強になったし、@marty-suzukiさんには感謝しかない。
具体的に良かったのは、「SwiftやiOSの新機能をさっと試すのに適当なサイズのサンプルアプリが欲しいとき、◯◯Kitというライブラリをフレームワーク化しておくとサンプルアプリを作りやすい」 ことに気づけたところ。
今までは個人で開発したアプリで新機能を試してたが、一度リリースしてしまうと既存ユーザーへの影響やデザインパターンとの調整を考えて、新機能を試すのが面倒になり何もしないことが多かった。
かといって、ゼロからサンプル用のアプリを作るのも面倒で、サンプルに適した形にするのもまた面倒だ。
結局、なにも試してない状態になってた。要するに、サボってた。
でも、この〇〇Kitとかを作るとcarthageでプロジェクトに取り込んでささっと適当なサイズのアプリにして新機能を試せていいかなと思い始めてる。
もちろん、試したい機能によって下準備とか別に必要だけど、それもフレームワーク化して 「新機能をすぐに試せる環境作りをしておくのが大切なんだ」 と実感した。
QiitaKitForSampleについて
本題に入る。GithubKitはSwift3の時代に開発されてたようで、Swift4の機能は使われていない。
というわけで、QiitaKitForSampleではSwift4の新機能の Codable
を使ってAPIのリクエスト、レスポンス、モデルを書き換えてみた。
リクエスト(Requestable)
まずは、リクエストの部分。ResponseTypeをCodableにしてリクエストして返ってきたデータをレスポンスにデコードする static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<ResponseType>
を定義した。
public protocol Requestable { associatedtype ResponseType: Codable static var baseURL: URL { get } static var notFoundText: String { get } var allHTTPHeaderFields: [String: String]? { get } var endpoint: URL { get } var path: String { get } var method: HttpMethod { get } var body: [String: Any] { get } static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<ResponseType> }
レスポンス(Response<T: Codable>)
次にレスポンスではリクエストの static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<ResponseType>
で処理するレスポンスのデコードメソッドを定義した。
singleはその名のとおりネストのないJSON(ex: {}
)の場合、unkeyedContainerは配列(ex: {},{}
)の場合に使う感じを想定して定義した。
ちなみに、.formatted(DateFormatter.ISO8601)
は .iso8601
でもOKだが、iOS9~11の対応を想定して開発したので独自で定義したDateFormatterを呼んで渡してる。
public struct Response<T: Codable> { public let totalCount: Int public let values: [T] init(single data: Data, response: HTTPURLResponse?) throws { let strTotalCount: String = response?.allHeaderFields["Total-Count"] as? String ?? "" self.totalCount = Int(strTotalCount) ?? 0 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(DateFormatter.ISO8601) self.values = [try decoder.decode(T.self, from: data)] } init(unkeyedContainer data: Data, response: HTTPURLResponse?) throws { let strTotalCount: String = response?.allHeaderFields["Total-Count"] as? String ?? "" self.totalCount = Int(strTotalCount) ?? 0 let decoder = JSONDecoder() decoder.dateDecodingStrategy = .formatted(DateFormatter.ISO8601) self.values = try decoder.decode([T].self, from: data) } }
モデル(User: Codable)
そして、レスポンスとなるモデルはスネークケースのキー名のみ、独自で定義すればOKなので簡潔になった。
public struct User: Codable { public let id: String public let name: String? public let profileImageUrl: URL public let followeesCount: Int public let followersCount: Int public let itemsCount: Int public let websiteUrl: String? public let location: String? public let organization: String? public let description: String? private enum CodingKeys: String, CodingKey { case id case name case profileImageUrl = "profile_image_url" case followeesCount = "followees_count" case followersCount = "followers_count" case itemsCount = "items_count" case websiteUrl = "website_url" case location case organization case description } }
GET /api/v2/users/:user_id/followees
最後に、リクエストプロトコル(Requestable)とレスポンス(Response<T: Codable>)とモデル(User: Codable)を組み合わせて、フォローしているユーザーの一覧を返すリクエストは下記のようになった。
このUserFolloweeRequestを見るだけで、どのようなリクエストでレスポンスとして何が取得できるのか、はっきりするのが良いですね。
public struct UserFolloweeRequest: Requestable { public typealias ResponseType = User public static let notFoundText: String = "フォロー中のユーザーがいません" public let endpoint: URL public let path: String = "users/%@/followees" public let method: HttpMethod = .get public let body: [String: Any] = [:] public init(page: Int, perPage: Int, userId: String) { let basePathURL: URL = UserFolloweeRequest.baseURL.appendingPathComponent(String(format: path, userId)) var components: URLComponents? = URLComponents(url: basePathURL, resolvingAgainstBaseURL: true) components?.queryItems = [URLQueryItem(name: "page", value: String(page)), URLQueryItem(name: "per_page", value: String(perPage))] self.endpoint = components?.url ?? basePathURL } // MARK: - ResponseType decode public static func decode(with data: Data, response: HTTPURLResponse?) throws -> Response<User> { return try .init(unkeyedContainer: data, response: response) } }
Codableを使うことで、Swift3のときにResponseTypeに準拠させていたJsonをdecodeするためのプロトコル(JsonDecodableやJsonDecodeError)は不要となり、Codableが全てを処理してくれるようになった。
Codableへの移行が簡単だったのは、GithubKitの設計が良かったからだと思う。
終わりに
まだまだ発見がたくさんあったけども、それはまた別の機会にする。
当初の目的は、iOSDesignPatternSamplesでiOSのデザインパターンを学ぶことだったので。
本家のiOSDesignPatternSamplesはGitHubKitなので、QiitaKitをつかってiOSDesignPatternSamplesの理解を深めていきたい。
ただコードを眺めたり、コピペするだけではやっぱり理解できないですね。
しかし、年内に終えるつもりだったのに間に合わない気がしてたぞ!!!!!!