MVP+AppRootController+Routerやってみた

## RootControllerとは
UIWindowのrootViewControllerにコンテナとしてViewControllerを配置し，そのViewControllerの子ViewControllerとして各画面を構成すると色々楽になる(らしい)

リポジトリは[こちら](https://github.com/Hyperbolic4183/MVP-RootController-Router)

- [AppRootControllerのご提案(簡略説明版)](https://speakerdeck.com/yimajo/approotcontrollerfalsegoti-an-jian-lue-shuo-ming-ban)
- [AppRootControllerを使ってスプラッシュアニメーションを実装する](https://speakerdeck.com/yimajo/approotcontroller-woshi-tutesupuratusiyuanimesiyonwoshi-zhuang-suru-2017-dong)
- [RootViewControllerで画面遷移をまとめた話](https://speakerdeck.com/satotakeshi/rootviewcontrollerdehua-mian-qian-yi-womatometahua)
- [iOS:Root Controller Navigation](https://stasost.medium.com/ios-root-controller-navigation-3625eedbbff)
  
で詳しく説明されている．
今回は，RedViewController,BlueViewController,GreenViewControllerの3つのViewControllerを用意し，各ViewControllerから遷移することを目標にした．
![](https://storage.googleapis.com/zenn-user-upload/4f9ed0d23738-20220226.jpg)

VC間の移動はRootViewControllerのcurrentプロパティを差し替え，RootViewControllerの子ViewControllerにすることで行っている．
## Routerとは
[iOSアプリ設計パターン入門](https://peaks.cc/books/iOS_architecture)によると，Routerの役割は，**遷移先の画面を生成し，遷移処理の責務を担うこと**である．ここでは，各ViewControllerがRouterを持つのではなく，あるViewControllerの遷移に関するメソッドをプロトコルに切り出し，共通のRouterクラスでそれに準拠させるという実装にした．

## 通知の流れ
MVPアーキテクチャを使い，ViewがうけたユーザーアクションをPresenterに通知し，PresenterからRouterに通知するという流れにした．
![](https://storage.googleapis.com/zenn-user-upload/52a23b1690a1-20220226.jpg)


ユーザーがボタンを押す．
↓
ボタンに`addTarget`した関数が実行される

```Swift
@objc func didTapTransitionToBlueButton() {
    guard let presenter = presenter as? RedPresenterProtocol else { fatalError() }
    presenter.didTransitionButtonTapped(to: .blue)
}
```
↓
presenterのメソッドが呼ばれる
```Swift
func didTransitionButtonTapped(to color: RedViewController.NextRoute) {
    router.transition(to: color)
}
```
↓
Routerのメソッドが呼ばれる
```Swift
func transition(to color: RedViewController.NextRoute) {
    switch color {
    case .red:
        transitionToRed()
    case .blue:
        transitionToBlue()
    case .green:
        transitionToGreen()
    }
}
```

## 具体的な実装

### PresenterInjectableプロトコル

各ViewControllerはPresenterを持つ．後述するRouterの中でViewControllerを差し替えるための抽象的な関数を作成するために，`PresenterInjectable`プロトコルを定義し，各ViewControllerに準拠させる．

```Swift
protocol PresenterInjectable: UIViewController {
    var presenter: ColorPresenter? { get }
    func inject(presenter: ColorPresenter)
}
```

### ViewController

ボタンを定義して配置したり，ボタンが押されたときの通知の設定を行っている．

```Swift
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に準拠させる．

```Swift
protocol ColorPresenter {}
```

```Swift
protocol RedPresenterProtocol: ColorPresenter {
    func didTransitionButtonTapped(to color: RedViewController.NextRoute)
}
protocol BluePresenterProtocol: ColorPresenter {
    func didTransitionButtonTapped(to color: BlueViewController.NextRoute)
}
```
`didTransitionButtonTapped()`を`ColorPresenter`で定義しようか迷ったが，そのためには引数のViewControllerを抽象化する必要があり，その抽象化にはあまり意味がない(サンプルアプリだからできる抽象化だと感じた)と思ったのでやめた．

### Presenterクラス

Viewクラスを弱参照で持たせた．
```Swift

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`プロトコルを定義した．

```Swift
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メソッドで済んでいる．

```Swift
extension Router: RedRouterProtocol {
    func transition(to color: RedViewController.NextRoute) {
        switch color {
        case .red:
            transitionToRed()
        case .blue:
            transitionToBlue()
        case .green:
            transitionToGreen()
        }
    }
}
```