TabのItemをタップして上部スクロールさせる
概要
タブのアイコンをタップした時に自動で最上部にスクロールさせる
実装
下準備
UITabBarControllerのviewControllersに任意の数のUINavigationControllerをセット
ここはどうでもいいので適宜お願いします
スクロールさせるケース
まず、どのような状態の時にスクロールさせるか整理します
スクロールさせるのは以下の一つのケースのみです
- navigationControllerのrootで同tabのアイコンをタップした場合
つまり、タブのアイコンをタップしても上記以外のケースではスクロールを起こしません。上記以外のケースは、
- navigationControllerのrootから別タブへ遷移
- navigationControllerのroot以外の画面から同タブのrootへ戻る
などなどですが、スクロールさせるケースが一つしかないので、そのケースのみで動くような条件を書けば良いわけです
スクロールさせる条件は
- 現在表示している画面がnavigationControllerのrootである
- 現在表示しているタブのアイコンをタップする
では一つずつ実装していきます
class ItemTapScrollTabController: UITabBarController { //アイコンタップでスクロールさせるためのラッパークラス
private var isRoot: Bool = false //表示中のviewControllerがrootかどうかのフラグ
private var currentItemIndex: Int = -1 //表示中のviewControllerのTabから見たときのインデックス
private var nextItemIndex: Int = -1 //遷移先のviewControllerのTabから見たときのインデックス
override func loadView() {
super.loadView()
delegate = self
}
}
extension ItemTapScrollTabController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
//アイコンをタップする時に呼ばれる
return true
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
//画面遷移後に呼ばれる
}
}
フラグ用のプロパティを作ったら中身を入れていきます
class ItemTapScrollTabController: UITabBarController {
private var isRoot: Bool = false
private var currentItemIndex: Int = -1
private var nextItemIndex: Int = -1
override func loadView() {
super.loadView()
delegate = self
}
}
extension ItemTapScrollTabController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
currentItemIndex = tabBarController.selectedIndex
return true
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
nextItemIndex = tabBarController.selectedIndex
}
}
tabBarController.selectedIndex
ですがshouldSelect内では現在表示中のviewControllerのUITabBarControllerから見たときのインデックス、didSelect内では遷移先のviewControllerのUITabBarControllerから見たときのインデックスが取得されます
class ItemTapScrollTabController: UITabBarController {
private var isRoot: Bool = false
private var currentItemIndex: Int = -1
private var nextItemIndex: Int = -1
override func loadView() {
super.loadView()
delegate = self
}
}
extension ItemTapScrollTabController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
currentItemIndex = tabBarController.selectedIndex
if let nav = viewController as? UINavigationController,
let current = nav.visibleViewController,
let currentStackIdx = nav.viewControllers.firstIndex(of: current) {
isRoot = currentStackIdx == 0
}
return true
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
nextItemIndex = tabBarController.selectedIndex
guard currentItemIndex == nextItemIndex, isRoot else { return }
//currntItemIndex == nextItemIndex → 表示中のviewControllerのタブから見たインデックスと遷移先のviewControllerのタブから見たインデックスが同じ, 表示中のタブと同じタブを選択
//isRoot → 表示中のviewControllerがnavigationControllerのrootである
}
}
shouldSelectのif let nav...
内では、まず
- UINavigationControllerにキャスト
- 現在表示中のviewControllerを取得
- そのviewControllerがUINavigationControllerから見たときのインデックスを取得
を行っています
そして、そのインデックスが0であればUINavigationControllerのroot、つまり、表示中のviewControllerがUINavigationControllerのrootViewControllerであることを意味するのでisRootをtrueにしています
次のdidSelectのguard currentItemIndex == ...
で上記の2つの条件をコードにおとしこんでいます
navigationController.visibleViewControllerですが、
と、変な挙動があるので注意が必要です(理由は分からないので偉い人教えてください)
shouldSelectでは表示中のviewControllerを取得したいので、別タブへ遷移した場合は正しくviewControllerが取得できません
本コードでもshouldSelect内のvisibleViewController
で取得されるviewControllerは状況によって変わってしまいますが、currentItemIndex == nextItemIndex
の部分で、同タブの遷移の場合しか処理を行わないようにしているため、動きとしては問題ありません
ここまくればあとはスクロール処理を書くだけです。スクロール処理はUIScrollView, UITableView, UICollectionViewなど複数のコンポーネントで実装される可能性があるので、プロトコルをきってそれぞれのviewControllerに実装させます
protocol Scrollable {
func scrollToTop()
}
class ViewController: UIViewController {
private let tableView: UITableView = .init()
}
extension ViewController: Scrollable {
func scrollToTop() {
tableView.scrollToRow(at: .init(row: 0, section: 0), at: .top, animated: true)
}
}
extension ItemTapScrollTabController: UITabBarControllerDelegate {
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
currentItemIndex = tabBarController.selectedIndex
if let nav = viewController as? UINavigationController,
let current = nav.visibleViewController,
let currentStackIdx = nav.viewControllers.firstIndex(of: current) {
isRoot = currentStackIdx == 0
}
return true
}
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
nextItemIndex = tabBarController.selectedIndex
guard currentItemIndex == nextItemIndex, isRoot else { return }
if let nav = viewController as? UINavigationController,
let current = nav.visibleViewController,
let scrollable = current as? Scrollable {
scrollable.scrollToTop()
}
}
}
終わり
思ったより挙動めんどくさくてうわーとなっていたのですが、自分の方では一旦上のコードで実装しています。あんまりテストしていないのでもしかしたら変な挙動があるかもしれません。
サンプルのGifをのせたいのですが容量オーバーでのせられないので(泣)、GitHubのリンクを置いておきます
参考
Discussion