Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save matthewrdev/2a4893b6fd8d988556b61ba6d9888ee4 to your computer and use it in GitHub Desktop.
Save matthewrdev/2a4893b6fd8d988556b61ba6d9888ee4 to your computer and use it in GitHub Desktop.
Intercepts the "soft" back button events from the iOS TopViewController and forwards them to the Page.OnBackButtonPressed for handling.
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(NavigationPage), typeof(MyApp.iOS.Renderers.InterceptBackButtonNavigationPageRenderer))]
namespace MyApp.iOS.Renderers
{
/// <summary>
/// This custom renderer overrides Xamarin.Forms implementation of the soft back navigation button and use the <see cref="Page.OnBackButtonPressed"/> result to decide if it should pop.
/// <para/>
/// Xamarin.Forms seals the navigation handling and does not provide a mechanism to override it. This class wires into the places where the toolbar icon is updated by Forms and replaces it with a custom back button with our own navigation handling.
/// <para/>
/// See:
/// * https://github.com/xamarin/Xamarin.Forms/blob/e9e97e2a240d4f47fff63bbe3590983b7c489ac7/Xamarin.Forms.Platform.iOS/Renderers/NavigationRenderer.cs#L831
/// </summary>
public class InterceptBackButtonNavigationPageRenderer : NavigationRenderer
{
private readonly FieldInfo _flyoutPageFieldInfo;
FlyoutPage FlyoutPage => _flyoutPageFieldInfo.GetValue(this) as FlyoutPage;
public InterceptBackButtonNavigationPageRenderer()
{
_flyoutPageFieldInfo = typeof(NavigationRenderer).GetField("_parentFlyoutPage", BindingFlags.Instance | BindingFlags.NonPublic);
}
enum NavigationEvent
{
Push,
Pop,
PopToRoot
}
private async void LeftNavigationItem_Clicked(object sender, System.EventArgs e)
{
if (Element is null)
{
return;
}
if (Element is INavigationPageController controller)
{
if (controller.StackDepth <= 1)
{
if (FlyoutPage != null)
{
FlyoutPage.IsPresented = !FlyoutPage.IsPresented;
}
}
else
{
var lastPage = controller.Pages?.LastOrDefault();
if (lastPage != null
&& lastPage.SendBackButtonPressed() == false)
{
await controller.PopAsyncInner(true);
}
}
}
}
private bool TryGetTopViewController(out UIViewController topViewController)
{
topViewController = null;
try
{
if (TopViewController is null || TopViewController.Handle == IntPtr.Zero)
{
return false;
}
topViewController = TopViewController;
return true;
}
catch (ObjectDisposedException)
{
Debug.WriteLine("Unable to retrieve the top-most view controller as it has been disposed. This is temporary and occurs while the App.MainPage is being changed. Subsequent calls should always succeed.");
}
return false;
}
private void BindNavigationIcon(NavigationEvent navigationEvent, Page page)
{
if (!TryGetTopViewController(out var topViewController))
{
return;
}
topViewController.NavigationItem.LeftBarButtonItem = null;
if (Element is NavigationPage navigationPage)
{
var currentPagesCount = navigationPage.Pages.Count();
switch (navigationEvent)
{
case NavigationEvent.Push:
break;
case NavigationEvent.Pop:
if (navigationPage.Pages.LastOrDefault() == page)
{
// Page is outgoing however is still on stack, reduce page count so we cater for it when detecting is we should setup the flyout menu.
currentPagesCount--;
}
break;
case NavigationEvent.PopToRoot:
currentPagesCount = 1;
break;
}
if (currentPagesCount <= 1)
{
if (FlyoutPage != null)
{
ApplyFlyoutMenuButton(topViewController);
}
}
else
{
ApplyBackNavigationButton(topViewController);
}
}
}
private void ApplyBackNavigationButton(UIViewController topViewController)
{
var backButton = new UIBarButtonItem();
backButton.Clicked += LeftNavigationItem_Clicked;
backButton.Image = UIImage.FromBundle("arrow_back"); // TODO: Provide your own "back navigation" icon here.
topViewController.NavigationItem.LeftBarButtonItem = backButton;
}
private void ApplyFlyoutMenuButton(UIViewController topViewController)
{
var flyoutButton = new UIBarButtonItem();
flyoutButton.Clicked += LeftNavigationItem_Clicked;
flyoutButton.Image = UIImage.FromBundle("icon_menu"); // TODO: Provide your own "flyout menu" icon here.
topViewController.NavigationItem.LeftBarButtonItem = flyoutButton;
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
BindNavigationIcon(NavigationEvent.Push, null);
}
protected override async Task<bool> OnPopViewAsync(Page page, bool animated)
{
var result = await base.OnPopViewAsync(page, animated);
BindNavigationIcon(NavigationEvent.Pop, page);
return result;
}
protected override async Task<bool> OnPopToRoot(Page page, bool animated)
{
var result = await base.OnPopToRoot(page, animated);
BindNavigationIcon(NavigationEvent.PopToRoot, page);
return result;
}
protected override async Task<bool> OnPushAsync(Page page, bool animated)
{
var result = await base.OnPushAsync(page, animated);
BindNavigationIcon(NavigationEvent.Push, page);
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment