Tab Reselection in Xamarin.Forms, Part 1

It’s getting to be pretty common these days to have an app with tab navigation to assign some special meaning to tapping on the active tab selector again — often something like pop to the root of the navigation stack, scroll to the top or load fresh content.  Unfortunately, Xamarin.Forms does not expose a way to detect this gesture, so we can’t support this pattern in our Xamarin.Forms apps without platform-specific code.

However since the renderers for TabbedPage can be derived from to add some behavior, we can fix that, and add an event in our cross-platform code that fires whenever a tab is double-tapped. This post will only handle Android and iOS. UWP will be handled in a subsequent post.

The Common Part

This is what we want to end up in our cross-platform code:

public partial class MainPage : TabbedPage
{
    // ... other stuff snipped out...

    public event Action<Page> OnTabReselected;

    // Will be called by custom renderers
    public void NotifyTabReselected()
    {
        OnTabReselected?.Invoke(CurrentPage);
    }
}

OnTabReselected will be used by other parts of the code to be notified on reselection. We’re going to use NotifyTabReselected to communicate from the custom renderers back to the PCL.

Tab Reselect for iOS

The Xamarin.Forms iOS TabbedRenderer is pretty easy to customize.  We will set it up to notify us on all tab selections, and it’s up to us to remember the last one. Of course we also need to figure out the initially selected tab, so that if the user’s first tap is on the initially-active tab, we’ll fire the event.

    public class MainTabPageRenderer : TabbedRenderer
    {
        private UIKit.UITabBarItem _prevItem;

        public override void ViewDidAppear(bool animated)
        {
            base.ViewDidAppear(animated);

            if (SelectedIndex < TabBar.Items.Length)
                _prevItem = TabBar.Items[SelectedIndex];
        }

        public override void ItemSelected(UIKit.UITabBar tabbar,
                                          UIKit.UITabBarItem item)
        {
            if (_prevItem == item && Element is MainPage)
            {
                var mainTabPage = Element as MainPage;
                mainTabPage.NotifyTabReselected();
            }
            _prevItem = item;
        }
    }

We use ViewDidAppear to initialize _prevItem. There are other overrides that would probably work; we just need on that is late enough that TabBar and SelectedIndex are initialized, and early enough that ItemSelected won’t get called first. ViewDidAppear fits the bill. OnElementChanged is called too early, so TabBar.Items is not yet available.

Overall, pretty straightforward. Onward!

Tab Reselect for Android

Unfortunately things aren’t quite as clean here with the switch to AppCompat and the different tab implementation.
The TabbedPageRenderer looks like it was designed to allow extensibility, but that extensibility was never quite exposed in any obvious way. However it is doable, but it’s relies on implementation details that could change.

It’s useful to look at the link above, and see that there is an method that looks like it’s designed to be called when a tab is reselected — called OnTabReselected.  If you trace through, you’ll see that that method is wired up to receive those events, and then do absolutely nothing.  What we’re going to do is to replace our own handler for that one.

This approach works as long as you’re careful to understand what you’re doing and to be extra careful anytime you pick up a new version of Xamarin.Forms.  If a new version ever starts doing anything with OnTabReselected you’ll need to rethink what you’re doing. With any luck, that will be because there is a fully-supported way to hook OnTabReselected events, and we can stop using reflection! In the meantime, this has been tested to work with Xamarin.Forms 2.3.4 for Android AppCompat (and earlier).

With that in mind, here’s the custom renderer implementation for Android:

public class MainTabPageRenderer : TabbedPageRenderer, TabLayout.IOnTabSelectedListener
{
    void TabLayout.IOnTabSelectedListener.OnTabReselected(TabLayout.Tab tab)
    {
        if (Element is MainPage)
        {
            var mainTabPage = Element as MainPage;
            mainTabPage.NotifyTabReselected();
        }
    }
}

This works because the Xamarin.Forms TabbedPageRenderer (at least the AppCompat version) uses itself as the listener for tab selection notifications, and implements TabLayout.IOnTabSelectedListener itself.  All we’re doing is overriding one of interface methods. So all other listeners on MainTabPageRenderer will be exactly the same as before.

Wrapping Up

For a fairly basic use case, that’s all you need.  Add the ExportRenderer attributes to the custom renderers, and it’ll all work. See complete project at https://github.com/david-js/XFTabReselect.

However if you use NavigationPages as the children of your TabbedPage, there’s an additional complication that I will deal with in a follow-up post.

Update: See the follow-up post for the exciting conclusion, handling UWP and NavigationPages!
 

Author