Instantly share code, notes, and snippets.
Created
November 18, 2021 01:25
-
Star
(3)
3
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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