はじめに
UIKit でタブバー選択時にアイコンのアニメーションの実装をすることがあったので、備忘録として記事を書きます!
経緯
元々はタブバーのアニメーションは、以下ライブラリを利用して実装していました。
ただ、storyboad 必須のライブラリで、コードのみの実装できませんでした。
個人的に storyboad はメンテしづらくて好きじゃないので、コードベースでタブバーを実装しようと思うと、animated-tab-bar
の依存を剥がさざるを得ませんでした。
そこで、タブバーのアニメーションは UIKit で直接実装することにしました。
ゴール
実装した結果はこんな感じです。
タブバーを選択したら、選択されたアイコンがバウンドするようなアニメーションを動かします。
実装する
タブバーの ViewController を用意する
まずはタブバーの ViewController クラスを実装します。
今回の例では、以下タブを用意します。
- Home
- Chat
- Graph
- Setting
実装内容はこちらです。
import UIKit
final class MainTabBarController: UITabBarController {
// MARK: - lifecycle method
override func viewDidLoad() {
super.viewDidLoad()
setupTab()
}
}
// MARK: - private method
private extension MainTabBarController {
func setupTab() {
guard let mainVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .MainViewController) as? MainViewController,
let chatVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .ChatViewController) as? ChatViewController,
let graphVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .DispViewController) as? DispViewController,
let settingVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .SettingViewController) as? SettingViewController
else {
assertionFailure("Not found require viewController.")
return
}
mainVC.tabBarItem = getTabBarItem(title: "Home",
systemImageName: "homekit",
tag: 0)
chatVC.tabBarItem = getTabBarItem(title: "Chat",
systemImageName: "message.fill",
tag: 1)
graphVC.tabBarItem = getTabBarItem(title: "Graph",
systemImageName: "chart.xyaxis.line",
tag: 2)
settingVC.tabBarItem = getTabBarItem(title: "Setting",
systemImageName: "gearshape.fill",
tag: 3)
viewControllers = [mainVC, chatVC, graphVC, settingVC]
}
func getTabBarItem(title: String, systemImageName: String, tag: Int) -> UITabBarItem {
let tabBarItem = UITabBarItem(title: title, image: getTabIcon(systemImageName), tag: tag)
tabBarItem.selectedImage = getTabIcon(systemImageName)
return tabBarItem
}
func getTabIcon(_ iconName: String) -> UIImage? {
return UIImage(systemName: iconName)
}
}
タブバー選択時のアニメーションを実装する
ここからが本題で、タブバー選択時に選択された要素をアニメーションさせていきます。
まずタブバーで選択されたときにアニメーションを行うので、UITabBarControllerDelegate
に準拠して、選択されたタイミングを検知できるようにします。
追加したdelegateメソッドのtabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool
では、タブバーのいずれかの要素をタップされた瞬間にトリガーされます。
先ほどのコードに、タブバーのいずれかの要素をタップされたときに検知できるコードを追加します。
import UIKit
final class MainTabBarController: UITabBarController {
// MARK: - lifecycle method
override func viewDidLoad() {
super.viewDidLoad()
setupTab()
+ self.delegate = self
}
}
+ extension MainTabBarController: UITabBarControllerDelegate {
+
+ func tabBarController(_ tabBarController: UITabBarController,
+ shouldSelect viewController: UIViewController) -> Bool {
+
+ return true
+ }
+ }
続いて、タブバーで選択された要素をアニメーションさせる処理を追加していきます。
extension MainTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool {
+ animateTabItem(tabBarController, shouldSelect: viewController)
return true
}
+ private func animateTabItem(_ tabBarController: UITabBarController,
+ shouldSelect viewController: UIViewController) {
+ guard let item = viewController.tabBarItem,
+ let index = tabBarController.tabBar.items?.firstIndex(of: item),
+ let targetTabBarView = getBarItemView(tabBar: tabBarController.tabBar,
+ selectedIndex: index + 1)
+ else {
+ return
+ }
+
+ bounceAnimation(target: targetTabBarView)
+ }
+
+ private func getBarItemView(tabBar: UITabBar, selectedIndex: Int) -> UIView? {
+
+ let tabBarSubviews = tabBar.subviews.sorted(by: { $0.frame.minX < $1.frame.minX })
+ guard tabBarSubviews.indices.contains(selectedIndex) else { return nil }
+ return tabBarSubviews[selectedIndex]
+ }
+
+ private func bounceAnimation(target view: UIView) {
+
+ UIView.animate(withDuration: 0.1) {
+
+ view.transform = .init(scaleX: 1.2, y: 1.2)
+ } completion: {
+
+ guard $0 else { return }
+ UIView.animate(withDuration: 0.1) {
+
+ view.transform = .init(scaleX: 1, y: 1)
+ }
+ }
+ }
}
animateTabItem
メソッドで選択されたタブバーのボタンをアニメーションさせる処理を行います。
具体的にやっていることはまず、アニメーションしたいtabBarItemをUIView
型として取得します。
以下処理で、選択されたタブの画面(viewController
)からtabBarItem
を取得して、選択された画面のタブバーからみたときのインデックスを取得しています。
guard let item = viewController.tabBarItem,
let index = tabBarController.tabBar.items?.firstIndex(of: item),
let targetTabBarView = getBarItemView(tabBar: tabBarController.tabBar,
selectedIndex: index + 1)
else {
return
}
getBarItemView
メソッドでは、選択されたインデックスを使ってタブバーのsubviews
から選択されたタブのUIView
型で取得します。
private func getBarItemView(tabBar: UITabBar, selectedIndex: Int) -> UIView? {
let tabBarSubviews = tabBar.subviews.sorted(by: { $0.frame.minX < $1.frame.minX })
guard tabBarSubviews.indices.contains(selectedIndex) else { return nil }
return tabBarSubviews[selectedIndex]
}
タブバーのsubviews
を取得するときに、frame
をみてタブの左から右の順に並び替えていますが(画面でタブを見た時と同じ並びにする)
これを行っているのは、こちらの記事に記載されている通り時々subviews
でとれるviewの並びが画面通りの並びになっておらず選択したタブのインデックスと異なるタブが取れてしまうことがあるため、並び替えています。
let tabBarSubviews = tabBar.subviews.sorted(by: { $0.frame.minX < $1.frame.minX })
上記流れで、アニメーションさせたいUIView
が取得できたので、bounceAnimation
でアニメーションさせる実装を追加します。
private func bounceAnimation(target view: UIView) {
UIView.animate(withDuration: 0.1) {
view.transform = .init(scaleX: 1.2, y: 1.2)
} completion: {
guard $0 else { return }
UIView.animate(withDuration: 0.1) {
view.transform = .init(scaleX: 1, y: 1)
}
}
}
アニメーションの実装はシンプルで、
0.1秒間で選択したタブを1.2倍に膨らませて、その後0.1秒間で元の大きさに戻すようにアニメーションをしています。
最終的な実装が以下の通りです。
final class MainTabBarController: UITabBarController {
// MARK: - factory method
static func build() -> UIViewController {
let vc = MainTabBarController()
return vc
}
// MARK: - lifecycle method
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
setupTab()
}
}
extension MainTabBarController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) -> Bool {
animateTabItem(tabBarController, shouldSelect: viewController)
return true
}
private func animateTabItem(_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController) {
guard let item = viewController.tabBarItem,
let index = tabBarController.tabBar.items?.firstIndex(of: item),
let targetTabBarView = getBarItemView(tabBar: tabBarController.tabBar,
selectedIndex: index + 1)
else {
return
}
bounceAnimation(target: targetTabBarView)
}
private func getBarItemView(tabBar: UITabBar, selectedIndex: Int) -> UIView? {
let tabBarSubviews = tabBar.subviews.sorted(by: { $0.frame.minX < $1.frame.minX })
guard tabBarSubviews.indices.contains(selectedIndex) else { return nil }
return tabBarSubviews[selectedIndex]
}
private func bounceAnimation(target view: UIView) {
UIView.animate(withDuration: 0.1) {
view.transform = .init(scaleX: 1.2, y: 1.2)
} completion: {
guard $0 else { return }
UIView.animate(withDuration: 0.1) {
view.transform = .init(scaleX: 1, y: 1)
}
}
}
}
// MARK: - private method
private extension MainTabBarController {
func setupTab() {
guard let mainVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .MainViewController) as? MainViewController,
let chatVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .ChatViewController) as? ChatViewController,
let graphVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .DispViewController) as? DispViewController,
let settingVC = UIStoryboard(custom: .Main).instantiateViewController(custom: .SettingViewController) as? SettingViewController
else {
assertionFailure("Not found require viewController.")
return
}
mainVC.tabBarItem = getTabBarItem(title: "Home",
systemImageName: "homekit",
tag: 0)
chatVC.tabBarItem = getTabBarItem(title: "Chat",
systemImageName: "message.fill",
tag: 1)
graphVC.tabBarItem = getTabBarItem(title: "Graph",
systemImageName: "chart.xyaxis.line",
tag: 2)
settingVC.tabBarItem = getTabBarItem(title: "Setting",
systemImageName: "gearshape.fill",
tag: 3)
viewControllers = [mainVC, chatVC, graphVC, settingVC]
}
func getTabBarItem(title: String, systemImageName: String, tag: Int) -> UITabBarItem {
let tabBarItem = UITabBarItem(title: title, image: getTabIcon(systemImageName), tag: tag)
tabBarItem.selectedImage = getTabIcon(systemImageName)
return tabBarItem
}
func getTabIcon(_ iconName: String) -> UIImage? {
return UIImage(systemName: iconName)
}
}
以上で、目標のタブバー選択時のタブのアニメーションの実装が完了です!
終わり
タブバーのアニメーションの実装例を紹介しましたが、他にももっと良い実装があると思うので、ご指摘などありましたらぜひコメントいただけると幸いです🙏