[Swift]ContainerViewを使ってみる

# 目的
ContainerViewを使用して、ViewControllerの埋め込みができるようになる。

:::note warn
Storyboardでなく、コードでの実装となります。
:::



# ContainerViewとは？
ChatGPTに聞いてみた。
> Swiftでは、ContainerViewは親ビュー内に他のビューコントローラのコンテンツを埋め込むための便利な方法です。ContainerViewは、親ビューコントローラ内に子ビューコントローラのビューを配置し、子ビューコントローラのライフサイクルを管理します。


ContainerViewを使用した例
* ViewControllerにPageViewControllerを埋め込み、一部箇所をスライド可能にする。
* 設置されているボタンをタップすると、ViewControllerが別のViewControllerに切り替わる。
* 画面端から内側にスワイプした際に現れる通知センターに別のViewControllerを使用する。

# ContainerViewの実装方法

Storyoboardから実装する場合は、下記を参考にするとわかりやすい！

https://qiita.com/ryotkato/items/a9bf82b2767a67f12335

コードで実装する又はxibファイルで実装する場合は、下記を参考にするとわかりやすい！

StoryboardのようにContainerViewをパーツとして載せるのではなく、埋め込みたいViewControllerを、埋め込むViewControllerの子として追加し、用意したUIViewの上に子ViewControllerのViewを載せるという感じ。

https://developer.apple.com/documentation/uikit/view_controllers/creating_a_custom_container_view_controller



# 実際にコードを書いていく
使い方を知れたところで実際に使って慣らしていく！
## 実践1
仕様は下記になる。
* MainViewControllerにSub1ViewControllerを埋め込む。
* MainViewControllerにnextButtonを設置し、タップした場合にMainDetailViewControllerに遷移する。
* Sub1ViewControllerにnextButtonを設置し、タップした場合にSub1DetailViewControllerに遷移する。

完成図
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/f9484f50-5642-40ce-d01b-e2bc8236877d.png">　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/25fbf726-2129-2b8b-55b0-ae06027866b8.png">　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/8a7bdfe1-0d75-7414-3c5a-fe4792a97b27.png">

以下では、MainViewControllerにSub1ViewControllerを埋め込む際に行なった手順のみを記載する。

1\. MainViewControllerにSub1ViewControllerを載せる`containerView`を設置する。
```swift:MainViewController.swift
import UIKit

class MainViewController: UIViewController {
    // ①
    private let containerView = UIView()
    
    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        self.view.backgroundColor = .systemBackground
        
        setUpContainerView()
    }
    
    private func setUpContainerView() {
        // ②
        containerView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(containerView)
        
        NSLayoutConstraint.activate([
            containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20),
            containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -100),
            containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20),
            containerView.heightAnchor.constraint(equalToConstant: self.view.bounds.height / 3)
        ])
    }
    
}
```
①については、containerの役割をするUIViewを設置している。このUIViewの上にSub1ViewControllerのViewが追加される。②については、AutoLayoutの設定になる。AutoLayoutの設定方法について知りたい場合は下記を参照する。

https://qiita.com/hohohehetoto/items/aecdb0c00686b070660c

2\. ContainerViewにSub1ViewControllerのViewを追加する。
```swift:MainViewController.swift
import UIKit

class MainViewController: UIViewController {
    
    private let containerView = UIView()

    // 省略
    
    private func setUpContainerView() {
        
        // 省略

        let sub1VC = Sub1ViewController()
        // ①
        addChild(sub1VC)
        sub1VC.view.translatesAutoresizingMaskIntoConstraints = false
        // ②
        containerView.addSubview(sub1VC.view)
        // ③       
        NSLayoutConstraint.activate([
            sub1VC.view.topAnchor.constraint(equalTo: containerView.topAnchor),
            sub1VC.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            sub1VC.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            sub1VC.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])
        // ④        
        sub1VC.didMove(toParent: self)
    }
    
}

```
①について、`addChild`を実行することで、現在のViewControllerの子としてSub1ViewControllerを追加している。このメソッドを実行して親子関係を作成することは、子ViewControllerのViewを現在のViewConotrollerのコンテンツに埋め込むときに必要である。（[公式](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621394-addchild)）
②について、子のRootViewをcontainerViewのView階層に追加している。③について、Sub1ViewControllerのViewをcontainerViewのレイアウトに合わせるよう、AutoLayoutを設定している。④について、プロセスの最後、子に遷移が完了したことを通知する。

:::note info
なぜ親子関係を作成するのか？
[公式](https://developer.apple.com/documentation/uikit/view_controllers/creating_a_custom_container_view_controller#3370392)には下記のように記載がある。
> Establishing a container-child relationship between view controllers prevents UIKit from interfering with your interface unintentionally. UIKit normally routes information to each of your app’s view controllers independently. When a container-child relationship exists, UIKit routes many requests through the container view controller first, giving it a chance to alter the behavior for any child view controllers. For example, a container view controller may override the traits of its children, forcing them to adopt a specific appearance or behavior.

ChatGPTにも聞いてみた。
> 親子関係を作成する理由は、ビューコントローラの階層を形成し、ビューの表示やイベントの伝達を適切に管理するためです。
>
>親子関係を形成することで、親ビューコントローラは子ビューコントローラを所有することができます。親ビューコントローラは、子ビューコントローラのライフサイクルを管理し、子ビューコントローラのビューを自身のビューの一部として表示することができます。
>
>具体的な理由としては以下のような点が挙げられます：
>
>1. ビューの階層構造の管理: 親子関係を作成することで、ビューの階層構造を形成することができます。親ビューコントローラのビューに子ビューコントローラのビューを追加することで、複数のビューを階層的に組み合わせることができます。
>
>2. ライフサイクルの管理: 子ビューコントローラが親ビューコントローラに追加されると、親ビューコントローラが子ビューコントローラのライフサイクルを管理します。例えば、親ビューコントローラが表示されたり非表示になったりするときに、子ビューコントローラもそれに応じて適切な処理を実行します。
>
>3. イベントの伝達: 親ビューコントローラがイベントを受け取ると、子ビューコントローラにもそのイベントが伝達されます。これにより、親ビューコントローラと子ビューコントローラ間での情報のやり取りや、イベント処理の委譲が容易になります。
>
>親子関係を作成することにより、ビューコントローラ間の関連性と相互作用を明確化し、ビューの管理と操作を効果的に行うことができます。
>
親子関係を作成することで、個別に動いていたViewControllerのライフサイクル(ViewWillApperなど)が、親ViewControllerに付随して動くようになる！
:::

:::note info
なぜ`didMove`を実行して終了を通知するのか？
UIViewController自身が他のコンテナに追加（もしくは削除）されている最中かどうかを知ることで処理を分岐する事もできるようになっていて、それを判断するために完了したことを通知する必要がある。
下記の記事はとてもわかりやすかった。
なぜ自動で呼ばれないようになっているのかという点も面白いと思った！

https://qiita.com/yimajo/items/a5e16fa0f9c332a55ccf

遷移完了時の処理がなくても、記載はした方が良いみたい。

:::

## 実践2
仕様は下記になる。
* 実践1で作成したnextButtonの下にchangeContainerButtonを設置する。
* changeContainerButtonをタップすると、Sub1ViewController箇所がSub2ViewControllerに切り替わる。
* Sub2ViewControllerの場合は、Sub1ViewControllerに切り替わる。

完成図
<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/1dbcf144-b176-6def-8fc2-ebccf77bf10b.png">　<img width="150" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1640998/c6359863-603f-0bf1-1339-42330912c65c.png">　
以下では、changeContainerButtonをタップした際の処理のみを記載する。

1\. Sub1ViewControllerと、Sub2ViewControllerをContainerに設置し、配列に保持する。
```swift:MainViewController.swift
import UIKit

class MainViewController: UIViewController {
    //　省略
    private var subViewControllers: [UIViewController] = []

    // MARK: - View Life Cycle

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setUp()
    }
    
    
    // MARK: - Action
    
    private func setUp() {
        self.view.backgroundColor = .systemBackground
        
        setUpContainerView()
        //　省略
    }
    
    private func setUpContainerView() {
        containerView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(containerView)
        
        NSLayoutConstraint.activate([
            containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 20),
            containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -100),
            containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -20),
            containerView.heightAnchor.constraint(equalToConstant: self.view.bounds.height / 3)
        ])
        //　①      
        addContainer(Sub2ViewController())
        addContainer(Sub1ViewController())
    }
    //　②
    private func addContainer(_ viewController: UIViewController) {
        viewController.view.translatesAutoresizingMaskIntoConstraints = false
        addChild(viewController)
        containerView.addSubview(viewController.view)
        NSLayoutConstraint.activate([
            viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
            viewController.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            viewController.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
            viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
        ])
        viewController.didMove(toParent: self)
        //　③
        subViewControllers.insert(viewController, at: 0)
    }
    
    //　省略
    
}
```
①について、一番下に表示するViewControllerからContainerViewに追加している。②について、親子関係を作成し、ContainerViewにRootViewを追加するためのメソッドを作成している。③について、追加したものが先頭に来るようにしている。

2\. Buttonをタップした際の処理を記載する。
```swift:MainViewController.swift
import UIKit

class MainViewController: UIViewController {
    // 省略
    // ①
    private var selectedIndex = 0 {
        didSet {
            let subViewController = subViewControllers[selectedIndex]
            containerView.bringSubviewToFront(subViewController.view)
        }
    }
   
    // MARK: - Action

    // 省略
    // ②
    @objc private func tapChangeContainerButton() {
        let index = selectedIndex + 1
        selectedIndex = index > subViewControllers.count - 1 ? 0 : index
    }
}
```
①について、selectedIndexがセットされると、該当番目のViewが前面に表示されるようにしている。②について、subViewControllerの総数以上の場合、先頭に戻るようにしている。

:::note warn
ContainerのViewControllerを切り替える実装について
調べてみたところ下記のように色々な方法が見つかりました。Hiddenで隠す方法や、removeFromSuperviewをしてから追加する方法、親子関係ごと切り替える方法など、どれが良いのかはまだわかっていません‥。わかる方がいらっしゃいましたら教えてくださると嬉しいです🙇

・bringSubviewToFrontを使用する方法
https://qiita.com/REON/items/24998d48e1c8154f32c4
・親子関係ごと切り替える方法
https://qiita.com/okyawa/items/d7d32517a01bbf8619ad
・removeFromSuperviewを使用する方法
https://www.slideshare.net/asakahara/container-viewcontroller
:::

# まとめ
今回わからなかった箇所はわかり次第修正していきます！
ここまでご覧いただきありがとうございました！
また、優れた知識と経験を共有してくださる皆様に感謝です！本当にありがとうございました！