今更だけどSwiftの型消去の使い所が少し分かったので備忘録
背景
2016年のtry!Swiftで話題になった型消去
なかなか使い所が分かってなかったんだけど、利用したいシーンに出会ったので備忘録を記す
説明
前提
私は、開発する上で、よくHTTPクライアントのライブラリであるAPIKitを使っています。
APIKitの使い方を簡単に説明すると、Request
というプロトコルに準拠したクラスを書いて、APIKitのSession
クラスのsend
メソッドの引数に指定することで、指定したエンドポイントにアクセスして結果を返してくれます。
このAPIKitのRequest
プロトコルは、エンドポイントにアクセスして取得するレスポンスの型をassociatedtype
を使って、Request
プロトコルに準拠したクラスで指定できるようになっています。
public protocol Request { /// The response type associated with the request type. associatedtype Response //その他、HttpMethodやpathやパラメータなどのプロパティがある //.... }
下記は、あるAPIを叩いて成功した場合にStringの配列を返すクラスです。
public class HogeEndpoint: Request { typealias Response = [String] //... }
実際に使う場合は下記のような感じになります
let request = HogeEndpoint() Session(request) { result in switch result { case .success(let items): //Stringの配列を取得 case .failure(let error): //Error処理 } }
型消去を使って解決した課題
APIKitの説明を踏まえた上で、Request
プロトコルに準拠したクラスで、APIのエンドポイントは違うけれども成功した時に返ってくる値の型は同じであるパターンを想定してみます。
例えば下記のようなHogeEndpoint
とFugaEndpoint
クラスです。
class HogeEndpoint: Request { typealias Response = [String] var path: String = "/api/v1/hoge" //... } class FugaEndpoint: Request { typealias Response = [String] var path: String = "/api/v1/fuga" //... }
これは、接続するエンドポイントは違いますが、結果として得られる値の型は同じです。
そこでString型の配列をリストで表示する画面を作ることを想定してみましょう。
リスト表示のViewControllerにRequest
プロトコルのStored propertyを宣言してやれば、ViewControllerが汎用的に使えると思い下記のようなコードを書きました。
import UIKit import APIKit class StringListTableViewController: UITableViewController { var request: Request! private var items: [String]? { didSet { tableView.reloadData() } } override func viewDidLoad() { super.viewDidLoad() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier") fetchItem() } private func fetchItem() { Session.send(request) { [weak self] result in switch result { case .success(let items): self?.items = items case .failure(let error): print(error) } } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) cell.textLabel?.text = items?[indexPath.row] return cell } }
これで色々な画面から、String配列のリスト表示画面であるStringListTableViewController
のStored propertyであるrequest
に値を渡すだけで汎用的に使えると思うかもしれません。
しかし、これではProtocol 'Request' can only be used as a generic constraint because it has Self or associated type requirements.
エラーになります。
Request
プロトコルのassociatedtype
の型が決まってないのでダメよってことですね。
当然といえば当然です。
ならばRequest
プロトコルにジェネリクスを使って下記のようにすればと思いますが残念ながらSwiftはプロトコルでのジェネリクスをサポートしていません。
public protocol Request<Result> { /// The response type associated with the request type. associatedtype Response //その他、HttpMethodやpathやパラメータなどのプロパティがある //.... } extension Request { typealias Response = Result }
Protocols do not allow generic parameters; use associated types instead
では、こんな時はどうすれば良いでしょうか?
結局、Request
プロトコルのassociatedtype
に引きづられて、同じようなViewを作るしかないのでしょうか?
いや待ってください。そうです。こんな時に型消去です。
型消去
(型消去自体の説明は沢山良記事があるので割愛します)
このエラーを回避してViewControllerを汎用的に使い回すために下記のような型消去クラスを作成します
struct TypeEraseEndpoint<ResponseType>: Request { typealias Response = ResponseType var path: String var method: HTTPMethod private let responseHandler: ((Any, HTTPURLResponse) throws -> ResponseType) init<R: Request>(request: R) where R.Response == ResponseType { self.path = request.path self.queryParameters = request.queryParameters self.method = request.method self.dataParser = request.dataParser self.responseHandler = request.response } func response(from object: Any, urlResponse: HTTPURLResponse) throws -> ResponseType { return try responseHandler(object, urlResponse) } }
このような型消去のクラスを使うことで、先ほどのStringListTableViewController
のStored propertyであるrequest
の型をTypeEraseEndpoint<[String]>!
に変更することで、ViewControllerを汎用的に使うことができました。
import UIKit import APIKit class StringListTableViewController: UITableViewController { var request: TypeEraseEndpoint<[String]>! private var items: [String]? { didSet { tableView.reloadData() } } override func viewDidLoad() { super.viewDidLoad() tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier") fetchItem() } private func fetchItem() { Session.send(request) { [weak self] result in switch result { case .success(let items): self?.items = items case .failure(let error): print(error) } } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items?.count ?? 0 } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) cell.textLabel?.text = items?[indexPath.row] return cell } }
この型消去のサンプルプロジェクトをQiitaのAPIを使って作ってみました。
もしよければ触ってみてください。
ただし、TypeEraseのジェネリクスにProtocol型を指定して柔軟にとかは出来ない・・
最後に
そもそもCleanArchitectureみたいなレイヤードアーキテクチャを意識して作れば、このような場面には出くわさないんですけどね。
ViewControllerにRequestプロトコル渡すような書き方するなよとか言わないで!!
とはいえ、ほんと今更の理解で凹む。。
普段からassociatedType
のProtocolを書いていないという設計力の低さを露呈しているような気がする。