クリーンアーキテクチャで用いられる画面遷移の責務を持つRouterデザインパターンについて


iOSでClean Architectureで用いられるRouter(Interactorとも呼ばれる?)について

この記事は以下の記事からRouter部分を詳細に解説した記事です

ある程度複雑なiOSアプリに必要なClean Architectureのベストプラクティスを考えてみた https://qiita.com/YOSUKE8080/items/c036c9cc17bbee773019

Routerパターンとは、その目的

一言でいうと、単一責任の原則を画面遷移に!
Routerは、画面遷移の責務を持ちます。
画面遷移まわりのコードが煩雑になっていると、Routerパターンを用いて切り出しすことができま。

画面遷移のコードはプロトコルにして、ViewControllerにアタッチできるようにします。

各ViewそれぞれにViewControllerを持つクラスを作成し、

そのViewControllerを持つクラスに、遷移したい処理のプロトコルを当てていきます。

ファイル構成

Router.swift

このファイルに実際に遷移するコードをprotocolで書く。

各Viewに対応したRouterを定義

各ViewのUIViewControllerを持ち、遷移したい先の上記のprotocolをあてていく。

Router.swift に定義する例

このファイルには、画面遷移の処理を書く。
各ViewとRouterが紐付いていて、
例えば、LoginViewTransitionableに定義してあるものは、ログイン画面へ遷移するものを記述する。
その画面から、他の画面へ遷移するということを定義しているわけではないです。

 ログイン画面へ遷移する

protocol LoginViewTransitionable {
    var viewController: UIViewController? { get set }

    func toLoginView()
}

extension LoginViewTransitionable {
    func toLoginView() {
        let viewController = LoginBuilder().build()
        guard let window = UIApplication.shared.windows.filter({ $0.isKeyWindow }).first else { return }
        guard let rootViewController = window.rootViewController else { return }
        viewController.view.frame = rootViewController.view.frame
        viewController.view.layoutIfNeeded()
        UIView.transition(with: window,
                          duration: 0.5,
                          options: .transitionCrossDissolve,
                          animations: { window.rootViewController = viewController },
                          completion: nil)
    }
}

Todoを作る画面をモーダルで表示する

protocol CreateTodoViewTransitionable {
    var viewController: UIViewController? { get set }
    func toCreateTodoView()
}

extension CreateTodoViewTransitionable {
    func toCreateTodoView() {
        let vc = CreateTodoBuilder().build()
        viewController?.present(vc, animated: true, completion: nil)
    }
}

モーダルで表示した画面をdismissする。

protocol DismissTransitionable {
    var viewController: UIViewController? { get set }
    func dismiss(animated: Bool, completion: (() -> Void)?)
}

extension DismissTransitionable {
    func dismiss(animated: Bool, completion: (() -> Void)?) {
        viewController?.dismiss(animated: animated, completion: completion)
    }
}

タスク詳細画面へナビゲーションコントローラを通じて遷移する

protocol TodoDetailViewTransitionable {
    var viewController: UIViewController? { get set }
    func toTodoDetailView(todo: Todo)
}

extension TodoDetailViewTransitionable {
    func toTodoDetailView(todo: Todo) {
        let vc = TodoDetailBuilder().build(todo: todo)
        viewController?.navigationController?.pushViewController(vc, animated: true)
    }
}

各画面に対応したRouter

例えば以下のLoginRouterはUIViewControllerを持ち、ログイン画面からユーザーを作る画面と、ログイン後に遷移するタスクリスト画面へと遷移することができる。

protocol LoginRouter: TodoListViewTransitionable,
    CreateUserViewTransitionable
{
    var viewController: UIViewController? { get set }
}

final class LoginRouterImpl: LoginRouter {
    weak var viewController: UIViewController?
}

依存性を注入するBuilder

struct LoginBuilder: AuthUseCaseInjectable {
    func build() -> UIViewController {
        let vc = LoginViewController.instantiate()
        let router = LoginRouterImpl()
        let presenter = LoginPresenterImpl()

        router.viewController = vc
        presenter.router = router
        vc.presenter = presenter

        return vc
    }
}

Presenterから画面遷移する例

TodoListViewTransitionableに定義してあるtoTodoListView()
CreateUserViewTransitionableに定義してあるtoCreateUserView()
を用いて、routerを通して画面遷移する。


final class LoginPresenterImpl: LoginPresenter {
    var router: LoginRouter!
    let bag = DisposeBag()

    func setBind() {
        toLoginRelay
            .subscribe(onNext: { [weak self] result in
                           guard let weakSelf = self else { return }
                           weakSelf.router.toTodoListView()
                       })
            .disposed(by: bag)

        toCreateUserViewRelay
            .subscribe(onNext: { [weak self] _ in
                self?.router.toCreateUserView()
            })
            .disposed(by: bag)
    }
}

最後に

読んでくれてありがとう