[Swift] 画面遷移をコードで記述する

## はじめに

複雑な画面遷移や複数の起動経路を持つようなアプリの画面遷移に、Storyboardを使用して実装するとSegueが入り乱れ大変なことになるようです。

学習の記録も兼ねて、単純な画面遷移を実装しました。

## 目的
可読性が良い＆改修のしやすいコードにすること

### 前提

1. 画面の装飾や遷移のためのボタンなどはStoryboardで設置している<br>※実際は全てをStoryboardに任せる訳にはいかないが、今回はサンプルのコードを簡潔にするためこのような手法を取りました。
- FirstView -> SecondView -> ThirdView と単純な画面遷移のみ
- それぞれの画面につき、ViewControllerとStoryboardを一つずつ使用
- Info.plistの`Main storyboard file base name`は削除する
- SceneDelegateは削除し使用しない<br>※SceneDelegateはiOS13から追加された機能となり、iOS12以前にも対応可能にするためには予め削除しておく必要がある。<br>本記事ではSceneDelegateは使用せず、AppDelegateに起動コードを記述している。

4,5についての解説は本記事では省略しますが、以下の記事が分かりやすく参考になると思いますのでリンクさせて頂きました。いつもありがとうございます。

- [【Xcode11】Storyboardを使わずコードだけで画面を生成する方法](https://qiita.com/edasan/items/68cbe9ab63d48ee71594)
- [SceneDelegateを使用せずにiOSでアプリをビルドするようにする]()
- [iOS 13 から導入されるSceneDelegateとは]()


## ファイル構成＆プレビュー

|ファイル構成|プレビュー|
|---|---|
|<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/662505/ced4a22f-0f25-a60d-06ec-67fd73429c23.png" width="300">|<img src="https://i.gyazo.com/efe00d448ce6e3b0d786614621f1ece5.gif" width="300">|

## ソースコード
### ①AppDelegate
```AppDelegate.swift
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 
        Router.showRoot(window: window) // 初期Viewの取得はRouterクラスで行う
       
        return true
    }
}
```

ViewControllerやNavigationControllerを取得するといった処理を、`showRoot`というメソッドとしてRouterクラスに任せています。
UIWindowのみこちらでインスタンス化し、メソッドに引数として渡します。


### ②FirstViewController(最初のView)
```FirstViewController.swift
import UIKit

class FirstViewController: UIViewController {
    // ボタンを押した時の処理
    @IBAction func tapGoSecond(_ sender: UIButton) {
        Router.showSecond(fromVC: self)
    }
}
```

遷移先のViewControllerを取得する処理をRouterクラスに任せています。
self.ViewControllerをメソッドの引数として渡します。


### ③SecondViewContrller(2つ目のView)
```SecondViewController.swift
import UIKit

class SecondViewController: UIViewController {
    // ボタンを押した時の処理
    @IBAction func tapGoThird(_ sender: UIButton) {
        Router.showThird(fromVC: self)
    }   
}
```
同じく、遷移先のViewControllerを取得する処理をRouterクラスに任せています。
self.ViewControllerをメソッドの引数として渡します。


### ④Router
前出の３つのクラスから画面遷移のメソッドをそれぞれ受け取っており、それらの処理を書いています。

```Router.swift
import UIKit

final class Router {

    // アプリ起動時にrootViewを取得する処理
    static func showRoot(window: UIWindow?) {
        let firstStoryboard = UIStoryboard(name: "First", bundle: nil)
        let firstVC = firstStoryboard.instantiateInitialViewController() as! FirstViewController
        let nav = UINavigationController(rootViewController: firstVC) // ナビゲーションコントローラーを定義。引数で最下層となるViewを指定
        window?.rootViewController = nav
        window?.makeKeyAndVisible()
    }
    
    // 2つ目のViewへ画面遷移する処理
    static func showSecond(fromVC: UIViewController) {
        let secondStoryboard = UIStoryboard(name: "Second", bundle: nil)
        let secondVC = secondStoryboard.instantiateInitialViewController() as! SecondViewController
        show(fromVC: fromVC, nextVC: secondVC)
    }
    
    // 3つ目のViewへ画面遷移する処理
    static func showThird(fromVC: UIViewController) {
        let thirdStoryboard = UIStoryboard(name: "Third", bundle: nil)
        let thirdVC = thirdStoryboard.instantiateInitialViewController() as! ThirdViewController
        show(fromVC: fromVC, nextVC: thirdVC)
    }
    
    // 実際に画面を遷移させる処理
    private static func show(fromVC: UIViewController, nextVC: UIViewController) {
        if let nav = fromVC.navigationController {
            nav.pushViewController(nextVC, animated: true)
        } else {
            fromVC.present(nextVC, animated: true, completion: nil)
        }
    }  
}
```

#### instantiateInitialViewControllerとは
instantiateInitialViewControllerはStoryboardクラスのメソッドで、「is initial view controller」が設定されているViewControllerを取得できる<br>※引数にstoryboardIDを直接指定して取得することも可能

#### showについて
showメソッドはNavigationControllerの有無をアンラップによって判別し、結果によって画面の表示方法を変えているもの

#### makeKeyAndVisibleとは
UIWindowクラスのインスタンスメソッド。
[AppleDeveloper](https://developer.apple.com/documentation/uikit/uiwindow/1621601-makekeyandvisible)を見ると、
> Shows the window and makes it the key window.
> ウィンドウを表示し、それをキーウィンドウにします

とあります。

~以下は自分の認識なので間違っているかもしれません~
このメソッドを実行することによって`キーウィンドウの指定と表示`を行う。つまり、`ウィンドウの直上に設置するrootViewを決めている`ということ。

今回のサンプルでいうと、

```swift
window?.rootViewController = UINavigationController(rootViewController: firstVC)
// windowのrootViewControllerプロパティにはfirstVCが代入されている
window?.makeKeyAndVisible()
// 現在のwindowの状態をキーウィンドウとして表示する
```

ということになると解釈しています。

## 後語り
以上がコードで書く画面遷移のやり方となります。

今回はただ画面を進めるだけのシンプルな画面遷移でしたが、複雑な画面遷移を必要とするアプリケーションの場合、このように切り離すことで、可読性が良くなり改修もしやすくなると思います。

また、UIWindowの扱いであったりrootViewControllerの差し替えなどについては勉強不足で理解しきれていませんので、今後の課題としていきます。