Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,19 @@ async Task UpdateFormsInnerNavigation(Page pageBeingRemoved)
[Internals.Preserve(Conditional = true)]
internal bool ShouldPopItem(UINavigationBar _, UINavigationItem __)
{
// Call ContentPage.SendBackButtonPressed() directly (not via NavPage.SendBackButtonPressed())
// to avoid triggering NavigationPage.OnBackButtonPressed → SafePop(), which would
// pop the MAUI stack while ShouldPopItem returns false (blocking UIKit's pop),
// causing a UIKit VC / MAUI navigation stack desync.
// Note: This bypasses NavigationPage subclass overrides of OnBackButtonPressed.
// Using _ignorePopCall to suppress SafePop was considered, but OnBackButtonPressed
// returns true for both "page handled it" and "SafePop handled it", making it
// impossible to distinguish cancellation from normal pop in ShouldPopItem.
if (NavPage?.CurrentPage?.SendBackButtonPressed() == true)
{
_uiRequestedPop = false;
return false;
}
_uiRequestedPop = true;
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,12 +184,15 @@ internal bool SendPop(UIViewController topViewController = null)
{
command.Execute(commandParameter);
}
_sendPopPending = false; // reset before returning
// Reset the iOS 26+ guard so subsequent back presses are not blocked.
_sendPopPending = false;
return false;
}

// Allow the page to intercept back navigation via OnBackButtonPressed
if (tracker.Value.Page?.SendBackButtonPressed() == true)
// Route through Shell.OnBackButtonPressed so that Shell subclass overrides
// are invoked consistently for both the navigation bar back button and the
// hardware/system back button.
if (_context.Shell?.SendBackButtonPressed() == true)
{
return false;
}
Expand Down
117 changes: 117 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue8296.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
namespace Maui.Controls.Sample.Issues
{
[Issue(IssueTracker.Github, 8296,
"ContentPage.OnBackButtonPressed is not invoked on iOS and MacCatalyst with NavigationPage",
PlatformAffected.iOS | PlatformAffected.macOS)]
public class Issue8296 : NavigationPage
{
public Issue8296() : base(new MainPage())
{
}

public class MainPage : ContentPage
{
internal static bool BackButtonPressedCalledReturnFalse;
readonly Label _returnFalseStatusLabel;

public MainPage()
{
Title = "HomePage";
BackButtonPressedCalledReturnFalse = false;

var navigateButton = new Button
{
Text = "Go to Second Page",
AutomationId = "NavigateButton"
};

navigateButton.Clicked += async (s, e) =>
await Navigation.PushAsync(new SecondPage());

var navigateReturnFalseButton = new Button
{
Text = "Go to Return False Page",
AutomationId = "NavigateReturnFalseButton"
};

navigateReturnFalseButton.Clicked += async (s, e) =>
await Navigation.PushAsync(new ReturnFalsePage());

_returnFalseStatusLabel = new Label
{
Text = "Waiting",
AutomationId = "ReturnFalseStatusLabel"
};

Content = new VerticalStackLayout
{
Children = { navigateButton, navigateReturnFalseButton, _returnFalseStatusLabel }
};
}

protected override void OnAppearing()
{
base.OnAppearing();
if (BackButtonPressedCalledReturnFalse)
{
_returnFalseStatusLabel.Text = "OnBackButtonPressed Called And Returned False";
BackButtonPressedCalledReturnFalse = false;
}
}
}

public class SecondPage : ContentPage
{
public SecondPage()
{
Title = "SecondPage";

var statusLabel = new Label
{
Text = "OnBackButtonPressed Not Called",
AutomationId = "BackButtonPressedLabel"
};

Content = new VerticalStackLayout
{
Children = { statusLabel }
};
}

protected override bool OnBackButtonPressed()
{
var label = (Label)((VerticalStackLayout)Content).Children[0];
label.Text = "OnBackButtonPressed Called";
// Return true to prevent navigation so the label stays visible for test verification
return true;
}
}

public class ReturnFalsePage : ContentPage
{
public ReturnFalsePage()
{
Title = "ReturnFalsePage";

Content = new VerticalStackLayout
{
Children =
{
new Label
{
Text = "Press back to test OnBackButtonPressed returning false",
AutomationId = "ReturnFalsePageLabel"
}
}
};
}

protected override bool OnBackButtonPressed()
{
MainPage.BackButtonPressedCalledReturnFalse = true;
// Return false to allow navigation to proceed
return false;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,8 @@ public void ClickingOnMenuItemInRootDoesNotCrash_NavPageVersion()
public void ClickingOnMenuItemInRootDoesNotCrash_TabPageVersion()
{
App.WaitForElement(StillHereId);
#if ANDROID || WINDOWS // On Android and Windows, two back navigation actions are needed because the back button's position is the same for both navigation and flyout pages. This requires a double navigation to return to the root page.
App.TapBackArrow();
App.WaitForElement(StillHereId);
#endif
App.TapBackArrow();

App.WaitForElement(StartTabPageTestId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests.Issues
{
public class Issue8296 : _IssuesUITest
{
public Issue8296(TestDevice device) : base(device) { }

public override string Issue => "ContentPage.OnBackButtonPressed is not invoked on iOS and MacCatalyst with NavigationPage";

protected override bool ResetAfterEachTest => true;

[Test]
[Category(UITestCategories.Navigation)]
public void OnBackButtonPressedShouldBeInvokedOnIOSWithNavigationPage()
{
// Navigate to the second page
App.WaitForElement("NavigateButton");
App.Tap("NavigateButton");

// Wait for the second page to appear
App.WaitForElement("BackButtonPressedLabel");

// Verify the initial state
var initialText = App.FindElement("BackButtonPressedLabel").GetText();
Assert.That(initialText, Is.EqualTo("OnBackButtonPressed Not Called"),
"Label should show 'Not Called' before pressing back.");

// Press the native back button
if (App is AppiumIOSApp iosApp && HelperExtensions.IsIOS26OrHigher(iosApp))
{
App.TapBackArrow(); // In iOS 26, the previous page title is not shown along with the back arrow, so we use the default back arrow
}
else
{
App.TapBackArrow(Device is TestDevice.iOS or TestDevice.Mac ? "HomePage" : "");
}

// OnBackButtonPressed should have been called, updating the label
// The second page stays visible because OnBackButtonPressed returns true
var updatedText = App.WaitForElement("BackButtonPressedLabel").GetText();
Assert.That(updatedText, Is.EqualTo("OnBackButtonPressed Called"),
"OnBackButtonPressed should have been invoked when pressing the back button.");
}

[Test]
[Category(UITestCategories.Navigation)]
public void OnBackButtonPressedReturnFalseShouldNavigateBack()
{
// Navigate to the return-false page
App.WaitForElement("NavigateReturnFalseButton");
App.Tap("NavigateReturnFalseButton");

// Wait for the return-false page to appear
App.WaitForElement("ReturnFalsePageLabel");

// Press the native back button
if (App is AppiumIOSApp iosApp && HelperExtensions.IsIOS26OrHigher(iosApp))
{
App.TapBackArrow();
}
else
{
App.TapBackArrow(Device is TestDevice.iOS or TestDevice.Mac ? "HomePage" : "");
}

// OnBackButtonPressed returned false, so navigation should proceed back to MainPage.
// The label on MainPage confirms OnBackButtonPressed was still called.
App.WaitForElement("ReturnFalseStatusLabel");
var statusText = App.FindElement("ReturnFalseStatusLabel").GetText();
Assert.That(statusText, Is.EqualTo("OnBackButtonPressed Called And Returned False"),
"OnBackButtonPressed should have been called even when returning false.");
}
}
}
Loading