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