ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

【iOS】【Swift】「RootViewController + Wireframe」で画面遷移での消耗を回避する

自己紹介

はじめまして、ペンギン村で一番やかましい住人のナガクラ(@nagakuta)です!
Slackだけでなく、ブログもやかましく更新していきます!!(宣言)

TL;DR

  • RootViewControllerAppDelegate.window.rootViewControllerに指定してから画面遷移するようにすると色々ラクだよ!
  • Wireframeも一緒に使うとテスト書くときラクだよ!!

RootViewControllerによる画面遷移

RootViewController #とは

自分がRootViewControllerについて知ったのは、ペンギン村に貼られた以下の記事でした。

medium.com

その記事のリンクにRootViewControllerについての詳細記事がありました。

medium.com

その記事中にて説明されているRootViewControllerについて簡潔にザザッと説明すると、

  • 概念として、Container ViewControllerの延長である
  • AppDelegate.window.rootViewControllerに設定するViewControllerである
  • このViewControllerを起点に画面遷移を行うようにする

となります。なにこれ便利そう😲😲😲

そして、RootViewControllerを実装するメリットをサクッと説明すると、

  • 1つのナビゲーションスタックだけで新しいViewControllerを表示したり、インタフェースなしで戻したりすることができる
  • なので、どんなにViewが重なっていようと、最下層のViewControllerをカンタンに取得、あるいは差し替えることができる
  • 実は、AppDelegate.window.rootViewControllerを直接差し替えても、差し替え前のViewControllerがメモリ解放されないらしく、差し替えるたびにメモリ領域を圧迫する😨
  • AppDelegate.window.rootViewControllerRootViewControllerに固定することで、メモリ領域の不必要な圧迫を防ぐ

の3点でしょうか。なにそれすごく便利そう😂😂😂

RootViewControllerを実装してみる

では、実際にRootViewControllerを実装してみます( ´∀`)(一部端折っている箇所があります🙇)

RootViewController.swift

final internal class RootViewController: UIViewController {

    // MARK: - Life Cycle Methods

    init() {
        super.init(nibName: nil, bundle: nil)
    }

    override viewDidLoad()
        super.viewDidLoad()

        // 起動直後に遷移させる画面を宣言
        let launchViewController: UIViewController = LaunchViewController()

        // 子ViewのViewControllerを指定
        self.addChildViewContrlller(launchViewController)

        // 子ViewをSubViewとして追加
        launchViewController.view.frame = UIScreen.main.bounds
        self.view.addSubView(launchViewController.view)

        // 子Viewの所有権を譲渡
        launchViewController.didMove(toParentViewController: self)
    }

    /// NextViewControllerに遷移
    func transitToNextViewController() {
        let nextViewController: UIViewController

        // 子ビューのChildViewControllerを削除
        let childViewController: UIViewController = self.childViewControllers.first!
        childViewController.willMove(toParentViewController: nil)
        childViewController.view.removeFromSuperView()
        childViewController.removeFromParentViewController()

        // 新しいViewControllerを子ビューとして追加
        self.addChildViewController(nextViewController)
        nextViewController.view.frame = UIScreen.main.bounds
        self.view.addSubView(nextViewController.view)
        nextViewController.didMove(toParentViewController: self)
    }

}

LaunchViewController.swift

final internal class LaunchViewController: UIViewController {

    // MARK: - Life Cycle Methods

    (中略)

    override viewDidAppear(_ animated: Bool) {
        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
        let rootViewController: UIViewController = appDelegate.window!.rootViewController as! RootViewController

        // NextViewControllerに遷移
        rootViewController.transitToNextViewController()
    }

    (以下略)

}

AppDelegate.swift

@UIApplicationMain
internal class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // 起動直後に遷移する画面をRootViewControllerに指定する
        self.window = UIWindow(frame: UIScreen.main.bounds)
        self.window.rootViewController = RootViewController()
        self.window.makeKeyAndVisible()

        return true
    }

    (以下略)

}

これで、起動 → RootViewControllerに遷移 → (RootViewControllerの子ビューをLaunchViewControllerに) → LaunchViewControllerに遷移 → (RootViewControllerの子ビューをNextViewControllerに置き換え) → NextViewControllerに遷移が実現できました( ´∀`)bグッ!

ここからNextViewControllerをログイン画面とするなり、メインコンテンツを表示させるなりするのが一般的なiOSアプリケーションの起動後の動きになると思います。

Wireframeで画面遷移

Wireframe #とは

Wireframeというのをざっくりと説明すると、「画面遷移をViewControllerの代わりに行うための仕組み」です。

通常、画面遷移を実装する際はUIViewControllerのメソッドを利用して行うと思います。(先程のRootViewControllerでも、Viewの上に重ねるという形で画面遷移を実装しています)

しかし、画面遷移をUIViewControllerクラスで行うと、少しViewControllerが膨らんでしまいますし、ViewControllerごとに同様の処理を何回も何回も実装するのはアホくs…スマートじゃないですよね😅

そんな時に使ってみたいのがWireframeです。Wireframeで画面遷移用のメソッドを実装し、ViewControllerクラスからそのメソッドを呼び出すようにすれば、いちいち画面遷移についてあーだこーだと考えずにViewControllerは画面表示のことに専念できます👍

また、遷移先の画面を指定できるようになれば、Unitテストの際にモックのViewControllerを指定することができるようになるため、テストを書くのが格段に楽チンになります😆😆😆

Wireframeによる画面遷移を実装してみる

ではでは、Wireframeを実装してみましょう(`・ω・´)ガンバル

RootWireframe.swift

internal protocol Wireframe {
    /// 指定した画面に遷移
    func transition(to viewController: UIViewController)
}

internal struct RootViewWireframe: Wireframe {

    func transition(to viewController: UIViewController) {
        let appDelegate: AppDelegate = UIApplication.shared.delegate as! AppDelegate
        let rootViewController: RootViewController = appDelegate.window!.rootViewController as! RootViewController

        // RootViewControllerの子ビューを削除
        if !rootViewController.childViewControllers.isEmpty {
            rootViewController.childViewControllers.forEach { (childViewController: UIViewController) in
                childViewController.willMove(toParentViewController: nil)
                childViewController.view.removeFromSuperView()
                childViewController.removeFromParentViewController()
        }

        // 以下はRootViewController.viewDidLoadメソッドの内容をそのまま移植
        rootViewController.addChildViewController(viewController)
        viewController.view.frame = UIScreen.main.bounds
        rootViewController.view.addSubView(viewController.view)
        viewController.didMove(toParentViewController: rootView)
    }

}

これでWireframeの実装ができました👍

では、RootViewControllerの画面遷移をWireframeに移譲します(`・ω・´)シャキーン

RootViewController.swift

final internal class RootViewController: UIViewController {

    let wireframe: RootViewWireframe = RootViewWireframe()

    (中略)

    override viewDidLoad() {
        super.viewDidLoad()

        // Wireframeによる画面遷移
        let childViewController: UIViewController = UIViewController()
        self.wireframe.transition(to childViewController)
    }

    (以下略)
}

LaunchViewController.swift

final internal class LaunchViewController: UIViewController {

    let wireframe: RootViewWireframe = RootViewWireframe()

    (中略)

    override func viewDidAppear(_ animated: Bool) {
        let nextViewController: UIViewController = UIViewController()
        self.wireframe.transition(to nextViewController)
    }

    (以下略)

}

これで、Wireframeによる画面遷移が実装できました!(∩´∀`)∩ワーイ

これで、AppDelegate.window.rootViewControllerの差し替えも容易になり、重なりまくった子ビューから最下層のViewを取得するのもRootViewController.childViewControllers.firstで取得できるようになります💪💪💪

あとがき

RootViewController + Wireframe による画面遷移」、いかがだったでしょうか😃😃😃

これからも、「楽に、最小効率で」を目的に勉強したTipsをこのブログで公開していきたいなと思います(`・ω・´)
次回更新もお楽しみに!!!