[Swift]protocol extensionの上手な使い方
InstantiateというSwiftライブラリの内部実装を調査した内容をメモがわりに置いておきます。
Instantiateについて
Instantiate(GitHub)はViewControllerのサブクラスなどが依存するクラスをDI(Dependency Injection)したい時などに、よしなにしてくれる便利なprotocolが実装されています。
参考: DI(Dependency Injection) の解説 - Qiita
例えば、ViewControllerがViewModelを依存させたい!という場面があることとします。
let storyboard = UIStoryboard(name: "ViewController", bundle: Bundle.main)
let vc = storyboard.instantiateInitialViewController() as! ViewController
vc.inject(ViewModel())
上記のようにViewControllerを宣言してからViewModelをなんらかのメソッド(今回はinject
メソッド)で呼び出しても良いですが、これではメソッドの呼び忘れなどのヒューマンエラーが起こりうるのです。
そこで、Instantiateを利用すると下記のように初期化時に保証できるようになります。
let vc = ViewController(with: ViewModel())
なるほど、便利ですね!
かなり簡潔に見えますがこれはどうやって実装されているのでしょうか?
Injectableプロトコル
まず、依存させたいクラス(上記ではViewModel)をどうやってViewControllerに設定するかですが、下記のように特定のprotocolに準拠させるだけです。
StoryboardでViewContorllerのUIを作っていればStoryboardInstantiatable
で、NibであればNibInstantiatable
になります。
また、InstantiateStandard
は同じクラス名のファイルを自動的に検出していい感じにしてくれるのでimportすると良いです。
import Instantiate
import InstantiateStandard
class ViewController: UIViewController {
}
extension ViewController: StoryboardInstantiatable {
func inject(_ dependency: ViewModel) {
viewModel = dependency
}
}
このStoryboardInstantiatable
はinjectable
を継承しています。
また、ViewController初期化時に、内部でStoryboardからViewControllerを生成し、injectメソッドを呼んでくれています。
そして、このinjectメソッドはprotocol extension
としてあらかじめ実装されています。
(わかりやすさのために簡略化しています)
Injectable.swift
protocol Injectable {
associatedtype Dependency = Void
func inject(_ dependency: Dependency)
}
extension Injectable where Dependency == Void {
func inject(_ dependency: Dependency) {
}
}
Storyboard+UIViewController.swift
protocol StoryboardInstantiatable: Injectable, 他のprotocol {
...
}
もともとのInjectableにはDependency = Void
とあり、すでに型が指定されています。
そのまま使うとするならViewModelなどの型を指定できず、Voidが入ってしまうのでは?という疑問が出てきます。
extension ViewController: StoryboardInstantiatable {
func inject(_ dependency: Void) {
}
}
protocolを作ってみる
Playgroundでも用意して実際に試しながら読むとさらに理解が深まるかと思います。
このprotocolの仕様を追うのにまず理解すべきはassociatedtype
についてです。
まずは下記のようにprotocolでassociatedtype
としてDependencyを用いるように定義してみます。(*1)
また、各VCがtypealias
として定義し具体型として実装できます。
protocol AProtocol {
associatedtype Dependency
func call(dependency: Dependency)
}
extension AProtocol where Self: UIViewController {
func call(dependency: Dependency) {
print("AProtocol: \(dependency)")
}
}
class VoidVC: UIViewController, AProtocol {
typealias Dependency = Void
}
VoidVC().call(dependency: ()) // AProtocol: ()
class StringVC: UIViewController, AProtocol {
typealias Dependency = String
}
StringVC().call(dependency: "aaaaa") // AProtocol: aaaa
では、ここでprotocol自身で定義しつつ特定の型を指定してみましょう。(*2)
protocolを継承したclass(ここではVoidVC)では省略可能であることがわかります。
一方、StringVCでは Dependency = String
として上書きできます。
protocol AProtocol {
associatedtype Dependency = Void
func call(dependency: Dependency)
}
class VoidVC: UIViewController, AProtocol {}
class StringVC: UIViewController, AProtocol {
typealias Dependency = String
}
...
上記ではどちらのVCからcallメソッドを呼んでも、( *1 )とおなじ結果が得られます。
ではcallメソッドを実装してみましょう。
class StringVC: UIViewController, AProtocol {
typealiase Dependency = String
func call(dependency: Dependency) {
print("StringVC: \(dependency)")
}
}
StringVC().call(with: "bbbbb") // StringVC: bbbbb
上記を省略した形で下記のように書くこともできます。
class StringVC: UIViewController, AProtocol {
func call(dependency: String) {
print("StringVC: \(dependency)")
}
}
callメソッドの引数に注目するとDependency
と String
と違って見えます。
もうお気づきの方がいるかもしれませんが、callメソッドのdependencyに具体型を定義することで、コンパイラがtypealiase Dependency = String
として勝手に解釈してコンパイルが通るようになります。
これがViewModelなどのように様々な型を依存させることができる仕組みです
まとめ
今回はInstantiate
というライブラリを題材に、内部実装をみていきました。
-
associatedtype
やtypealiase
の使い方 -
protocol extension
について
上記について少しでも理解の一助になれれば幸いです。
Swiftの言語仕様をうまく使っていてとても参考になりますね。👏
こうやってOSSの実装方法を調査してみるととても勉強になるので継続的にやっていけたらと思います。
最後までお読みいただきありがとうございました。
Discussion