使いどころがよくわかってなかったけど気になっていたSwiftのassociatedtype
個人開発アプリでいい感じに使えたのでここに書いておきます。
associatedtypeとは
- protocolに定義する連想型です
- protocolの準拠時に、具体的な型を指定します(または型推論で指定されます)
- ジェネリクスにおける
T
的なやつです
使いどころ
上記の通りなのですが、protocol定義時点では決められず、準拠側で指定したい型があるときが使いどころです。
具体的には、**APIを叩いて、レスポンスに含まれるJSONから特定の型を作りたい!**ってときに使えました。
具体例
以下、AlamofireとQiita APIをサンプルに使った例です。
やりたいこと
- Qiita APIを使ってユーザー情報を取得したい( https://qiita.com/api/v2/docs#get-apiv2usersuser_id )
- リクエストメソッドのクロージャ内で、レスポンスのJSONをデコードした型として扱いたい
これを単純に書くとこんな感じになります。
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が使えそうです。
protocol APIConfigure {
// APIの呼び出し先
static var path: String { get }
// リクエストメソッド(※クロージャは後述)
static func request()
}
呼び出し先は準拠する側で決めるとして、リクエスト処理は共通なのでprotoocol extensionで実装します。
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型に指定する型です。
protocol APIConfigure {
associatedtype ResponseEntity // 👈追加
// APIの呼び出し先
static var path: String { get }
// リクエストメソッド(※クロージャは後述)
static func request()
}
protocol extension側を修正していきます。
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から変換可能な型なのかどうかがわからないのです。
そこで型制約の追加です。
protocol APIConfigure {
// 👇型制約の追加
associatedtype ResponseEntity: Codable
// APIの呼び出し先
static var path: String { get }
// リクエストメソッド(※クロージャは後述)
static func request()
}
これでResponseEntityはJSONから変換可能な型(=Codable)であると制約をかけられました。
そしてクロージャへ
リクエスト処理にクロージャを追加して、呼び出し元でJSONをデコードした型として扱えるようにします。
protocol APIConfigure {
associatedtype ResponseEntity: Codable
// APIの呼び出し先
static var path: String { get }
// リクエストメソッド 👇クロージャを追加
static func request(completion: ((ResponseEntity) -> ())?)
}
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に準拠させます。
struct UserGet: APIConfigure {
typealias ResponseEntity = User // 👈ここで具体的な型を指定
static let path = "https://qiita.com/api/v2/users/akeome"
}
protocolで定義したassociatedtypeを準拠側で明示的に指定するにはtypealias
を使います。
これでUserGetを使ったリクエスト処理のクロージャで受け取る型はUser型に指定できました。
APIを呼び出す共通的なstructを作ってまとめていくことが考えられます。
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使ってみた結果
func hoge() {
APIClient.UserGet.request(completion: { user in
print(user.id) // akeome
print(user.userDescription) // ぁけぉめです。以下略
})
}
こんな感じで、リクエスト処理のクロージャで受け取る型を自動的にUserにできました。
とてもすっきりしたのではないでしょうか。
今後ユーザー情報取得だけでなく記事一覧も取得したくなっても
APIClient.GetItems.request(〜
と書くだけです。
比較のため、冒頭に記載したassociatedtypeを使わないパターンも再掲しておきます。
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