【iOS】iOSアプリ開発入門~ 画面遷移編３~

## はじめに
前回はiOSアプリにおける画面遷移の種類についてお話しました。
画面遷移編２：https://qiita.com/euJcIKfcqwnzDui/items/6d37aaf00c0bc7ce26ca

今回はその具体的な実装方法について説明していきます。
[以前](https://qiita.com/euJcIKfcqwnzDui/items/679b1cd30694519f4916)モーダル遷移の実装をしたプロジェクトをベースとして説明していきます。

## 前準備
その前に、今までプロジェクト作成されたときに生成された`ViewController.swift`と`Main.storyboard`をそのまま使ってきましたが、画面が多くなるとわかりにくくなるので変更しておきましょう。
それぞれ`FirstViewController.swift`と`FirstViewController.storyboard`にリネームします。

まずはファイル名をリネームします。
ファイルを選択しもう一度ファイル名をクリックするとリネームできます。
<img width="276" alt="スクリーンショット 2020-05-30 17.23.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/3f7c3433-a289-340d-0888-d201cd42c728.png">

次はクラス名を変更します。

```swift:FirstViewController.swift
import UIKit

class FirstViewController: UIViewController { // ViewControllerから変更
    /// テキストフィールド
    @IBOutlet weak var textField: UITextField!
    
    /// 画面遷移ボタンタップ処理
    /// - Parameter sender: ボタン
    @IBAction func didTapTransitionButton(_ sender: Any) {
...
...
```
クラス名を変更すると今までそのクラスを使用していた場所も変更してあげないといけません。
今の状態だと`FirstViewController.storyboard`の`ViewController`オブジェクトに設定されています。
`FirstViewController.storyboard`を開き`ViewController`オブジェクトを選択します。
その状態で[Identity Inspector]を選択し、[class]の項目を`FirstViewController`と入力します。
<img width="1184" alt="スクリーンショット 2020-05-30 17.30.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/bb6db718-1973-e692-de90-a8e86ae9c22f.png">
ここで一度シミュレータで実行してみましょう。
すると、起動直後アプリがクラッシュし以下のエラーが表示されます。

```
Thread 1: Exception: "Could not find a storyboard named 'Main' in bundle NSBundle ...
```
「Mainという名前のstoryboardが見つからない」とのこと。
`Main.storyboard`もリネームしているので当然です。
アプリを起動した後どのstoryboardから表示するのかXcodeに教えてあげなければいけません。
プロジェクトを作成した段階では`Main.storyboard`が指定されています。
これを`FirstViewController.storyboard`に変更してあげます。

フォルダ構成の一番上、プロジェクト名を選択します。
ここではプロジェクトの全体的な設定をすることができます。
[TARGETS]にはいくつかありますが一番上のプロジェクト名を選択します。
さらに上のタブで[General]を選択。
[Development Info]に[Main Interface]という項目があるのでプルダウンから`FirstViewController.storyboard`を選択してください。
<img width="1167" alt="スクリーンショット 2020-05-30 17.39.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/f08ca1e1-5b95-e81f-6151-7d54e5e8d1d1.png">
さらにXcode11から`Info.plist`というファイルにも「Main」が設定されています。
その中の[Storyboard Name]の項目を「FirstViewController」と書き換えてください。
<img width="1184" alt="スクリーンショット 2020-05-30 17.51.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/5a1536d0-5240-e93d-0a3a-2d71cec2f9b8.png">

これでファイル名の変更は完了です。
シミュレータで実行し問題ないか確認してください。

## モーダル
これは以前説明したので省きます。
https://qiita.com/euJcIKfcqwnzDui/items/679b1cd30694519f4916#%E7%94%BB%E9%9D%A2%E9%81%B7%E7%A7%BB%E3%81%99%E3%82%8B

## プッシュ
### segueを使った実装
プッシュの実装方法について説明します。
プッシュの画面遷移には以前説明したsegueを使った実装をします。

まずは`Main.storyboard`を開きます。
プッシュの遷移には`UINavigationController`というクラスを使用します。
この`UINavigationController`をStoryboardに追加しましょう。
`FirstViewController`を選択します。
その状態でXcode上部のツールバーから[Edior]>[Embed In]>[Navigation Controller]を選択していってください。
<img width="1177" alt="スクリーンショット 2020-05-29 23.31.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/9f4b6a33-6d31-6cf8-9a04-a7450efa5112.png">
`FirstViewController`にくっつくような形で`Navigation Controller`が追加されました。
<img width="1178" alt="スクリーンショット 2020-05-29 23.33.39.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/10283c52-091e-175e-e826-61d60c9620cb.png">
`Navigation Controller`は画面のような見た目をしていますが、これはあくまでStoryboard上の表示での話であって実際はユーザから見られる画面というわけではありません。

`UINavigationController`は**Controller**とあるようにコントロールするためのクラスです。
Navigationは画面遷移と言い換えてもらえればいいかと思います。
つまり`UINavigationController`は画面遷移を制御するためのクラスであって画面そのものではありません。
今の`Navigation Controller`を追加した操作は`UINavigationController`という概念のようなオブジェクトを`FirstViewController`に追加するというイメージです。

もう少し補足すると`UIViewController`もControllerとあるように、これ自体が画面というわけではありません。
これも画面をコントロールするオブジェクトで、実際に表示されている画面は`FirstViewController`の中にある`View`というUIです。
コントローラは画面の表示制御やユーザからのイベントを扱うという役割です。

少し細かい話をしてしまいました。
実装を進めます。

`FirstViewController`にあるsegueを選択してください。
その状態で[Attributes Inspector]を選択すると[Kind]という項目があるので選択します。
<img width="1174" alt="スクリーンショット 2020-05-30 0.06.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/6ca0e583-4194-0f3d-aba0-d9b6616caa72.png">
ポップアップメニューが表示されるので[Show(e.g. Push)]を選択します。
これでsegueがプッシュ遷移用に変更されました。
<img width="225" alt="スクリーンショット 2020-05-30 0.10.48.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/b2b94cc0-931d-f4f3-c211-68bf6b62ec34.png">
Storyboardの設定はこれだけです。

あとはソースコードから処理実行するだけです。
ですがこれはモーダル遷移の場合と同じで`performSegue`を呼び出してあげるだけです。
segueのidentifierを変えていないので以前モーダルを実装したままのコードですでにプッシュ遷移ができます。
該当のコードは以下です。

```swift:FirstViewController.swift
self.performSegue(withIdentifier: "SecondViewController", sender: nil)
```
実装は以上です。
シミュレータで実行し動作を確認してみてください。

またパラメータもモーダルと同様の方法で渡すことができます。

```swift:FirstViewController.swift
let text = textField.text
self.performSegue(withIdentifier: "SecondViewController", sender: text)
```
### UINavigationControllerについて
`UINavigationController`では一連のプッシュ遷移における画面を管理します。
`UINavigationController`は`rootViewController`というプロパティを持ち、プッシュ遷移の一番最初の画面を保持します。
Storyboardから追加した今回の場合だと、`UINavigationController`を追加した`FirstViewController`が`rootViewController`になります。

また`UINavigationController`は`viewControllers: [UIViewController]`というプロパティを持っており、遷移した画面を配列で管理しています。
今回の例では画面遷移が完了した状態では`viewControllers[0]`に`FirstViewController`、`viewControllers[1]`に`SecondViewController`が格納されています。

プッシュ遷移先の`SecondViewController`に[戻る]ボタンが表示されていたかと思います。
[戻る]ボタンをタップすると一画面前に戻ります。
画面を戻ると`SecondViewController`は`UINavigationController`の管理対象からはずれ解放されます。
つまり`viewControllers[1]`は存在しなくなります。
図で表すと以下のようになります。
<img width="528" alt="プッシュ画面遷移前.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/a9374720-624c-436f-d2ca-193f07d9b2e8.png">
<img width="583" alt="プッシュ画面遷移後.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/d7064731-a391-66c5-fa8a-8f14f281ebe3.png">
<img width="528" alt="プッシュ戻るボタンタップ後.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/30641fb8-2e0c-4f20-843b-b049894a71f3.png">


### SecondViewControllerからさらに遷移する
当然`SecondViewController`から`ThirdViewController`という画面にさらにプッシュ遷移した場合管理下に追加され`viewControllers[2]`に格納されます。

実装してみます。
これは特に難しい話ではなく同様に`SecondViewController`から`ThirdViewController`へのsegueを作ってあげて`performSegue`を呼び出してあげるだけです。

`SecondViewController`を追加した[手順](https://qiita.com/euJcIKfcqwnzDui/items/679b1cd30694519f4916#%E7%94%BB%E9%9D%A2%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B)と同様に`ThirdViewController.swift`、`ThirdViewController.storyboard`を追加以下のように編集してください。

```swift:ThirdViewController.swift
import UIKit

class ThirdViewController: UIViewController {
    
}
```
<img width="1176" alt="スクリーンショット 2020-05-30 14.16.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/1d180f87-7439-193b-20ff-0ffd0bd433dd.png">

さらに`SecondViewController.storyboard`から`ThirdViewController`へのsegueをプッシュで作成してください。
また`SecondViewController.storyboard`にボタンを追加し、ボタンタップで`ThirdViewController`に遷移するようにします。
シミュレータで実行すると３画面遷移できるようになります。
<img width="1183" alt="スクリーンショット 2020-05-30 14.21.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/eaed7543-eaaf-47d6-34df-d01020aac252.png">

```swift:SecondViewController
/// 画面遷移ボタンタップ処理
/// - Parameter sender: ボタン
@IBAction func didTapTransitionButton(_ sender: Any) {
    // 画面遷移
    self.performSegue(withIdentifier: "ThirdViewController", sender: nil)
}
```
ここで注意してほしいことは`SecondViewController.storyboard`に`UINavigationController`は追加しないということです。
`UINavigationController`はrootとなる画面にのみ追加します。
今rootは`FirstViewController`としているため`SecondViewController`はrootではありません。
`SecondViewController`からプッシュ遷移する場合、rootに設定した`UINavigationController`が参照されます。

画面遷移後は以下のような状態になります。
<img width="852" alt="プッシュ遷移３画面.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/217946e9-a5b3-5fa7-3fe5-78bef6d9afc0.png">

予想がつくかと思いますが`ThirdViewController`で[戻る]ボタンを押すと以下のような構成になります。
<img width="583" alt="Third戻る.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/0762bf57-997e-c410-69ca-0cb4a1c65247.png">
このように`UINavigationController`では最後に追加されたものから消えていきます。
この管理方法を**スタック(stack)**と呼びます。
余談ですがスタックは画面管理の方法ではなく配列の管理方法の１つです。
スタックは基本情報試験にも出てくるIT技術の基礎知識なので抑えておきましょう。
別の配列管理方法では**キュー(queue)**があります。一度調べておいてください。

### UINavigationControllerの参照のされ方
ここまでの画面遷移処理で少し違和感を覚える方もいるかもしれません。

rootである`FirstViewController`はStoryboardで`UINavigationController`のオブジェクトを追加したので画面遷移に`UINavigationController`を使えるのはなんとなくわかるかと思います。

ですが`SecondViewController`には`UINavigationController`は設定されていないはずなのにどうして使えるのでしょうか？

実は`UIViewController`には`navigationController`というように現在自分が管理されている`UINavigationController`をプロパティとして持っています。
画面遷移の具体的な処理は`UIKit`が勝手にやってくれているので詳細は不明なのですが、おそらくプッシュ遷移のタイミングで自分の`navigationController`を次の画面に一緒にセットしています。

従って１つの`UINavigationController`のオブジェクトが次へ次へと共有されているのです。
図にすると以下のようなイメージになります。
<img width="852" alt="オブジェクト関係図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/bbe41300-7a33-5e15-2558-5c0e09e8618b.png">
このような形でroot画面からプッシュ遷移した画面の全てを管理しています。

## タブ
### segueを使った実装
タブ遷移の実装方法について説明します。
タブに関してもsegueを使った実装をします。

タブの遷移には`UITabBarController`というクラスを使用します。
`UITabBarController`を扱うために、まず`TabViewController.storyboard`というファイルを追加しましょう。
<img width="278" alt="スクリーンショット 2020-05-30 18.15.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/44de102c-4026-88b3-a4bf-4ec1da8a1498.png">
`TabViewController.storyboard`を開き、[+]ボタンから`UITabBarController`を探しStoryboardに追加します。
<img width="1003" alt="スクリーンショット 2020-05-30 18.16.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/de15a727-6a27-85c2-c588-461ae343ffb6.png">
`Tab Bar Controller`というオブジェクトと一緒に`item 1 Scene`と`item 2 Scene`というオブジェクトが追加されました。
これはタブ`Tab Bar Controller`がデフォルトで表示する画面テンプレートです。
今回は不要なので削除します。
それぞれ選択し削除しておいてください。
<img width="1182" alt="スクリーンショット 2020-05-30 18.18.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/c2db53cd-8b23-c773-16cc-fffa2fafa993.png">

`Tab Bar Controller`も画面のような形ですが、これも「Controller」とあるように画面そのものではなくタブ遷移において画面を管理するオブジェクトです。
階層１つしたにある`Tab Bar`というオブジェクトは画面一番下に表示されるタブ自体です。これは表示されるUIで`UITabBar`というクラスです。

この`TabBarController.storyboard`で最初に参照するオブジェクトは`TabBarController`なので[Is Initial View Controller]のチェックを付けます。
<img width="1173" alt="スクリーンショット 2020-05-30 18.24.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/ca1af2d9-50f2-7257-28a4-4215337731fc.png">
`TabBarController`が画面を追加します。
`FirstViewController.storyboard`、`SecondViewController.storyboard`、`ThirdViewController.storyboard`への参照を追加します。
他の画面遷移と同様`Storyboard Reference`を画面数だけ配置します。
配置した`Storyboard Reference`にそれぞれStoryboardを設定します。
<img width="1175" alt="スクリーンショット 2020-05-30 18.41.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/311c8ad0-beb7-d8df-ce06-dfc261e6f6dd.png">
`TabBarController`からそれぞれの`Storyboard Reference`までsegueを作成します。
segueの種類は[Relationship Segue]の[view controllers]を選択してください。
<img width="945" alt="スクリーンショット 2020-05-30 18.43.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/8ca637d2-6e7c-af59-336c-e4d4b276b3ff.png">
以下のようになればタブ遷移の準備は完了です。
<img width="932" alt="スクリーンショット 2020-05-30 18.47.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/4e843d21-25be-dbc5-1cc5-a382b8bd5567.png">
次はアプリ起動直後に表示するStoryboardを変更します。
[先述](https://qiita.com/euJcIKfcqwnzDui/items/c0c0ccbd8d301b96d8b7#%E5%89%8D%E6%BA%96%E5%82%99)した`Main.storyboard`をリネームする際、説明したようにプロジェクトの[Main Interface]と`Info.plist`の[Storyboard Name]を「TabBarController」に変更してください。
<img width="947" alt="スクリーンショット 2020-05-30 18.52.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/c0bf10f3-a670-914b-609b-b5d370dec197.png">
<img width="929" alt="スクリーンショット 2020-05-30 18.53.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/36d30141-6038-212b-06aa-b77f7cc6b1a4.png">
シミュレータで実行するとタブバーが表示されます。
タブバーには何も表示されていませんが、タブボタン自体は配置されています。
適当な場所をタップするとタブ遷移されます。
<img width="448" alt="スクリーンショット 2020-05-30 19.02.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/cdc257d4-5ebf-3a2d-9f03-3b3a1251720a.png">
ですがこれではユーザからすると何のUIかわからないので画像と名前を設定してあげましょう。

`SecondViewController.storyboard`を開いてください。
[+]ボタンから`UITabBarItem`を探し、`SecondViewController`に追加してください。
`UITabBarItem`はタブボタンのオブジェクトです。
ここにアイテム名や画像を設定してあげるとそれがタブに表示されます。
`UITabBarItem`を選択し、[Attributes Inspector]の[BarItem]に[Title]と[Image]を設定してください。
※画像はまだ追加していないので標準でXcodeに入っているものが出ます。自分で画像を追加していくこともできます。
<img width="1173" alt="スクリーンショット 2020-05-30 19.12.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/10ee6662-7598-386c-b04d-470f8beb5c5b.png">
`ThirdViewController.storyboard`にも同様の手順で追加します。

`FirstViewController.storyboard`についても本来同様の手順でよかったのですが、今回`UINavigationController`が追加されています。
そのため`FirstViewController`オブジェクトに追加しても表示されません。
この場合は`NavigationController`オブジェクトに追加してあげてください。
[Is Initial ViewController]のチェックを入れたオブジェクトに追加する必要があると覚えましょう。
<img width="1184" alt="スクリーンショット 2020-05-30 19.23.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/f4bfa19a-5c2f-15f0-7024-ce41accbfec8.png">
シミュレータで実行するとタブアイテムが表示されます。
<img width="448" alt="スクリーンショット 2020-05-30 19.26.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/0df066f1-cfdd-87a5-b55a-2585a850486a.png">

### UITabBarControllerについて
`UITabBarController`も`UINavigationController`と同様に画面を管理するクラスです。
ですがあくまで並列な画面遷移のためrootというものは存在しません。
表示される画面は同様に`viewControllers`で管理されています。
タブ遷移ではプッシュ遷移と違い画面を消すということが基本的にはないため`viewControllers`はスタック管理されているというわけではありません。
ただ配列として画面を持っているというだけです。

また`UIViewController`は`tabBarController`を持っており、それぞれのViewControllerが同一の`UITabBarController`を参照しています。

図にすると以下のようなイメージです。
<img width="852" alt="タブ関係図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/173da6bb-6d0f-5b57-80fc-7f933a03c98a.png">

## プッシュ遷移とタブ遷移の組み合わせ
実際のアプリではプッシュ遷移とタブ遷移両方を組み合わせて画面遷移を実装していきます。
また、今回の`FirstViewController`ではすでに両方の遷移をしています。
`FirstViewController`には`NavigationController`オブジェクトが追加されているからですね。

今回の画面の構成を正確に書くと以下のようになります。
<img width="1048" alt="全体関係図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326173/49defb03-b6cc-3377-04f4-87227d6425c9.png">
若干複雑な関係となります。

図を見ると`NavigationController`も`tabBarController`というプロパティを持っています。
これは`UINavigationController`が`UIViewController`を継承したクラスのためです。
同様に`UITabBarController`も`UIViewController`を継承しているため`navigationController`というプロパティを持ちます。
そのためさらに`TabBarController`に`NavigationController`を持たせることもできます。

この図をいますぐ理解できる必要はありませんが、実際にアプリを作る際には自分がどんな構成の画面遷移を実装しているのか図に起こせるように理解できるようにしましょう。

## 最後に
今回でiOSの画面遷移編は終了です。
モーダル、プッシュ、タブそれぞれの遷移に意味があり実装方法が多少異なります。
積極的に使って自分のものにしていきましょう。

最後の画面遷移の組み合わせはiOSを学ぶ上で躓く部分かもしれません。
これは画面遷移を実装→図に起こす→画面遷移を実装...と繰り返して感覚的に身につけていってください。

次回はライフサイクルという画面表示非表示になる際にコールされるイベントについて説明します。
https://qiita.com/euJcIKfcqwnzDui/items/d50dc4ec1ccc798fc03e

本連載ではプログラミング未経験からiOSアプリ開発が行えるようになることを目的としています。
今までの投稿をまとめていますのでこちらもご覧ください。
http://naoyalog.com/