概要
iOSアプリでよく見るこの動きをサクッと実装します
- tabbarの既に開いているタブアイコンをタップすると、一番上までスクロールする
- ただしUINavigationControllerでの「戻る遷移」を邪魔しない
アプリの構造
- タブバー(UITabBarController)が複数の画面(UIViewController)を持っている
- 各UIViewControllerはUINavigationControllerで遷移する
注意点
概要の通りだが、
UINavigationControllerで遷移する場合、タブをタップ時のデフォルトの動きは
『UINavigationControllerのrootに戻る』であり、
このときはスクロールさせないようにしたい
実装
class SampleTabBarController: UITabBarController, UITabBarControllerDelegate {
// shouldSelectでは、現在開いている画面がnavigationControllerのルートかを判定するまで。
var shouldScroll = false
func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool {
self.shouldScroll = false
// 表示しているvcがnavigationControllerルートのときはスクロールさせる
// ルート以外は、navigationControllerの戻る機能を優先しスクロールさせない
if let navigationController: UINavigationController = viewController as? UINavigationController {
let visibleVC = navigationController.visibleViewController!
if let index = navigationController.viewControllers.index(of: visibleVC), index == 0 {
shouldScroll = true
}
}
// 遷移を許可するためのtrueを返す
return true
}
// didSelectで、選択されたタブが、前回と同様なら、shouldSelectの結果(shouldScroll)も考慮し、スクロールさせる
var lastSelectedIndex = 0
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
guard self.shouldScroll else { return }
if self.lastSelectedIndex == tabBarController.selectedIndex {
if let navigationController: UINavigationController = viewController as? UINavigationController {
let visibleVC = navigationController.visibleViewController!
if let scrollableVC = visibleVC as? ScrollableProtocol {
scrollableVC.scrollToTop()
}
}
}
self.lastSelectedIndex = tabBarController.selectedIndex
}
}
各タブのViewControllerたちの実装
// スクロールを持つプロトコル
protocol ScrollableProtocol {
func scrollToTop()
}
// ↑のように protocolで縛ることで、各画面のスクロールが
// scrollViewだろうが、tableViewやcollectionViewだろうが、
// tabbarControllerは意識せずに実行できる
class AAAViewController: ScrollableProtocol {
// scrollViewを持つViewController
@IBOutlet weak var scrollView: UIScrollView!
func scrollToTop() {
scrollView.setContentOffset(CGPoint(x:0, y:0 - scrollView.contentInset.top), animated: true)
}
}
class BBBViewController: ScrollableProtocol {
// tableViewを持つViewController
@IBOutlet weak var tableView: UITableView!
func scrollToTop() {
let indexPath = IndexPath(row: 0, section: 0)
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
}
}
class CCCViewController: ScrollableProtocol {
// collectionViewを持つViewController
@IBOutlet weak var collectionView: UICollectionView!
func scrollToTop() {
let indexPath = IndexPath(row: 0, section: 0)
collectionView.scrollToItem(at: indexPath, at: .top, animated: true)
}
}
実装の解説と、改善したいと思っているところ
UINavigationControllerで遷移する場合、タブをタップ時のデフォルトの動きは
『UINavigationControllerのrootに戻る』であり、このときはスクロールさせないようにしたい
この動きがやや面倒で
今回の実装ではTabBarControllerのshouldSelect
とdidSelect
で判定を行っている。
didSelect
の発火時では、すでにUINavigationControllerのルートに移動してしまっているため、
UINavigationControllerで遷移先のVCに居たとしても常にスクロールが走ってしまう。
一方でshouldSelect
ではtabBarController.selectedIndex
がまだ
「今回選択されたタブ」ではなく「前回選択されたタブ」であるため
『既に開いているタブが選択された』が判定できない。
この辺をきれいに改善する方法があれば教えてほしいです。