MVP+AppRootController+Routerやってみた
RootControllerとは
UIWindowのrootViewControllerにコンテナとしてViewControllerを配置し,そのViewControllerの子ViewControllerとして各画面を構成すると色々楽になる(らしい)
リポジトリはこちら
- AppRootControllerのご提案(簡略説明版)
- AppRootControllerを使ってスプラッシュアニメーションを実装する
- RootViewControllerで画面遷移をまとめた話
- iOS:Root Controller Navigation
で詳しく説明されている.
今回は,RedViewController,BlueViewController,GreenViewControllerの3つのViewControllerを用意し,各ViewControllerから遷移することを目標にした.
VC間の移動はRootViewControllerのcurrentプロパティを差し替え,RootViewControllerの子ViewControllerにすることで行っている.
Routerとは
iOSアプリ設計パターン入門によると,Routerの役割は,遷移先の画面を生成し,遷移処理の責務を担うことである.ここでは,各ViewControllerがRouterを持つのではなく,あるViewControllerの遷移に関するメソッドをプロトコルに切り出し,共通のRouterクラスでそれに準拠させるという実装にした.
通知の流れ
MVPアーキテクチャを使い,ViewがうけたユーザーアクションをPresenterに通知し,PresenterからRouterに通知するという流れにした.
ユーザーがボタンを押す.
↓
ボタンにaddTarget
した関数が実行される
@objc func didTapTransitionToBlueButton() {
guard let presenter = presenter as? RedPresenterProtocol else { fatalError() }
presenter.didTransitionButtonTapped(to: .blue)
}
↓
presenterのメソッドが呼ばれる
func didTransitionButtonTapped(to color: RedViewController.NextRoute) {
router.transition(to: color)
}
↓
Routerのメソッドが呼ばれる
func transition(to color: RedViewController.NextRoute) {
switch color {
case .red:
transitionToRed()
case .blue:
transitionToBlue()
case .green:
transitionToGreen()
}
}
具体的な実装
PresenterInjectableプロトコル
各ViewControllerはPresenterを持つ.後述するRouterの中でViewControllerを差し替えるための抽象的な関数を作成するために,PresenterInjectable
プロトコルを定義し,各ViewControllerに準拠させる.
protocol PresenterInjectable: UIViewController {
var presenter: ColorPresenter? { get }
func inject(presenter: ColorPresenter)
}
ViewController
ボタンを定義して配置したり,ボタンが押されたときの通知の設定を行っている.
class RedViewController: UIViewController, PresenterInjectable {
var presenter: ColorPresenter?
func inject(presenter: ColorPresenter) {
self.presenter = presenter
}
enum NextRoute {
case red
case blue
case green
}
let transitionToRedButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("toRed", for: .normal)
button.addTarget(self, action: #selector(didTapTransitionToRedButton), for: .touchUpInside)
return button
}()
let transitionToBlueButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("toBlue", for: .normal)
button.addTarget(self, action: #selector(didTapTransitionToBlueButton), for: .touchUpInside)
return button
}()
let transitionToGreenButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("toGreen", for: .normal)
button.addTarget(self, action: #selector(didTapTransitionToGreenButton), for: .touchUpInside)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .red
setup()
}
private func setup() {
setupTransitionToRedBuuton()
setupTransitionToBlueButton()
setupTransitionToGreenButton()
}
private func setupTransitionToRedBuuton() {
view.addSubview(transitionToRedButton)
transitionToRedButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
transitionToRedButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
transitionToRedButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
private func setupTransitionToBlueButton() {
view.addSubview(transitionToBlueButton)
transitionToBlueButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
transitionToBlueButton.leadingAnchor.constraint(equalTo: view.leadingAnchor),
transitionToBlueButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
private func setupTransitionToGreenButton() {
view.addSubview(transitionToGreenButton)
transitionToGreenButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
transitionToGreenButton.trailingAnchor.constraint(equalTo: view.trailingAnchor),
transitionToGreenButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
@objc func didTapTransitionToRedButton() {
guard let presenter = presenter as? RedPresenterProtocol else { fatalError()
}
presenter.didTransitionButtonTapped(to: .red)
}
@objc func didTapTransitionToBlueButton() {
guard let presenter = presenter as? RedPresenterProtocol else { fatalError() }
presenter.didTransitionButtonTapped(to: .blue)
}
@objc func didTapTransitionToGreenButton() {
guard let presenter = presenter as? RedPresenterProtocol else { fatalError() }
presenter.didTransitionButtonTapped(to: .green)
}
}
ColorPresenterプロトコル
先ほど触れたViewControllerを差し替えるための抽象的な関数の作成には,ViewControllerの抽象化だけでなくPresenterの抽象化を必要となる.ColorPresenter
プロトコルを定義し,各Presenterに準拠させる.
protocol ColorPresenter {}
protocol RedPresenterProtocol: ColorPresenter {
func didTransitionButtonTapped(to color: RedViewController.NextRoute)
}
protocol BluePresenterProtocol: ColorPresenter {
func didTransitionButtonTapped(to color: BlueViewController.NextRoute)
}
didTransitionButtonTapped()
をColorPresenter
で定義しようか迷ったが,そのためには引数のViewControllerを抽象化する必要があり,その抽象化にはあまり意味がない(サンプルアプリだからできる抽象化だと感じた)と思ったのでやめた.
Presenterクラス
Viewクラスを弱参照で持たせた.
final class RedPresenter {
private(set) weak var view: PresenterInjectable!
private let router: RedRouterProtocol
init(view: PresenterInjectable) {
print("RedPresenter is initialized")
self.view = view
self.router = AppDelegate.shared.router
}
}
extension RedPresenter: RedPresenterProtocol {
func didTransitionButtonTapped(to color: RedViewController.NextRoute) {
router.transition(to: color)
}
}
Routerクラス
古いViewControllerをremoveし,新しいViewControllerを生成しPresenterを設定し,addChildするのはreplace(with viewController: PresenterInjectable, presenter: ColorPresenter)
が行っている.この関数を作るためにPresenterInjectable
プロトコルとColorPresenter
プロトコルを定義した.
class Router {
func transitionToBlue() {
let blueViewController = BlueViewController()
let bluePresenter = BluePresenter(view: blueViewController)
replace(with: blueViewController, presenter: bluePresenter)
}
func transitionToRed() {
let redViewController = RedViewController()
let redPresenter = RedPresenter(view: redViewController)
replace(with: redViewController, presenter: redPresenter)
}
func transitionToGreen() {
let greenViewController = GreenViewController()
let greenPresenter = GreenPresenter(view: greenViewController)
replace(with: greenViewController, presenter: greenPresenter)
}
func replace(with viewController: PresenterInjectable, presenter: ColorPresenter) {
let rootViewController = AppDelegate.shared.rootViewController
viewController.inject(presenter: presenter)
rootViewController.current.willMove(toParent: nil)
rootViewController.current.removeFromParent()
rootViewController.current.view.removeFromSuperview()
rootViewController.current = viewController
rootViewController.addChild(rootViewController.current)
rootViewController.current.view.frame = rootViewController.view.bounds
rootViewController.view.addSubview(rootViewController.current.view)
rootViewController.current.didMove(toParent: rootViewController)
}
}
各RouterProtocolに準拠させている.ViewControllerが列挙型NextRoute
を持たせることで,3つの遷移先に対して一つのRouterメソッドで済んでいる.
extension Router: RedRouterProtocol {
func transition(to color: RedViewController.NextRoute) {
switch color {
case .red:
transitionToRed()
case .blue:
transitionToBlue()
case .green:
transitionToGreen()
}
}
}
Discussion