BaseViewControllerの肥大化
ある特定の画面への遷移など、共通の処理を書きたいことはしばしばあります。
下の例はMyViewControllerA
及びMyViewControllerB
のどちらからもMyViewControllerC
へ遷移したい時のコードです。
class MyViewControllerA: UIViewController {
// ...省略...
func showViewControllerC() {
navigationController?.pushViewController(MyViewControllerC(), animated: true)
}
// ...省略...
}
class MyViewControllerB: UIViewController {
// ...省略...
func showViewControllerC() {
navigationController?.pushViewController(MyViewControllerC(), animated: true)
}
// ...省略...
}
class MyViewControllerC: UIViewController {
// ...省略...
}
MyViewControllerA
及びMyViewControllerB
のどちらにも実装されているshowViewControllerC
メソッドを共通化したい時、一つの選択肢としてBaseViewController(共通のスーパークラス)を作る方法があります。
class MyBaseViewController: UIViewController {
// ...省略...
func showViewControllerC() {
navigationController?.pushViewController(MyViewControllerC(), animated: true)
}
// ...省略...
}
class MyViewControllerA: MyBaseViewController {
// ...省略...
}
class MyViewControllerB: MyBaseViewController {
// ...省略...
}
class MyViewControllerC: UIViewController {
// ...省略...
}
これでこのコードはすっきりして悪くはないのですが、共通の処理をMyBaseViewController
に記述しすぎて肥大化するということがしばしば起こります。
また、意外とはまりがちなのが、UITableViewController
やUICollectionViewController
のサブクラスにも同じ処理を実装したくなった場合です。
BaseViewController方式には、こういったところでいつか限界点が訪れることになるでしょう。
もう一つの方法としては、
extension UIViewController {
// ...ここをどんどん拡張していく...
func showViewControllerC() {
navigationController?.pushViewController(MyViewControllerC(), animated: true)
}
}
のような実装方法も考えられます。が、アプリ固有のコードをextension
でどんどん拡張していくのもあまり良い経験がありません。
ProtocolExtensionを使う
上記の問題はProtocolExtensionでMixinを実装することで解決できます。
一般的にMixinというと、インターフェースを定義してそれぞれのクラスが実装を担保するという形が多いと思いますが、ProtocolExtensionで行えるMixinはもっともっと強力です。
プロトコルに実装を記述することができるので、あるプロトコルを採用するだけで実装を組み込むことができてしまうのです。
下記のコードを見てみましょう。
protocol TransisionProtocol {
func showViewControllerC()
}
extension TransisionProtocol where Self: UIViewController {
func showViewControllerC() {
navigationController?.pushViewController(MyViewControllerC(), animated: true)
}
}
class MyViewControllerA: UIViewController {
// ...省略...
}
class MyViewControllerB: UIViewController {
// ...省略...
}
class MyViewControllerC: UIViewController {
// ...省略...
}
// Mixin
extension MyViewControllerA: TransisionProtocol {}
extension MyViewControllerB: TransisionProtocol {}
解説
TransisionProtocol
インターフェースを宣言します。
protocol TransisionProtocol {
func showViewControllerC()
}
TransisionProtocol
に実装を定義します。
extension TransisionProtocol where Self: UIViewController {
func showViewControllerC() {
self.navigationController?.pushViewController(MyViewControllerC(), animated: true)
}
}
ここで重要なのが、where Self: UIViewController
の部分です。
SelfがUIViewControllerを継承していることが保証されるので、実装側で、selfがあたかもUIViewControllerであるとして記述することができます。
あとは、各ViewControllerはTransisionProtocol
を採用するだけです。
// Mixin
extension MyViewControllerA: TransisionProtocol {}
extension MyViewControllerB: TransisionProtocol {}
これで各ViewControllerにshowViewControllerC
の実装を組み込むことができます。
コツ
目的ごとにプロトコルを宣言することで特定のクラスやプロトコルが肥大化するのを抑えることができるでしょう。
そうすれば、どのViewControllerがどのProtocolを採用しているかを見ればやりたいことが一目瞭然になります。
デメリット
ここまで読めばメリットはなんとなくわかると思います。
ただ、この方法にはメリットだけではなく、必ず抑えて置かなければいけないデメリットも存在します。
変なところではまらないために、このデメリットの方を強調したいです。
- クラス側でプロトコルの実装を上書きしても気づけない
- 名前が被らないように気を配る必要がある
などなどありますが、前者が一番大きなデメリットだと思います。
なぜかというと、コンパイルエラーにならないからです!
下記の例を見てみましょう。
protocol TransisionProtocol {
func showViewControllerC()
func pop() // 追加
}
extension TransisionProtocol where Self: UIViewController {
func showViewControllerC() {
navigationController?.pushViewController(MyViewControllerC(), animated: true)
}
// 戻る画面遷移
func pop() {
navigationController?.popViewControllerAnimated(true)
print("TransisionProtocol pop")
}
}
class MyViewControllerA: UIViewController {
// ...省略...
// 実装を上書き
func pop() {
// なにかpop的な別の処理
print("MyViewControllerA pop")
}
}
class MyViewControllerB: UIViewController {
// ...省略...
}
class MyViewControllerC: UIViewController {
// ...省略...
}
// Mixin
extension MyViewControllerA: TransisionProtocol {}
extension MyViewControllerB: TransisionProtocol {}
TransisionProtocol
にpop
という、画面を戻すためのメソッドを追加してみました。
この状態でもし仮に、MyViewControllerA
にpop
というpop的動作の何かを実行するメソッドを追加したとします。
継承の時のようにoverride
をつけなくてもpop
の実装を上書きしてしまうことができるため、この状態でpop
を実行すると上書きされた方が実行されることになります。
myViewControllerA.pop() // 出力: MyViewControllerA pop
myViewControllerB.pop() // 出力: TransisionProtocol pop
このように無意識のうちに実装を上書きしてしまい、予期せぬ動作になることはバグの原因です。
プロトコル側のメソッドにあまりにもシンプルな名前を付け過ぎるとこのようなことが起きやすくなるでしょう。
最後に
この方法を実際のプロダクトに採用し始めてから日が浅いので、もっと別のデメリットに遭遇することがあるかもしれません。
また、他にもデメリットがあるという方がいらっしゃいましたらご意見いただけるとありがたいです!