UITabBarControllerを使わず2画面の切り替えUIを実装する

# はじめに
複数のメイン画面を並列で提供するUIといえば画面下部や上部にあるタブがメジャーですが、個人開発のような画面数の少ないアプリを作っていると以下のような課題やニーズが生まれることがあると思います。

- タブを使うと画面数の少なさが際立ってチープに見えてしまう
- タブの表示領域がメイン機能の領域を圧迫してしまっている
- タブに換わるようなシンプルなUIで遷移させたい
 
今回は上記の課題を解決できるように自作スイッチングUIを作ってみました。

# 実装するUIの狙い
下記のようなUIを実装しました。

![実装結果.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/2f9fb9c5-a141-bcc5-0996-d7ae2c53b13b.gif)

このUIの狙いとしては、

- 遷移先のみを明示することで、画面数を感じさせないようにする。
- コンポーネント領域は最低限にして多くの領域をメイン画面に利用できるようにする
- シンプルなアニメーションで画面の切り替わりをわかりやすくする

# 実装

## 画面構成
以下のような画面構成です。

```
RootViewController
├ SwitchButtonView
└ ContainerView
  └ ContentView
    ├ HomeViewController
    └ SearchViewController

SwitchButtonView: 遷移先を示すボタンのViewコンポーネント
HomeViewController: 遷移先の一つ。メインコンテンツを表示するホーム画面を想定。
SearchViewController: 遷移先の一つ。検索画面を想定。
```

## SwitchButtonView
1. xibでCustomViewを作る。今回は下記のようなViewを用意して回転によってどちらか画像のみが表示されるようにします。
![スクリーンショット 2020-11-02 1.10.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/639f20be-0b6b-29e7-7ab2-61d9df1dd8f4.png)

2. 各ボタンとなるViewに`UITapGestureRecogniger`をaddする。
3. `CGAffineTransform`でViewを回転する。タップされたボタンによって回転方向を変える。
4. delegateでRootViewControllerにタップイベントを伝播。

```Swift:SwitchButtonView.swift

import UIKit

final class SwitchButtonView: UIView {

    @IBOutlet private weak var toSearchButtonView: UIView! {
        didSet {
            // 2. 各ボタンとなるViewに`UITapGestureRecogniger`をaddする。
            toSearchButtonView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapToSearchButtonView)))
        }
    }
    @IBOutlet private weak var toHomeButtonView: UIView! {
        didSet {
            // 2. 各ボタンとなるViewに`UITapGestureRecogniger`をaddする。
            toHomeButtonView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tapToHomeButtonView)))
        }
    }
    
    var delegate: SwitchButtonViewDelegate?
    
    // ~~ 中略 ~~

}

// MARK: - Event
extension SwitchButtonView {
    @objc
    private func tapToSearchButtonView() {
        // 3. Viewの回転
        self.rotate(to: .search)
        // 4. delegateでRootViewControllerにタップイベントを伝播
        self.delegate?.switchButtonView(self, didTapButton: .search)
    }
    
    @objc
    private func tapToHomeButtonView() {
        // 3. Viewの回転
        self.rotate(to: .home)
        // 4. delegateでRootViewControllerにタップイベントを伝播
        self.delegate?.switchButtonView(self, didTapButton: .home)
    }
    
    /// SwitchButtonViewの回転処理
    /// - Parameter type: タップされたボタンのタイプ。タップするボタンのタイプ似合わせて回転方向を変える。
    private func rotate(to type: ContentType) {
        UIView.animate(withDuration: 0.3) {
            switch type {
            case .search:
                // 検索画面への遷移だったら時計回りで回転
                self.transform = CGAffineTransform(rotationAngle: -.pi * 0.999) // 反転時は時計回りにしたいので半回転しきらないようにしておく
            case .home:
                // ホーム画面への遷移だったら反時計回りで戻す
                self.transform = .identity
            }
        }
    }
}

protocol SwitchButtonViewDelegate {
    func switchButtonView(_ switchButtonView: SwitchButtonView, didTapButton destinationContentType: ContentType)
}

/// スイッチングで表示するコンテンツのタイプ
enum ContentType: Int {
    case home
    case search
}
```

**注意点**

回転を半分きっかりさせてしまうと、`CGAffineTransform.identity`で戻すときに回転が逆にならないので、半回転よりわずかに少なくしておきます。

## 画面遷移
1. Outletをセットして、SwitchButtonViewのdelegateもセットする。
2. delegateメソッドからContainerViewに遷移先のVCをセットする関数を呼ぶ。
3. `UIView.transition`で画面遷移を実行する。 

```Swift:RootViewController.swift

import UIKit

final class RootViewController: UIViewController {

    // 1. OutletとSwitchButtonViewのdelegatewをセット
    @IBOutlet private weak var switchButtonView: SwitchButtonView! {
        willSet {
            newValue.delegate = self
        }
    }
    @IBOutlet private weak var containerView: UIView!
    
    var chiledViewControllers: [UIViewController] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.switchButtonView.setup()
        self.setContentViewController(contentType: .home)
    }
}

extension RootViewController: SwitchButtonViewDelegate {
    // 2. delegateメソッドからContainerViewに遷移先のVCをセットする関数を呼ぶ。
    func switchButtonView(_ switchButtonView: SwitchButtonView, didTapButton destinationContentType: ContentType) {
        self.setContentViewController(contentType: destinationContentType)
    }
}

// MARK: - setup
extension RootViewController {
    // 3. UIView.transitionでコンテナ内に表示するViewの切り替え
    /// 指定されたコンテンツのVCにアニメーション遷移する
    /// - Parameter contentType: 表示するコンテンツ home/search
    private func setContentViewController(contentType: ContentType) {
        let vc = self.chiledViewControllers[contentType.rawValue]
        self.addChild(vc)
        let currentView = self.containerView.subviews.first ?? UIView()
        UIView.transition(
            from: currentView,
            to: vc.view,
            duration: 0.3,
            options: .transitionCrossDissolve,
            completion: { [weak self] _ in
                guard let `self` = self else { return }
                // addChildをしたあとはトランジションが完了したらdidMoveを呼ばなきゃいけない
                // 呼んでいない場合、追加したViewControllerのviewWillAppear:が動作しない場合がある
                vc.didMove(toParent: self)
                if let view = self.containerView.subviews.first {
                    view.translatesAutoresizingMaskIntoConstraints = false
                    // 制約をつけ直す
                    view.topAnchor.constraint(equalTo: self.containerView.topAnchor).isActive = true
                    view.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor).isActive = true
                    view.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor).isActive = true
                    view.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor).isActive = true
                }
        })
    }
}

```
# アプリでの利用イメージ
個人開発しているアプリでの利用イメージです👇

![実装結果_利用イメージ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/287671/c4d9403c-bac4-be1c-a172-4cf2c6c905ac.gif)


# リポジトリ
ソースコードは以下です

https://github.com/ichikawa7ss/SwitchableViewController

# 参考
- [自前コンテナでaddChildViewController:するときdidMoveToParentViewController:はやはり使うべき](https://qiita.com/yimajo/items/a5e16fa0f9c332a55ccf)