[iOS] RootViewControllerを実装して画面を切り替えてみた

#はじめに
ログイン/ログアウトによって画面を切り替えたいような状況はよくあると思います。
UIWindowのrootViewControllerを差し替えてアプリのどこからでも画面を切り替えられるようにしてみました。
下記GIF画像のようにアプリの画面切り替えを実装します。
<img width=250" alt="switch_screen.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/667697/451461e8-631c-cd05-6735-0ba9a76037f4.gif">

#環境
[Xcode] Version 12.4
[Swift] Version 5.3.2
[iOS] 14.4
[MacOS] 10.15.7　

#実装手順
1. UIWindowのrootViewControllerに設定するための、RootViewControllerを作成  
rootViewControllerを直接切り替えることはあまり推奨されないらしく、rootViewControllerの子ViewControllerを切り替えて、rootViewControllerには`RootViewController`を常に設定します。
理由はメモリの関係で、、、という記事を見かけたことがありますが、私自身がその内容をちゃんと理解していないため、あくまで画面を切り替える１手法として取り入れたいと思います。

2. 今回はアプリの立ち上げ画面 (`SplashViewController`)を作成し、遷移先の画面の切り替えを行います  
3. ログイン画面とメイン画面はNavigationBarにタイトルが出るだけの単純なものを作っています。  
ここの内容は省略します。

本アプリではSceneDelegateが必要ないため、SceneDelegateを削除してAppDelegateを経由してWindowのrootViewControllerを取得しています。iOS13以降でSceneDelegateを使わずにAppDelegateを使う方法についての説明は本記事では省きます。
[こちらの記事](https://ulog.sugiy.com/ios13-without-scenedelegate/)が分かりやすかったので参考にさせていただきました。

#RootViewController

```Swift:RootViewController
import UIKit

final class RootViewController: UIViewController {
    
    //現在のアプリケーションの状態を追跡するために、現在のViewControllerを指す変数を作成
    private var current = UIViewController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        //viewをロードするとすぐに呼ばれるSplashViewControllerを準備する
        current = SplashViewController()     
        addChild(current)
        current.view.frame = view.bounds
        view.addSubview(current.view)
        current.didMove(toParent: self)  
    }

    //メイン画面への遷移メソッド
    func switchToMainScreen() {
        let mainViewController = MainViewController()
        let new = UINavigationController(rootViewController: mainViewController)
        animateFadeTransition(to: new)
    }

    //ログイン画面へ遷移するメソッド
    func switchToLogin() { 
        let loginViewController = LoginViewController()
        let new = UINavigationController(rootViewController: loginViewController)
        animateFadeTransition(to: new) 
    }
    
    //メイン画面に遷移する際のアニメーションメソッド
    private func animateFadeTransition(to new: UIViewController, completion: (() -> Void)? = nil) {
        current.willMove(toParent: nil)
        addChild(new)
        //ページ遷移
        transition(from: current, to: new, duration: 0.3, options: [.transitionCrossDissolve, .curveEaseOut], animations: {}) { (completed) in
            self.current.removeFromParent()
            new.didMove(toParent: self)
            self.current = new
            //完了
            completion?()  
        }
    }

  //上記遷移メソッドの中身の各コードを説明 (showLoginScreen()というメソッドがあると仮定します)
    func showLoginScreen() {
        //遷移先のViewControllerオブジェクトを作成
        let loginViewController = LoginViewController()
        let new = UINavigationController(rootViewController: loginViewController)
        //それをRootViewControllerの子ViewControllerとして追加
        addChild(new)
        //Viewのフレームを位置合わせ
        new.view.frame = view.bounds
        //そのビューをaddSubView
        view.addSubview(new.view)
        //新しいViewControllerを追加した直後に呼び出す必要あり
        new.didMove(toParent: self)
    
        //willMoveを呼び出して、現在の子ViewControllerを削除する準備
        current.willMove(toParent: nil)
        //現在のビューをスーパービューから削除
        current.view.removeFromSuperview()
        //現在の子ViewControllerを親のRootViewController切り離す
        current.removeFromParent()
        //最後に現在の子viewControllerを更新することを忘れずに。
        current = new
    }
```

#SplashViewController

```Swift:SplashViewController

import UIKit

//ユーザーの状況によって、アプリ起動後の遷移先を切り替える
final class SplashViewController: UIViewController {
    
    //処理進行を示すインジケータ
    private lazy var activityIndicator: UIActivityIndicatorView = {
        let activityIndicator = UIActivityIndicatorView(style: .large)
        activityIndicator.frame = view.bounds
        activityIndicator.color = .white
        activityIndicator.backgroundColor = UIColor(white: 0.5, alpha: 0.4)
        return activityIndicator
    }()

    override func viewDidLoad() {
        super.viewDidLoad()   
        view.backgroundColor = .white
        view.addSubview(activityIndicator)
        //アプリ起動時の画面切り替え
        makeScreenTransition()
    }
    
    //ユーザーの状態によって画面を切り替えるメソッド
    private func makeScreenTransition() {
        //1秒後に止まる遅延処理
        activityIndicator.startAnimating()
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(1)) {
            self.activityIndicator.stopAnimating()
            //今回はログイン状況をデバイスに保存している想定です
            if UserDefaults.standard.bool(forKey: "isLogin") {
                //メイン画面へ
                AppDelegate.shared.rootViewController.switchToMainScreen()
            } else {
                //ログイン画面へ
                AppDelegate.shared.rootViewController.switchToLogin()
            }   
        }  
    }
}
```

#AppDelegate
AppDelegateのシングルトンを作成して、どこからでも呼び出して画面を切り替えられるようにしています。

```Swift:AppDelegate
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {       
        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window
        //RootViewControllerを設定
        window.rootViewController = RootViewController()
        window.makeKeyAndVisible()
        
        return true
    }
}

extension AppDelegate {
    //シングルトン
    static var shared: AppDelegate {
        return UIApplication.shared.delegate as! AppDelegate
    }
    
    var rootViewController: RootViewController {
        return window!.rootViewController as! RootViewController
    }
}
```

#最後に
RootViewControllerが画面の切り替えを担うので、私のようにシンプルな設計のアプリを開発している場合には実装、管理の容易な方法と思いました。
今後、より複雑な状況への対応や、より良い実装方法があれば学んで記事にしていけたらと思います。

#参考文献
この記事は以下の情報を参考にしました。
- [iOS: Root Controller Navigation](https://medium.com/@stasost/ios-root-controller-navigation-3625eedbbff)
- [iOS13でSceneDelegateを使わないでアプリを作る](https://qiita.com/u5_03/items/ff005e7cab7ff509f140)