[Swift]GitHub検索プログラムをprotocol, associatedTypesで改良する


先日、Swiftの基本的な内容を記した本「改訂新版 Swift実践入門(石川洋資・西山勇世、技術評論社、2018)」に従って、GitHubのリポジトリの検索プログラムを実装しました。

GitHub検索用APIには豊富な検索オプションが用意されているのですが、この本ではユーザーとリポジトリの検索のみ記載されていたのでとりあえずそれに従って実装しました。

検索結果を表すために、ジェネリクスを利用したSearchResponse構造体を実装しています。

SearchResponse.swift
struct SearchResponse<Item: Decodable>: Decodable {
    let totalCount: Int
    let items: [Item]//構造体User, 構造体Repositoryを収められる

    //中略
}

この構造体は、SearchRepositories, SearchUsersという、APIへのリクエストを表す構造体にそれぞれassociatedTypesを用いて関連づけられています。

GitHubAPI.swift
final class GitHubAPI {

    struct SearchRepositories: GitHubRequest {
        //中略

        //リクエストに、対応するレスポンスを関連付けしている
        typealias Response = SearchResponse<Repository>

        // The word you would like to search with.
        let keyword: String
    }

    struct SearchUsers: GitHubRequest {
        //中略

        //リクエストに、対応するレスポンスを関連付けしている
        typealias Response = SearchResponse<User>

        let keyWord: String
    }

}

GitHubRequest.swift
protocol GitHubRequest {
    associatedtype Response: Decodable
    //後略
}

associatedTypesとは、swiftのprotocolに任意の型を関連づけられるというものです。protocol版のジェネリクスのようなものと大雑把に捉えています(認識がおかしかったら教えてください)。

ところが本書の例では、プログラムのエントリポイントとなるmain.swiftで、SearchReponse<Repository>を使ってリポジトリは検索できるようになっているのに、せっかくのSearchReponse<User>は使っておらず、ユーザーでは検索できないようになっていたのです👇

main.swift
//中略
//SearchRepository構造体を用いてリクエストを作成
let request = GitHubAPI.SearchRepositories(keyword: "\(keyWord)")

//リクエストをサーバへ送る
client.send(request: request, completion: {(result) in
    switch result {
    case let .success(response):
        response.items.forEach {print($0)}
        exit(0)
    case let .failure(error):
        print(error)
        exit(1)
    }
})
//中略

せっかくユーザーで検索する仕組みを用意しているのに使わないではもったいない。
そこで、ユーザかリポジトリかどちらか選んで検索できるようにしようと実装してみました。

具体的には、SearchReponse<Repository>SearchReponse<User>もどちらも渡して処理できるsendClient<T: GitHubRequest>(request: T)関数を実装しようとしました。

しかしコンパイルエラーが発生しました。

main.swift
func sendClient<T: GitHubRequest>(request: T) {

    let client = GitHubClient()

    client.send(request: request, completion: {(result) in
        switch result {
        case let .success(response):
            response.items.forEach {print($0)} //Error: Value of type 'T.Response' has no member 'items'
            exit(0)
        case let .failure(error):
            print(error)   
            exit(1)
        }
    })   
}

は?

どうやら他の箇所にも手を加えないと実現できないようです。解決策を考えてみました。

エラー内容はValue of type 'T.Response' has no member 'items'

原因は、GitHubRequestプロトコルにて、associatedtype Response: Decodableとのみ規定していた事、つまりコンパイラからはResponseという型に対する制約は「Decodable である」事以外に規定されていなかった、という事でした。

言い換えれば、Responseという型がitemsという変数を持つという制約は今の所どこにもないため、コンパイラからはそのことが分からないのです。だからエラーが発生したのです。

まずは、Responseという型に対して「Decodableである」のみならず「itemsという変数を持つ」という制約も付け加える必要があります。そのため、新しいプロトコルAbstructResponseを作成し、Response型をこれに準拠させることにしました。

SearchResponse.swift
//新たなプロトコルに準拠させる
struct SearchResponse<Item: Decodable>: AbstructResponse {
    typealias Item = Item

    let totalCount: Int
    let items: [Item]

      //中略
    }
}

//新たなプロトコルを定義
protocol AbstructResponse: Decodable {
    associatedtype Item: Decodable

    var totalCount: Int {get}
    var items: [Item] {get} //itemsという変数を持つという制約が加わる
}

はい、このようにするとコンパイラはResponse型を見ただけで、それがitemsという変数を持つことが分かるようになり、エラーが解消します。あとは、変更点に合わせて他の部分も変更するだけです。

全ての変更点をまとめると下記のようになります。

GitHubRequest.swift

SearchResponse.swift

main.swift




コードをコピペなどしたい場合はこちら参照: https://gitlab.com/Satoru_Aikawa/githubsearchproject2/commit/6591bce7dfca238e47afc658a3e3b4e7e6bbd1c0

参考文献・ウェブサイト

www.amazon.co.jp/dp/477419414X