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

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

- tabbarの既に開いているタブアイコンをタップすると、一番上までスクロールする
- ただしUINavigationControllerでの「戻る遷移」を邪魔しない

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25725/26ee2eb2-894c-f661-8e83-a10b01b8a647.gif)


## アプリの構造
- タブバー(UITabBarController)が複数の画面(UIViewController)を持っている
- 各UIViewControllerはUINavigationControllerで遷移する

## 注意点
概要の通りだが、
UINavigationControllerで遷移する場合、タブをタップ時のデフォルトの動きは
『UINavigationControllerのrootに戻る』であり、
このときはスクロールさせないようにしたい

## 実装
```swift

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たちの実装
```swift
// スクロールを持つプロトコル
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`がまだ
「今回選択されたタブ」ではなく「前回選択されたタブ」であるため
『既に開いているタブが選択された』が判定できない。

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


## 参考
- [tabBarController:shouldSelectViewController](https://developer.apple.com/documentation/uikit/uitabbarcontrollerdelegate/1621166-tabbarcontroller?language=objc)
- [UITableviewの表示を一番上にスクロールさせるには](https://ja.stackoverflow.com/questions/40854/swift4%E3%81%A7uitableview%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E4%B8%80%E7%95%AA%E4%B8%8A%E3%81%AB%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%95%E3%81%9B%E3%82%8B%E3%81%AB%E3%81%AF)

- [自分（ViewController）がナビゲーションコントローラーのルートかどうかを判定する処理](https://scrapbox.io/iOS-App-Development/%E8%87%AA%E5%88%86%EF%BC%88ViewController%EF%BC%89%E3%81%8C%E3%83%8A%E3%83%93%E3%82%B2%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%B3%E3%83%B3%E3%83%88%E3%83%AD%E3%83%BC%E3%83%A9%E3%83%BC%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%88%E3%81%8B%E3%81%A9%E3%81%86%E3%81%8B%E3%82%92%E5%88%A4%E5%AE%9A%E3%81%99%E3%82%8B%E5%87%A6%E7%90%86)
- [UITabBarControllerDelegateのdidSelectViewControllerが呼ばれない](https://qiita.com/hikaruna/items/3d28e701e91c1c1c2715)