GithubKitForSampleを参考にしてQiitaKitForSampleを作ってみた

これはなに

この記事はiOS2 Advent Calendar 2017の5日目の記事です。

今年のiOSDCで注目を集めた iOSDesignPatternSamples を参考にしてiOSデザインパターンを勉強してる。このiOSDesignPatternSamplesは、GithubKit というライブラリを使って開発されている。

今回は、このGithubKitが面白かったので自分も真似して作ってみたという話。しかし、ただ真似するだけではコピペになるし学びが薄いので、APIをQiitaに変更して開発してみた。

github.com

github.com

先に作った感想

結論から先に言うと、とても勉強になったし、@marty-suzukiさんには感謝しかない。

具体的に良かったのは、「SwiftやiOSの新機能をさっと試すのに適当なサイズのサンプルアプリが欲しいとき、◯◯Kitというライブラリをフレームワーク化しておくとサンプルアプリを作りやすい」 ことに気づけたところ。

今までは個人で開発したアプリで新機能を試してたが、一度リリースしてしまうと既存ユーザーへの影響やデザインパターンとの調整を考えて、新機能を試すのが面倒になり何もしないことが多かった。

かといって、ゼロからサンプル用のアプリを作るのも面倒で、サンプルに適した形にするのもまた面倒だ。

結局、なにも試してない状態になってた。要するに、サボってた。

でも、この〇〇Kitとかを作るとcarthageでプロジェクトに取り込んでささっと適当なサイズのアプリにして新機能を試せていいかなと思い始めてる。

もちろん、試したい機能によって下準備とか別に必要だけど、それもフレームワーク化して 「新機能をすぐに試せる環境作りをしておくのが大切なんだ」 と実感した。

QiitaKitForSampleについて

github.com

本題に入る。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の理解を深めていきたい。

ただコードを眺めたり、コピペするだけではやっぱり理解できないですね。

しかし、年内に終えるつもりだったのに間に合わない気がしてたぞ!!!!!!