LoginSignup
22
11

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-07-11

概要

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がまだ
「今回選択されたタブ」ではなく「前回選択されたタブ」であるため
『既に開いているタブが選択された』が判定できない。

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

参考

22
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
11