LoginSignup
1
1

More than 1 year has passed since last update.

MVP+AppRootController+Routerやってみた

Last updated at Posted at 2022-02-26

RootControllerとは

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

リポジトリはこちら

で詳しく説明されている.
今回は,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()
        }
    }
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1