Skip to main content
  1. Posts/

UITabBarのアイコンを、タップしたときにフェードアニメーションさせる方法

Table of Contents

iOSアプリで、タブのアイコンをタップしたときにどうしてもアニメーションさせることがあるかもしれません。デフォルトではアニメーションしないのですが(おそらくタブをタップした時にユーザーが見ているのはコンテンツであってタブではないからだと思いますが)、それでもアニメーションさせたいことがあります。その方法をまとめます。

方針 #

UITabBarのボタンは、NormalからSelectedの状態変化のときにアニメーションすることができません。 そのため、CALayerを使ってアニメーションさせる必要があります。 タブをタップしたときに、以下の処理を行います

  • Tabの画像レイヤーを一旦、透明にする(imageViewを透明にすると、全て見えなくなるので)
  • TabにNormalとSelectedの画像を表示するレイヤーを挿入する
  • Normal←→Selectedのディゾルブアニメーションをする
  • アニメーションが終了したら、画像レイヤーの不透明度を1.0に戻す

これだけです。

実装 #

final class CustomTabViewController: UITabBarController {
    override func viewDidLoad() {
        super.viewDidLoad()
        delegate = self
    }
}

extension CustomTabViewController: UITabBarControllerDelegate {
    // shouldSelectでアニメーションさせる。
    // didSelectでは、アニメーションさせるには手遅れ
    func tabBarController(_ tabBarController: UITabBarController,
                          shouldSelect viewController: UIViewController) -> Bool {
        animateTabItem(tabBarController, shouldSelect: viewController)
        return true
    }

    // アニメーションさせる
    private func animateTabItem(_ tabBarController: UITabBarController,
                                shouldSelect viewController: UIViewController) {
        // 変更前後のindexを取得
        let selectedIndex = tabBarController.selectedIndex
        let nextIndex: Int = (tabBarController.viewControllers?.firstIndex(of: viewController))!

        // TabBarのButtonを取得する
        // TabBarのsubviewsにはUITabBarButtonがいるものの、
        // このクラスは公開されていないため、UIControlにキャストして使う、UIViewでも良い
        let tabBarButtons = tabBarController.tabBar.subviews.compactMap { $0 as? UIControl }
        let currentButton = tabBarButtons[selectedIndex]
        let nextButton = tabBarButtons[nextIndex]

        // ボタン画像、ノーマル画像、選択済み画像のレイヤーを取得する
        guard let (cImage, cNormal, cSelected) =
                createImageLayer(tabBarController, 
                                 index: selectedIndex,
                                 button: currentButton),
              let (nImage, nNormal, nSelected) =
                createImageLayer(tabBarController, 
                                 index: nextIndex,
                                 button: nextButton)
        else {
            return
        }

        // CAアニメーションのインスタンスを生成
        let duration = 0.15
        let fadeout = CABasicAnimation(keyPath: "opacity")
        fadeout.fromValue = 1.0
        fadeout.toValue = 0.0
        fadeout.duration = duration
        let fadein = CABasicAnimation(keyPath: "opacity")
        fadein.fromValue = 0.0
        fadein.toValue = 1.0
        fadein.duration = duration

        // ボタン画像のレイヤーを、一旦透明にする
        cImage.opacity = 0.0
        nImage.opacity = 0.0

        // ディゾルブアニメーション定義開始
        CATransaction.begin()

        // アニメーション終了後の処理を登録
        CATransaction.setCompletionBlock {
            // ボタン画像を不透明に戻す
            cImage.opacity = 1.0
            nImage.opacity = 1.0

            // アニメーションで使ったレイヤーを削除
            cNormal.removeFromSuperlayer()
            cSelected.removeFromSuperlayer()
            nNormal.removeFromSuperlayer()
            nSelected.removeFromSuperlayer()
        }

        // アニメーションを登録
        cNormal.add(fadein, forKey: nil)
        cSelected.add(fadeout, forKey: nil)
        nNormal.add(fadeout, forKey: nil)
        nSelected.add(fadein, forKey: nil)

        // アニメーション後の値を設定
        cNormal.opacity = 1.0
        cSelected.opacity = 0.0
        nNormal.opacity = 0.0
        nSelected.opacity = 1.0

        // アニメーションの実行
        CATransaction.commit()
    }

    // TabBarButtonの画像レイヤー、
    // Normal状態の画像レイヤー、
    // Selected状態の画像レイヤーを生成する
    private func createImageLayer(_ tabBarController: UITabBarController,
                                  index: Int,
                                  button: UIControl) -> (CALayer, CALayer, CALayer)? {
        // buttonから画像レイヤーを取得する
        // UI仕様が変わったら失敗するので、諸刃の剣ではある
        guard let tabBarItem = tabBarController.viewControllers?[index].tabBarItem,
              let imageLayer = button.layer.sublayers?.first
        else {
            return nil
        }

        // Normal stateの画像レイヤーを作る
        let normalImageLayer = CALayer()
        normalImageLayer.frame = imageLayer.frame
        normalImageLayer.contents = tabBarItem.image?.cgImage
        button.layer.insertSublayer(normalImageLayer, below: imageLayer)

        // Selected stateの画像レイヤーを作る
        let selectedImageLayer = CALayer()
        selectedImageLayer.frame = imageLayer.frame
        selectedImageLayer.contents = tabBarItem.selectedImage?.cgImage
        button.layer.insertSublayer(selectedImageLayer, below: imageLayer)

        return (imageLayer, normalImageLayer, selectedImageLayer)
    }
}

備考 #

アニメーションの実装は、どうやら順序が大切のようです。 以下の順序です。

CATransaction.begin()
CATransaction.setCompletionBlock { } 
layer.add(animation, forKey: nil)
layer.someProperty = animation.toValue
CATransaction.commit()

とくに、アニメーションのaddより先に、completionをsetすること。 addしたあとは、アニメーションさせるプロパティを、アニメーション終了時のプロパティ値をセットしておくこと。 このあたりができていないと、画像がちらついたりうまくアニメーションしてくれなかったりします。


CALayerのアニメーション、最終的な実装自体はそんなに難しくないのですが、思ったような結果が出ず苦戦しました。