diff --git a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs index 73a51d9e34bf..970f7c1cbe40 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/NavigationPage/iOS/NavigationRenderer.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using CoreGraphics; +using Foundation; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; @@ -42,8 +43,9 @@ public class NavigationRenderer : UINavigationController, IPlatformViewHandler ViewHandlerDelegator _viewHandlerWrapper; bool _navigating = false; VisualElement _element; + bool _uiRequestedPop; // User tapped the back button or swiped to navigate back - [Preserve(Conditional = true)] + [Internals.Preserve(Conditional = true)] public NavigationRenderer() : base(typeof(MauiControlsNavigationBar), null) { _viewHandlerWrapper = new ViewHandlerDelegator(Mapper, CommandMapper, this); @@ -780,7 +782,7 @@ void UpdateToolBarVisible() TopViewController?.NavigationItem?.TitleView?.LayoutSubviews(); } - internal async Task UpdateFormsInnerNavigation(Page pageBeingRemoved) + async Task UpdateFormsInnerNavigation(Page pageBeingRemoved) { if (NavPage == null) return; @@ -789,9 +791,24 @@ internal async Task UpdateFormsInnerNavigation(Page pageBeingRemoved) _ignorePopCall = true; if (Element.Navigation.NavigationStack.Contains(pageBeingRemoved)) + { await (NavPage as INavigationPageController)?.RemoveAsyncInner(pageBeingRemoved, false, true); + if (_uiRequestedPop) + { + NavPage?.SendNavigatedFromHandler(pageBeingRemoved); + } + } + _ignorePopCall = false; + _uiRequestedPop = false; + } + [Export("navigationBar:shouldPopItem:")] + [Internals.Preserve(Conditional = true)] + internal bool ShouldPopItem(UINavigationBar _, UINavigationItem __) + { + _uiRequestedPop = true; + return true; } internal static void SetFlyoutLeftBarButton(UIViewController containerController, FlyoutPage FlyoutPage) @@ -959,7 +976,9 @@ void SetupLines() class MauiNavigationDelegate : UINavigationControllerDelegate { + bool _finishedWithInitialNavigation; readonly WeakReference _navigation; + public MauiNavigationDelegate(NavigationRenderer navigationRenderer) { _navigation = new WeakReference(navigationRenderer); @@ -974,6 +993,12 @@ public override void DidShowViewController(UINavigationController navigationCont { pvc.UpdateFrames(); } + + if (r.Element is NavigationPage np && !_finishedWithInitialNavigation) + { + _finishedWithInitialNavigation = true; + np.SendNavigatedFromHandler(null); + } } } diff --git a/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Legacy.cs b/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Legacy.cs index 94a9c59642e4..8bdfe2f2dfdb 100644 --- a/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Legacy.cs +++ b/src/Controls/src/Core/HandlerImpl/NavigationPage/NavigationPage.Legacy.cs @@ -14,10 +14,9 @@ namespace Microsoft.Maui.Controls /// public partial class NavigationPage : INavigationPageController { - internal async Task PopAsyncInner( + async Task PopAsyncInner( bool animated, - bool fast, - bool requestedFromHandler) + bool fast) { if (NavigationPageController.StackDepth == 1) { @@ -27,16 +26,15 @@ internal async Task PopAsyncInner( var page = (Page)InternalChildren.Last(); var previousPage = CurrentPage; SendNavigating(); - var removedPage = await RemoveAsyncInner(page, animated, fast, requestedFromHandler); + var removedPage = await RemoveAsyncInner(page, animated, fast); SendNavigated(previousPage); return removedPage; } - internal async Task RemoveAsyncInner( + async Task RemoveAsyncInner( Page page, bool animated, - bool fast, - bool requestedFromHandler) + bool fast) { if (NavigationPageController.StackDepth == 1) { @@ -53,7 +51,7 @@ internal async Task RemoveAsyncInner( var removed = true; EventHandler requestPop = _popRequested; - if (requestPop != null && !requestedFromHandler) + if (requestPop != null) { requestPop(this, args); @@ -74,15 +72,14 @@ internal async Task RemoveAsyncInner( return page; } - Task INavigationPageController.PopAsyncInner(bool animated, bool fast) { - return PopAsyncInner(animated, fast, false); + return PopAsyncInner(animated, fast); } Task INavigationPageController.RemoveAsyncInner(Page page, bool animated, bool fast) { - return RemoveAsyncInner(page, animated, fast, false); + return RemoveAsyncInner(page, animated, fast); } EventHandler _popRequested; @@ -207,6 +204,20 @@ async Task PushAsyncInner(Page page, bool animated) Pushed?.Invoke(this, args); } +#if IOS + // Because iOS currently doesn't use our `IStackNavigationView` structures + // there are scenarios where the legacy handler needs to alert the xplat + // code of when a navigation has occurred. + // For example, initial load and when the user taps the back button + internal void SendNavigatedFromHandler(Page previousPage) + { + if (CurrentPage.HasNavigatedTo) + return; + + SendNavigated(previousPage); + } +#endif + void PushPage(Page page) { InternalChildren.Add(page); diff --git a/src/Controls/src/Core/NavigationPage.cs b/src/Controls/src/Core/NavigationPage.cs index 6cdc4eef9f5b..0330819a69c8 100644 --- a/src/Controls/src/Core/NavigationPage.cs +++ b/src/Controls/src/Core/NavigationPage.cs @@ -377,13 +377,6 @@ protected override bool OnBackButtonPressed() return base.OnBackButtonPressed(); } - internal void InitialNativeNavigationStackLoaded() - { - SendNavigated(null); - } - - - void SendNavigated(Page previousPage) { previousPage?.SendNavigatedFrom(new NavigatedFromEventArgs(CurrentPage)); diff --git a/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs b/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs index 2890c229dfe1..6e645bf42c94 100644 --- a/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs +++ b/src/Controls/tests/Core.UnitTests/PageLifeCycleTests.cs @@ -6,14 +6,18 @@ namespace Microsoft.Maui.Controls.Core.UnitTests public class PageLifeCycleTests : BaseTestFixture { - [Theory] - [InlineData(false)] - [InlineData(true)] - public void NavigationPageInitialPage(bool useMaui) + [Fact] + // This test isn't valid for non handler based + // navigation because the initial navigated event + // fires from the legacy code instead of the + // new handler code + // We have device tests to also verify this works on + // each platform + public async Task NavigationPageInitialPage() { var lcPage = new LCPage(); - NavigationPage navigationPage = new TestNavigationPage(useMaui, lcPage); - navigationPage.InitialNativeNavigationStackLoaded(); + var navigationPage = new TestNavigationPage(true, lcPage); + await navigationPage.NavigatingTask; Assert.Null(lcPage.NavigatingFromArgs); Assert.Null(lcPage.NavigatedFromArgs); Assert.NotNull(lcPage.NavigatedToArgs); diff --git a/src/Controls/tests/Core.UnitTests/TestClasses/TestNavigationPage.cs b/src/Controls/tests/Core.UnitTests/TestClasses/TestNavigationPage.cs index e3816bbc9f15..bec58167e33c 100644 --- a/src/Controls/tests/Core.UnitTests/TestClasses/TestNavigationPage.cs +++ b/src/Controls/tests/Core.UnitTests/TestClasses/TestNavigationPage.cs @@ -39,7 +39,7 @@ public async Task SendBackButtonPressedAsync() return result; } - + public Task NavigatingTask => Handler?.NavigatingTask ?? Task.CompletedTask; } public class TestNavigationHandler : ViewHandler @@ -54,8 +54,11 @@ public static PropertyMapper Naviga public NavigationRequest CurrentNavigationRequest { get; private set; } + TaskCompletionSource _navigationSource; + + public Task NavigatingTask => (_navigationSource?.Task ?? Task.CompletedTask); - public void CompleteCurrentNavigation() + public async void CompleteCurrentNavigation() { if (CurrentNavigationRequest == null) throw new InvalidOperationException("No Active Navigation in the works"); @@ -63,15 +66,24 @@ public void CompleteCurrentNavigation() var newStack = CurrentNavigationRequest.NavigationStack.ToList(); CurrentNavigationRequest = null; + var source = _navigationSource; + _navigationSource = null; + if ((this as IElementHandler).VirtualView is IStackNavigation sn) sn.NavigationFinished(newStack); + + + await Task.Delay(1); + source.SetResult(); } async void RequestNavigation(NavigationRequest navigationRequest) { - if (CurrentNavigationRequest != null) + if (CurrentNavigationRequest != null || _navigationSource != null) throw new InvalidOperationException("Already Processing Navigation"); + + _navigationSource = new TaskCompletionSource(); CurrentNavigationRequest = navigationRequest; await Task.Delay(10); diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs index 5bb46b44b54d..5f64763c68b6 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs @@ -50,6 +50,43 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async }); } + [Fact] + public async Task InitialPageFiresNavigatedEvent() + { + SetupBuilder(); + var page = new ContentPage(); + var navPage = new NavigationPage(page) { Title = "App Page" }; + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + await OnNavigatedToAsync(page); + Assert.True(page.HasNavigatedTo); + }); + } + + [Fact] + public async Task PushedPageFiresNavigatedEventOnInitialLoad() + { + SetupBuilder(); + + bool pageFiredNavigated = false; + var page = new ContentPage(); + page.NavigatedTo += (_, _) => pageFiredNavigated = true; + + var page2 = new ContentPage(); + + var navPage = new NavigationPage(page) { Title = "App Page" }; + await navPage.PushAsync(page2); + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + await OnNavigatedToAsync(page2); + Assert.True(page2.HasNavigatedTo); + }); + + Assert.False(pageFiredNavigated); + } + #if !IOS && !MACCATALYST [Fact(DisplayName = "Back Button Visibility Changes with push/pop")] public async Task BackButtonVisibilityChangesWithPushPop() diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs index 8166ba33f264..50323c68fdde 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.iOS.cs @@ -1,8 +1,41 @@ -using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Platform; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Controls.Handlers.Compatibility; +using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; +using UIKit; +using Xunit; namespace Microsoft.Maui.DeviceTests { public partial class NavigationPageTests : ControlsHandlerTestBase { + [Fact] + public async Task NavigatingBackViaBackButtonFiresNavigatedEvent() + { + SetupBuilder(); + var page = new ContentPage(); + + var navPage = new NavigationPage(page) { Title = "App Page" }; + + await navPage.PushAsync(new ContentPage()); + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + await OnNavigatedToAsync(navPage.CurrentPage); + var navController = navPage.Handler as UINavigationController; + + Assert.False(page.HasNavigatedTo); + navController.NavigationBar.TapBackButton(); + await OnNavigatedToAsync(page); + Assert.True(page.HasNavigatedTo); + }); + } } }