MVCしかやってこなかった自分が、半年ほどの間に学んだVIPERの書き方で詰まったところについての記事です。
今までのMVCならこうやって書くけど、VIPERだとどうなるの?って思ったことが中心です。
#VIPERとは
iOSのアーキテクチャの一つです。
VIPERについては先人がいくつもの素晴らしい記事を書かれているので、ここは割愛しますが、VIPERを書く際に自分が参考にしている記事のリンクを貼っておきます。
・iOS Project Architecture : Using VIPER [和訳]
ファイル生成が大変なのでこういうのもあります。
・iOS開発で VIPER / Clean Architecture を使うなら、ファイル自動生成の Generamba もどうぞ
・【iOS】GenerambaのテンプレートエンジンLiquidの書き方
それでは、自分がVIPERでコードを書くに当たってどうすれば良いのかわからなかった部分について見ていきましょう。
#UIAlertController
VIPERでまずぶち当たる問題がこれです。
UIAlertControllerはUIViewControllerのpresentメソッドで出すことから画面遷移としてRouterで処理を書きたくなります。
しかし、そうするとアラートのボタンを押したActionで何かをやりたいときに、Presenterに伝えるのが難しくなる(最悪クロージャーやNotificationで出来なくもないが無理がある。)ので、View側で出してあげると上手く行きます。
class SampleViewController: UIViewController, SampleViewInterface {
var presenter: SamplePresentation!
func showAlert() {
// UIAlertControllerのextensionで追加したメソッドです。
UIAlertController.showOkCancelAlert(self,
title: "Hoge",
message: "Fuga",
okHandler: { self.presenter.didSelectOK() },
cancelHandler: nil)
}
}
ただ、これの問題は、UIViewControllerのVIPERモジュールであれば、ViewがUIViewControllerなので問題なくUIAlertControllerを表示させることが出来ますが、UIViewでモジュールを作成した場合、Viewからアラートを出すことが出来ないので、それを考えると、一概にViewで出すと決めるのは難しいところです。
そういう問題になりそうな場合はUIViewのVIPERモジュールを絶対に作らないと決めて、その代わりにCustomViewを作る際は、UIViewControllerで一旦作成して、viewをaddSubViewする前提で作成すればこのあたりは解決できます。
#画面間のDelegate
画面間のDelegateもなかなか難しい問題です。
Delegateの実体をViewにするかPresenterにするか、実際のdelegateを保持してメソッドを呼び出すのはどこになるかも考えなければなりません。
自分がやった中で一番しっくりくるのはPresenterを実体として渡し、呼び出し元のPresenterで保持する方法です。
例としてFirstモジュールからSecondモジュールにdelegateを渡す処理を挙げてみます。
例
###1.SecondPresenter
SecondモジュールのPresenterにDelegateを定義して、イニシャライズメソッドでDelegateを受け取るようにします。
加えて、呼び出しのメソッドも書きましょう。
protocol SecondDelegate: class {
func hoge()
}
class SecondPresenter: SecondPresentation {
weak var view: SecondViewInterface?
var interactor: SecondUsecase!
var router: SecondWireframe!
private weak var delegate: SecondDelegate?
init(_ delegate: SecondDelegate) {
self.delegate = delegate
}
// Viewから受け取った処理
func didTapHogeButton() {
// デリゲートメソッドを呼ぶ
self.delegate?.hoge()
}
}
###2.SecondRouter
SecondRouterのモジュール生成メソッドの引数にDelegateを追加します。
そして、Presenterの生成時は、1で新たに作成したイニシャライズのメソッドを使用して生成します。
class SecondRouter {
static func assembleModule(_ delegate: SecondDelegate) -> UIViewController {
// UIViewControllerの生成とか色々
let presenter = SecondPresenter(delegate)
return viewController
}
}
###3.FirstRouter
FirstRouterのSecondへの遷移メソッドに引数を追加して、SecondRouterのassembleModuleにそのまま引数を渡すようにします。
class FirstRouter {
weak var viewController: UIViewController?
func showSecond(_ delegate: SecondDelegate) {
let vc = SecondRouter.assembleModule(delegate)
// 遷移処理
}
}
###4.FirstPresenter
FirstPresenterが呼び出すFirstRouterのSecond遷移メソッドの引数にselfを渡します。
加えて、SecondDelegateを実装します。
class FirstPresenter: FistPresentation {
weak var view: FirstViewInterface?
var interactor: FirstUsecase!
var router: FirstWireframe!
func didTapShowSecondButton() {
// デリゲートにselfを渡す
self.router.showSecond(self)
}
}
extension FirstPresenter: SecondDelegate {
func hoge() {
// SecondPresenterから受け取った後の処理を書く
}
}
考え方としては、VIPERにおいて、処理を振り分けるのはPresenterなので、ViewやInteractorやRouterに処理を振り分ける中で、Delegateにも振り分けるよ。という感じがしっくり来たので各モジュール間のDelegateにおいては、遷移元画面のPresenterを遷移先のPresenterに渡すやり方が良いなと思いました。