iOSアプリ開発入門#2 ~UINavigationController~

## 目的
- よく使うであろうUINavigationControllerを使用したアプリをStoryboardありとなしでサクッと作れるようになる
- ありなしの両方を試すことで、Storyboardが何をやっているのかなんとなくイメージできるようになる

## Step1. 爆速でUINavigationController管理化
[前回のコード](https://qiita.com/chang1019/items/1641bfc6a937743beb41) をベースに作業。
今回もひとまずStoryboardではなくコード側で実装してみる

### 1.1. AppDelegateの修正
まずはAppDelegateを以下のように修正

```diff:AppDelegate.swift
--- a/PracticeApp/AppDelegate.swift
+++ b/PracticeApp/AppDelegate.swift
@@ -16,6 +16,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
+        let vc: ViewController = window?.rootViewController as! ViewController
+        let navc: UINavigationController = UINavigationController(rootViewController: vc)
+        window?.rootViewController = navc
+
         return true
     }
```

ポイントとしては

- AppDelegateのwindowはStoryboardを利用している場合、didFinishLaunchingWithOptions内部ではすでに諸々の初期化が済んでいるため、rootViewControllerはStoryboardで設定したViewControllerになっている
- 新たにUINavigationControllerをコンストラクトし、これをwindowのrootViewControllerに差し替えるとともに、もとのViewControllerをUINavigationController側のrootViewControllerに付け替える（linked listにおける挿入処理のイメージ）
- なお、素直にViewController()でコンストラクトすると、Storyboard上の設定が反映されない

もちろん、こんな妙な処理にしなくても、ViewControllerの初期化処理内でちゃんと表示系を実装してやればそれでいいのだが、あくまでStoryboardとソースコードとの関連を知るのが目的なので。

### 1.2. ViewControllerも一応修正
また、ViewController側もトップにそれっぽいタイトルを表示してみる

```diff:ViewController.swift
--- a/PracticeApp/ViewController.swift
+++ b/PracticeApp/ViewController.swift
@@ -23,6 +23,8 @@ class ViewController:
 
     override func viewDidLoad() {
         super.viewDidLoad()
+
+       self.title = "フルーツ"
     }
 
     // UITableViewDataSource
```

### 1.3. 起動
Runすると、一応それっぽくなる
<img width="310" alt="スクリーンショット 2019-03-02 13.36.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/fe6afc42-1b23-d7f8-33b1-8d0424a87e64.png">


## Step2. BarButtonItem追加＋画面遷移（スタック）
UINavigationControllerの機能だけでとりあえず画面遷移させてみる。
基本的な考え方としては

- UINavigationController管理下のUIViewControllerからNavigationController関連機能にアクセスする際は、インスタンス変数「navigationItem」、「navigationController」を使用する
    - （UINavigationControllerの使用如何に関わらず、navigationItemやnavigationControllerといったpropertyがあるのはいかがなものかと思わなくもない）
- 画面が次に進む、前に戻るという挙動は、UINavigationControllerが管理するStackにpushする、popするという概念に対応づけられる

### 2.1. ViewControllerの修正
というわけで、以下のようにViewControllerを修正する。

```diff:ViewController.swift
--- a/PracticeApp/ViewController.swift
+++ b/PracticeApp/ViewController.swift
@@ -21,10 +21,20 @@ class ViewController:
         ("ぶどう", "Grape"),
     ]
 
+    private var stackIndex = 0
+
     override func viewDidLoad() {
         super.viewDidLoad()
 
-       self.title = "フルーツ"
+        self.title = "フルーツ index:\(stackIndex)"
+
+        self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back",
+                                                                style: .plain,
+                                                                target: nil,
+                                                                action: nil)
+        self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
+                                                                 target: self,
+                                                                 action: #selector(onAddClick))
     }
 
     // UITableViewDataSource
@@ -46,5 +56,12 @@ class ViewController:
         cell.detailTextLabel?.text = data[rowIndex].1
         return cell
     }
+
+    @objc
+    func onAddClick() {
+        let vc: ViewController = self.storyboard!.instantiateInitialViewController() as! ViewController
+        vc.stackIndex = self.stackIndex + 1
+        self.navigationController?.pushViewController(vc, animated: true)
+    }
 }
```

ポイントとしては

- navigationControllerのpushViewControllerでpush操作
- popはデフォルトでbackBarButtonItemにバインドされている
- 子ViewControllerはこのクラス自身であり、Storyboard側で初期化を行なっているため、UIStoryboardのinstantiateInitialViewControllerで取得している


### 2.2. 起動
Runすると、「+」押下で同じ画面が次々にStackされていくのがわかる。

<img width="310" alt="スクリーンショット 2019-03-02 13.55.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/e7e843a3-94d7-23ab-47ac-947e31ed2029.png">


## Step3. Storyboardを使ってみる
今回もコードで地道に書いているところをStoryboardにお任せする流れ

### 3.1. UINavigationControllerの追加
例によってStoryboard右上のボタンからコンポネント追加
<img width="500" alt="スクリーンショット 2019-03-02 14.09.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/06fcc6c7-361e-556b-d356-7fd5fa598efc.png">

ここから追加すると、RootViewControllerが紐づいた状態で配置されてとても邪魔。
爆速で消しにかかる。
<img width="1512" alt="スクリーンショット_2019-03-02_14_10_03.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/b9806639-0610-f405-e34e-d7724b558cd2.png">

また、EntrypointをUINavigationController側に変更する
<img width="1512" alt="スクリーンショット_2019-03-02_14_10_41.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/8ff08f06-f7a7-4b73-44fa-17324fd86999.png">

UINavigationControllerのrootViewControllerとして既存のViewControllerをバインドする
<img width="1512" alt="スクリーンショット_2019-03-02_14_10_56.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/4858e4ca-2f4f-9cac-edd1-10e587c5e923.png">

Storyboard経由でViewControllerを取得するために、identityも忘れずに設定しておく
<img width="1512" alt="スクリーンショット_2019-03-02_14_11_08.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/ac1a9acc-294b-77f6-78da-b5858f6af8d7.png">

### 3.2. コード修正
UI操作はこれだけ。
これによって以下のコードが削減できる。

```diff:AppDelegate.swift
--- a/PracticeApp/AppDelegate.swift
+++ b/PracticeApp/AppDelegate.swift
@@ -16,10 +16,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
 
     func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
         // Override point for customization after application launch.
-        let vc: ViewController = window?.rootViewController as! ViewController
-        let navc: UINavigationController = UINavigationController(rootViewController: vc)
-        window?.rootViewController = navc
-
         return true
     }
```

```diff:ViewController.swift
--- a/PracticeApp/ViewController.swift
+++ b/PracticeApp/ViewController.swift
@@ -59,7 +59,7 @@ class ViewController:

     @objc
     func onAddClick() {
-        let vc: ViewController = self.storyboard!.instantiateInitialViewController() as! ViewController
+        let vc: ViewController = self.storyboard!.instantiateViewController(
+            withIdentifier: "MainViewController") as! ViewController
         vc.stackIndex = self.stackIndex + 1
         self.navigationController?.pushViewController(vc, animated: true)
     }
```

ポイントとしては

- Storyboardからinitialと異なるViewControllerを取得するにはidentityの設定が必要


### 3.3. 起動
Runすると同じ動き
<img width="310" alt="スクリーンショット 2019-03-02 14.40.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/1144/19050da3-1881-3b11-bcca-dc30af10df57.png">


## 4. まとめ
最終的に以下のようなコードになるはず

```swift:ViewController.swift
import UIKit

class ViewController:
    UIViewController,
    UITableViewDelegate,
    UITableViewDataSource
{
    @IBOutlet weak var tableView: UITableView!

    private let data: [(String,String)] = [
        ("りんご", "Apple"),
        ("みかん", "Orange"),
        ("ぶどう", "Grape"),
    ]

    private var stackIndex = 0

    override func viewDidLoad() {
        super.viewDidLoad()

        self.title = "フルーツ index:\(stackIndex)"

        self.navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back",
                                                                style: .plain,
                                                                target: nil,
                                                                action: nil)
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add,
                                                                 target: self,
                                                                 action: #selector(onAddClick))
    }

    // UITableViewDataSource

    func tableView(_ tableView: UITableView,
                   numberOfRowsInSection section: Int) -> Int
    {
        return data.count
    }

    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell
    {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell",
                                                                  for: indexPath)

        let rowIndex: Int = indexPath[1]
        cell.textLabel?.text = data[rowIndex].0
        cell.detailTextLabel?.text = data[rowIndex].1
        return cell
    }

    @objc
    func onAddClick() {
        let vc: ViewController = self.storyboard!.instantiateViewController(
            withIdentifier: "MainViewController") as! ViewController
        vc.stackIndex = self.stackIndex + 1
        self.navigationController?.pushViewController(vc, animated: true)
    }
}
```