Implementing “double tap tab bar to scroll to top”

I got inspired by Ivory for Mastodon and implemented my solution in SwitchBuddy.

Published: Feb. 4, 2023
App Store

My SwitchBuddy app has a couple of “main” screens that have a lot of content, and the user can scroll pretty far. And while you can tap the top of the screen to scroll to the top across iOS - I don’t think many people use it. Not to mention it is not super convenient to do.

Ivory (and Tweetbot) have this cool feature where you can tap any tab bar item to scroll to the top quickly. I have gotten used to it and wanted something similar in SwitchBuddy.

My solution is based on the method func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) provided by the UITabBarControllerDelegate. This gets called each time the user taps a tab bar item. The main problem to solve is to detect double taps correctly.

I initially started with just tracking the last item that was tapped and then seeing if the next tap is on the same item. If so, that indicated double tap. But with this, you could have any amount of time between the taps that would get detected. So I have added a timestamp to the mix.

Implementation

These two properties to keep track of state:

private var lastSelectedItem: UITabBarItem? {
    didSet {
        guard lastSelectedItem != nil else { return }
        lastSelectTimeStamp = CFAbsoluteTimeGetCurrent()
    }
}

private var lastSelectTimeStamp: CFAbsoluteTime?

And below is the implementation of the delegate method:

extension MainTabBarController: UITabBarControllerDelegate {
    override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        if item === lastSelectedItem {
            if let lastTime = lastSelectTimeStamp {
                let diff = CFAbsoluteTimeGetCurrent() - lastTime

                // Empirically 3 seconds seems like a good value
                if diff < 3 {
                    if let nvc = selectedViewController as? UINavigationController, let scrollsToTopVC = nvc.topViewController as? SupportsScrollToTop {
                        scrollsToTopVC.scrollToTop()
                    }
                }

                lastSelectedItem = nil
                lastSelectTimeStamp = nil
            }

            return
        } else {
            lastSelectTimeStamp = nil
        }

        lastSelectedItem = item
    }
}

Perhaps not super elegant, but works pretty well for me.

To accomplish the actual scrolling, I have this simple protocol:

protocol SupportsScrollToTop where Self: UIViewController {
    func scrollToTop()
}

And then, I can conform view controllers that are part of the tab bar, and scrolling to the top makes sense.

Here is an example from the games screen:

extension GamesListViewController: SupportsScrollToTop {
    func scrollToTop() {
        collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
    }
}

And that’s it. If you have a more straightforward solution - feel free to share it.

Filip Němeček profile photo

WRITTEN BY

Filip Němeček @nemecek_f@iosdev.space

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀

iOS blogger and developer with interest in Python/Django. Want to see most recent projects? 👀