ContainerViewController / Custom TabBarControllerを作る

TabBarをカスタマイズしたい

標準のUITabBarControllerだと、痒いところをカスタマイズするのが大変です。 TabBarをUIViewのサブクラスとして自作することで、自由にUIやアニメーションの実装ができるようになると思います。 UITabBarController - UIKit | Apple Developer Documentation

TabViewControllerの挙動

iOS5からContainerViewControllerを作成するAPIが公開されたようで、それを使ってTabViewControllerを自作していきます。

f:id:churabou:20191029215038p:plain:w300

TabViewControllerの挙動を整理しておくと、以下のようにタブ切り替えのUIを実現しつつ適切なタイミングでライフサイクルメソッドが呼ばれることになります。

  • タブをタップすると画面が切り替わる
  • 切り替わる際は以下の順でライフサイクルイベントが発火する

    • 遷移先ViewControllerのViewDidDisappear
    • 遷移元ViewControllerのviewWillDisappear
    • 遷移元ViewControllerのviewDidDisappear
    • 遷移先ViewControllerのDidAppear
  • PushやModalで遷移すると、TabViewControllerと選択されているViewControllerのViewDidAppearが呼ばれる

  • 同一タブを選択しても何も起こらない。

必要なメソッド

以下の3種類のUIViewControllerのメソッドを使うことで実現することができます。

  • willMove / didMove
  • addChild / removeFromParent
  • begeinAppearanceTransition / endAppearenceTransition

実装例

ViewControllerにTabViewとContentViewを配置します。

final class ContainerTapViewController: UIViewController {
    private let contentView = UIView()
    private let tabView = TabView()
    private let controllers: [UIViewController] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(contentView)
        view.addSubview(tabView)
    }
}

最初に選択したいViewControllerを追加します。

let controller = controllers[0]
controller.willMove(toParent: self)
controller.view.frame = view.bounds
addChild(controller)
contentView.addSubview(controller.view)

タグが切り替わった際に以下のようにメソッドを呼びます

func didSelectTag(_ selectedIndex: Int) {
    let fromController = selectedViewController
    let toController = controllers[selectedIndex]
    selectedViewController = toController
    self.remove(controller: fromController)
    self.add(controller: toController)
}

add, removeメソッドの実装はこのようにします。

func add(controller: UIViewController) {
    addChild(controller)
    controller.view.frame = view.bounds
    contentView.addSubview(controller.view)
    controller.didMove(toParent: self)
}

func remove(controller: UIViewController) {
    controller.willMove(toParent: nil)
    controller.removeFromParent()
    controller.view.removeFromSuperview()
}

またUITabBarViewConntrollerとライフサイクルイベントの発火タイミングを揃えるとこんな感じになります。 toViewController.begin, fromViewController.begin, fromViewController.end, toViewController.end とfromではさんであげる必要があります。

func didSelectTab(_ selectedIndex: Int) {
    guard let fromViewController = selectedViewController,
        viewControllers.count > selectedIndex else {
            return
    }

    let toViewController = viewControllers[selectedIndex]
    if fromViewController !== toViewController {
        toViewController.beginAppearanceTransition(true, animated: true)
        fromViewController.beginAppearanceTransition(false, animated: true)

        add(controller: toViewController)
        remove(controller: fromViewController)

        fromViewController.endAppearanceTransition()
        toViewController.endAppearanceTransition()

        selectedViewController = toViewController
    }
}

上記のようにお作法に従えばUITabViewControllerとほぼ同じなContainerViewControllerを作成することができます 実際に手を動かさないと理解をできないのとメソッドを呼ばなくても動いたりするのでちょっと勉強しにくい コピペで動くものを一応貼っておきます。

Custom TabBarViewController · GitHub

参考

ちゃんとドキュメントがあって助かった。

developer.apple.com

https://cocoacasts.com/managing-view-controllers-with-container-view-controllers

https://github.com/mixi-inc/iOSTraining/wiki/2.3-Custom-Container-View-Controller