163
105

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Swift】associatedtypeの使いどころ

Posted at

使いどころがよくわかってなかったけど気になっていたSwiftのassociatedtype
個人開発アプリでいい感じに使えたのでここに書いておきます。

associatedtypeとは

  • protocolに定義する連想型です
  • protocolの準拠時に、具体的な型を指定します(または型推論で指定されます)
  • ジェネリクスにおけるT的なやつです

使いどころ

上記の通りなのですが、protocol定義時点では決められず、準拠側で指定したい型があるときが使いどころです。
具体的には、**APIを叩いて、レスポンスに含まれるJSONから特定の型を作りたい!**ってときに使えました。

具体例

以下、AlamofireとQiita APIをサンプルに使った例です。

やりたいこと

これを単純に書くとこんな感じになります。

protocol、associatedtypeを使わないパターン.swift
func hoge() {
    Alamofire
        .request("https://qiita.com/api/v2/users/akeome")
        .response(completionHandler: { response in
            guard
                let data = response.data,
                let user = try? JSONDecoder().decode(User.self, from: data) else {
                    return
            }
    
        print(user.id)  // akeome
        print(user.userDescription)  // ぁけぉめです。以下略
    })
}

なお、ここでのUser型はJSONを元に自動生成1したCodableなstructです。

もっとよくできないかしら

上記のような処理を繰り返し書くことがあれば、こんな不満と願望が湧いてきます

  • この長い処理を何度も書きたくない → リクエストメソッド自体は共通化したい
  • 処理を呼ぶたびにURLとレスポンスJSONの型をいちいち書きたくない → APIの呼び出し先を指定するだけでJSONをデコードする型も決まってほしい

こんな願いを叶えてくれる機能がSwiftにはちゃんと備わっているのです。
それがprotocol、そしてassociatedtypeです。

protocolにしてみよう

まずはAPIを呼び出せるprotocolを考えましょう。
リクエストメソッド自体は共通化しつつAPIの呼び出し先を変えるには、protocolが使えそうです。

APIConfigure.swift
protocol APIConfigure {
    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド(※クロージャは後述)
    static func request()
}

呼び出し先は準拠する側で決めるとして、リクエスト処理は共通なのでprotoocol extensionで実装します。

APIConfigure.swift
extension APIConfigure {
    static func request() {
        Alamofire
            .request(Self.path)
            .response(completionHandler: { response in
                guard
                    let data = response.data,
                    //                                  👇👇👇👇
                    let xxx = try? JSONDecoder().decode(User.self, from: data) else {
                    return
                }
            })
    }
}

さてここで困ったことがあります。
JSONDecoder().decode()の第一引数に渡す型を指定しなければならないのです!

共通化したい!でも具体的な型はまだ決められん!準拠側で指定したいんや!
そんなときこそassociatedtypeの出番です。

そう、associatedtypeならね

associatedtypeの定義

protocol本体の実装に戻って、「今はまだ決められないけど準拠時に決めてね」という型を定義します。
今回の例の場合、準拠時にUser型に指定する型です。

APIConfigure.swift
protocol APIConfigure {
    associatedtype ResponseEntity  // 👈追加

    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド(※クロージャは後述)
    static func request()
}

protocol extension側を修正していきます。

APIConfigure.swift
extension APIConfigure {
    static func request() {
        Alamofire
            .request(Self.path)
            .response(completionHandler: { response in
                guard
                    let data = response.data,
                    //                                            👇associatedtype
                    let responseEntity = try? JSONDecoder().decode(ResponseEntity.self, from: data) else {
                    return
                }
            })
    }
}

型制約の追加

このままでは以下のエラーになります。

Instance method 'decode(_:from:)' requires that 'Self.ResponseEntity' conform to 'Decodable'`

JSONDecoder().decode()の第一引数に渡す型はDecodableに準拠している必要があるのです。
このままでは、ResponseEntityがJSONから変換可能な型なのかどうかがわからないのです。

そこで型制約の追加です。

APIConfigure.swift
protocol APIConfigure {
    //                            👇型制約の追加
    associatedtype ResponseEntity: Codable

    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド(※クロージャは後述)
    static func request()
}

これでResponseEntityはJSONから変換可能な型(=Codable)であると制約をかけられました。

そしてクロージャへ

リクエスト処理にクロージャを追加して、呼び出し元でJSONをデコードした型として扱えるようにします。

APIConfigure.swift
protocol APIConfigure {
    associatedtype ResponseEntity: Codable

    // APIの呼び出し先
    static var path: String { get }
    // リクエストメソッド               👇クロージャを追加
    static func request(completion: ((ResponseEntity) -> ())?)
}
APIConfigure.swift
extension APIConfigure {
    static func request(completion: ((ResponseEntity) -> ())?) {
        Alamofire
            .request(Self.path)
            .response(completionHandler: { response in
                guard
                    let data = response.data,
                    let entity = try? JSONDecoder().decode(ResponseEntity.self, from: data) else {
                    return
                }
                
                completion?(entity)
            })
    }
}

ここまでで、悲願の「リクエストメソッドの共通化」「APIの呼び出し先を指定するだけでJSONをデコードする型の確定」を満たせそうなprotocolができあがりました。

準拠側

Qiita APIのユーザー情報取得を扱うstructを作り、protocolに準拠させます。

UserGet.swift
struct UserGet: APIConfigure {
    typealias ResponseEntity = User  // 👈ここで具体的な型を指定
    static let path = "https://qiita.com/api/v2/users/akeome"
}

protocolで定義したassociatedtypeを準拠側で明示的に指定するにはtypealiasを使います。
これでUserGetを使ったリクエスト処理のクロージャで受け取る型はUser型に指定できました。

APIを呼び出す共通的なstructを作ってまとめていくことが考えられます。

APIClient.swift
struct APIClient {
    // ユーザー情報取得
    struct UserGet: APIConfigure {
        typealias ResponseEntity = User
        static let path = "https://qiita.com/api/v2/users/akeome"
    }

    // 記事一覧取得
    struct ItemsGet: APIConfigure {
        typealias ResponseEntity = [Item]
        static let path = "https://qiita.com/api/v2/items"
    }

    // その他いろいろ
}

associatedtype使ってみた結果

protocol、associatedtypeを使ったパターン.swift
func hoge() {
    APIClient.UserGet.request(completion: { user in
        print(user.id)  // akeome
        print(user.userDescription)  // ぁけぉめです。以下略
    })
}

こんな感じで、リクエスト処理のクロージャで受け取る型を自動的にUserにできました。
とてもすっきりしたのではないでしょうか。

今後ユーザー情報取得だけでなく記事一覧も取得したくなっても
APIClient.GetItems.request(〜と書くだけです。

比較のため、冒頭に記載したassociatedtypeを使わないパターンも再掲しておきます。

protocol、associatedtypeを使わないパターン.swift
func hoge() {
    Alamofire
        .request("https://qiita.com/api/v2/users/akeome")
        .response(completionHandler: { response in
            guard
                let data = response.data,
                let user = try? JSONDecoder().decode(User.self, from: data) else {
                    return
            }
    
        print(user.id)  // akeome
        print(user.userDescription)  // ぁけぉめです。以下略
    })
}

まとめ

今回は割愛しましたが、

  • protocolにHTTPMethodを持たせてGETやPOSTに対応する
  • protocolにパラメーターを持たせる
  • protocol extensionでベースURLを定義する
  • リクエスト処理のエラーに対応する

などすれば、より実用的なコードになるかと思います。

associatedtypeについての記事がなかなか見つからなかったので参考になる方がいらっしゃれば幸いです。

おまけ

associatedtypeを使ってこんなアプリを作ってます。
CardPort - App Store

  1. https://app.quicktype.io/

163
105
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
163
105

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?