【Swift】iOSアプリでよく見る『既に開いているタブをタップして、一番上までスクロール』の実装方法


概要

iOSアプリでよく見るこの動きをサクッと実装します


  • tabbarの既に開いているタブアイコンをタップすると、一番上までスクロールする

  • ただしUINavigationControllerでの「戻る遷移」を邪魔しない

ezgif.com-video-to-gif.gif


アプリの構造


  • タブバー(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のshouldSelectdidSelectで判定を行っている。

didSelectの発火時では、すでにUINavigationControllerのルートに移動してしまっているため、

UINavigationControllerで遷移先のVCに居たとしても常にスクロールが走ってしまう。

一方でshouldSelectではtabBarController.selectedIndexがまだ

「今回選択されたタブ」ではなく「前回選択されたタブ」であるため

『既に開いているタブが選択された』が判定できない。

この辺をきれいに改善する方法があれば教えてほしいです。


参考