From 8fb1bb0eea882cb4643ede277f1cac39b28e6383 Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Tue, 7 Jan 2020 18:07:00 -0700 Subject: [PATCH] Detect when pages are popped from clicking on tab (#9086) --- .../Issue9006.cs | 83 +++++++++++++++++++ .../TestPages/TestPages.cs | 17 +++- ...rin.Forms.Controls.Issues.Shared.projitems | 3 +- .../Shell/IShellSectionController.cs | 12 +++ Xamarin.Forms.Core/Shell/ShellSection.cs | 46 +++++++++- .../Renderers/ShellSectionRenderer.cs | 68 +++++++++++---- 6 files changed, 211 insertions(+), 18 deletions(-) create mode 100644 Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9006.cs diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9006.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9006.cs new file mode 100644 index 000000000..13765835e --- /dev/null +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Issue9006.cs @@ -0,0 +1,83 @@ +using Xamarin.Forms.CustomAttributes; +using Xamarin.Forms.Internals; + +#if UITEST +using Xamarin.UITest; +using NUnit.Framework; +using Xamarin.Forms.Core.UITests; +#endif + +namespace Xamarin.Forms.Controls.Issues +{ + [Preserve(AllMembers = true)] + [Issue(IssueTracker.Github, 9006, "[Bug] Unable to open a new Page for the second time in Xamarin.Forms Shell Tabbar", + PlatformAffected.iOS)] +#if UITEST + [NUnit.Framework.Category(UITestCategories.Shell)] +#endif + public class Issue9006 : TestShell + { + protected override void Init() + { + Routing.RegisterRoute("Issue9006_ContentPage", typeof(ContentPage)); + Routing.RegisterRoute("Issue9006_FinalPage", typeof(ContentPage)); + + var contentPage = AddBottomTab("Tab 1"); + Items[0].CurrentItem.AutomationId = "Tab1AutomationId"; + AddBottomTab("Ignore Me"); + + Label label = new Label() + { + Text = "Clicking on the first tab should pop you back to the root", + AutomationId = "FinalLabel" + }; + + Button button = null; + bool navigated = false; + button = new Button() + { + Text = "Click Me", + AutomationId = "Click Me", + Command = new Command(async () => + { + await GoToAsync("Issue9006_ContentPage"); + await GoToAsync("Issue9006_FinalPage"); + + button.Text = "Click me again. If pages get pushed again then test has passed."; + DisplayedPage.Content = new StackLayout() + { + Children = + { + label + } + }; + if (navigated) + label.Text = "Success"; + + navigated = true; + }) + }; + + contentPage.Content = new StackLayout() + { + Children = + { + button + } + }; + } + + +#if UITEST && __IOS__ + [Test] + public void ClickingOnTabToPopToRootDoesntBreakNavigation() + { + RunningApp.Tap("Click Me"); + RunningApp.WaitForElement("FinalLabel"); + RunningApp.Tap("Tab1AutomationId"); + RunningApp.Tap("Click Me"); + RunningApp.Tap("Success"); + } +#endif + } +} diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs index b01929448..0e6706c9d 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/TestPages/TestPages.cs @@ -602,6 +602,14 @@ namespace Xamarin.Forms.Controls #endif } + protected ContentPage DisplayedPage + { + get + { + return (ContentPage)(CurrentItem.CurrentItem as IShellSectionController).PresentedPage; + } + } + public ContentPage AddTopTab(string title) { var page = new ContentPage(); @@ -633,8 +641,15 @@ namespace Xamarin.Forms.Controls public ContentPage AddBottomTab(string title) { - ContentPage page = new ContentPage(); + if (Items.Count == 0) + { + var item = AddContentPage(page); + item.Items[0].Items[0].Title = title ?? page.Title; + item.Items[0].Title = title ?? page.Title; + return page; + } + Items[0].Items.Add(new ShellSection() { Title = title, diff --git a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems index 44ef16ada..88eeebbc1 100644 --- a/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems +++ b/Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems @@ -11,6 +11,7 @@ + @@ -1684,4 +1685,4 @@ MSBuild:UpdateDesignTimeXaml - \ No newline at end of file + diff --git a/Xamarin.Forms.Core/Shell/IShellSectionController.cs b/Xamarin.Forms.Core/Shell/IShellSectionController.cs index 7841cce88..ee16e692d 100644 --- a/Xamarin.Forms.Core/Shell/IShellSectionController.cs +++ b/Xamarin.Forms.Core/Shell/IShellSectionController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Threading.Tasks; using Xamarin.Forms.Internals; @@ -21,8 +22,19 @@ namespace Xamarin.Forms void SendInsetChanged(Thickness inset, double tabThickness); + void SendPopping(Task poppingCompleted); + void SendPoppingToRoot(Task finishedPopping); + + [Obsolete] + [EditorBrowsable(EditorBrowsableState.Never)] void SendPopped(); + + [Obsolete] + [EditorBrowsable(EditorBrowsableState.Never)] void SendPopping(Page page); + + [Obsolete] + [EditorBrowsable(EditorBrowsableState.Never)] void SendPopped(Page page); } } \ No newline at end of file diff --git a/Xamarin.Forms.Core/Shell/ShellSection.cs b/Xamarin.Forms.Core/Shell/ShellSection.cs index 761396f62..7de7883c6 100644 --- a/Xamarin.Forms.Core/Shell/ShellSection.cs +++ b/Xamarin.Forms.Core/Shell/ShellSection.cs @@ -118,6 +118,45 @@ namespace Xamarin.Forms _lastTabThickness = tabThickness; } + async void IShellSectionController.SendPopping(Task poppingCompleted) + { + if (_navStack.Count <= 1) + throw new Exception("Nav Stack consistency error"); + + var page = _navStack[_navStack.Count - 1]; + + _navStack.Remove(page); + UpdateDisplayedPage(); + + await poppingCompleted; + + RemovePage(page); + SendUpdateCurrentState(ShellNavigationSource.Pop); + } + + async void IShellSectionController.SendPoppingToRoot(Task finishedPopping) + { + if (_navStack.Count <= 1) + throw new Exception("Nav Stack consistency error"); + + var oldStack = _navStack; + _navStack = new List { null }; + + for (int i = 1; i < oldStack.Count; i++) + oldStack[i].SendDisappearing(); + + UpdateDisplayedPage(); + await finishedPopping; + + for (int i = 1; i < oldStack.Count; i++) + RemovePage(oldStack[i]); + + SendUpdateCurrentState(ShellNavigationSource.PopToRoot); + } + + [Obsolete] + [EditorBrowsable(EditorBrowsableState.Never)] + void IShellSectionController.SendPopped() { if (_navStack.Count <= 1) @@ -131,6 +170,8 @@ namespace Xamarin.Forms SendUpdateCurrentState(ShellNavigationSource.Pop); } + [Obsolete] + [EditorBrowsable(EditorBrowsableState.Never)] void IShellSectionController.SendPopping(Page page) { if (_navStack.Count <= 1) @@ -140,9 +181,11 @@ namespace Xamarin.Forms SendAppearanceChanged(); } + [Obsolete] + [EditorBrowsable(EditorBrowsableState.Never)] void IShellSectionController.SendPopped(Page page) { - if(_navStack.Contains(page)) + if (_navStack.Contains(page)) _navStack.Remove(page); RemovePage(page); @@ -313,6 +356,7 @@ namespace Xamarin.Forms if (previousPage != DisplayedPage) { + previousPage?.SendDisappearing(); PresentedPageAppearing(); SendAppearanceChanged(); } diff --git a/Xamarin.Forms.Platform.iOS/Renderers/ShellSectionRenderer.cs b/Xamarin.Forms.Platform.iOS/Renderers/ShellSectionRenderer.cs index cf4c1f3ba..afc2d4219 100644 --- a/Xamarin.Forms.Platform.iOS/Renderers/ShellSectionRenderer.cs +++ b/Xamarin.Forms.Platform.iOS/Renderers/ShellSectionRenderer.cs @@ -63,6 +63,7 @@ namespace Xamarin.Forms.Platform.iOS TaskCompletionSource _popCompletionTask; IShellSectionRootRenderer _renderer; ShellSection _shellSection; + bool _ignorePopCall; public ShellSectionRenderer(IShellContext context) { @@ -265,16 +266,48 @@ namespace Xamarin.Forms.Platform.iOS DisposePage(page); } - protected virtual async void OnPopToRootRequested(NavigationRequestedEventArgs e) + public override UIViewController[] PopToRootViewController(bool animated) { - var animated = e.Animated; + if (!_ignorePopCall && ViewControllers.Length > 1) + { + ProcessPopToRoot(); + } + return base.PopToRootViewController(animated); + } + + async void ProcessPopToRoot() + { var task = new TaskCompletionSource(); var pages = _shellSection.Stack.ToList(); _completionTasks[_renderer.ViewController] = task; - e.Task = task.Task; + ((IShellSectionController)ShellSection).SendPoppingToRoot(task.Task); + await task.Task; - PopToRootViewController(animated); + for (int i = pages.Count - 1; i >= 1; i--) + { + var page = pages[i]; + DisposePage(page); + } + } + + protected virtual async void OnPopToRootRequested(NavigationRequestedEventArgs e) + { + var animated = e.Animated; + var task = new TaskCompletionSource(); + var pages = _shellSection.Stack.ToList(); + + try + { + _ignorePopCall = true; + _completionTasks[_renderer.ViewController] = task; + e.Task = task.Task; + PopToRootViewController(animated); + } + finally + { + _ignorePopCall = false; + } await e.Task; @@ -328,6 +361,7 @@ namespace Xamarin.Forms.Platform.iOS _ = _context.ApplyNativeImageAsync(ShellSection, ShellSection.IconProperty, icon => { TabBarItem = new UITabBarItem(ShellSection.Title, icon, null); + TabBarItem.AccessibilityIdentifier = ShellSection.AutomationId ?? ShellSection.Title; }); } @@ -399,11 +433,10 @@ namespace Xamarin.Forms.Platform.iOS var poppedPage = _shellSection.Stack[_shellSection.Stack.Count - 1]; // this is used to setup appearance changes based on the incoming page - ((IShellSectionController)_shellSection).SendPopping(poppedPage); + ((IShellSectionController)_shellSection).SendPopping(popTask); await popTask; - ((IShellSectionController)_shellSection).SendPopped(poppedPage); DisposePage(poppedPage); } @@ -491,17 +524,22 @@ namespace Xamarin.Forms.Platform.iOS navigationController.SetNavigationBarHidden(!navBarVisible, true); var coordinator = viewController.GetTransitionCoordinator(); - if (coordinator != null) + if (coordinator != null && coordinator.IsInteractive) { // handle swipe to dismiss gesture - coordinator.NotifyWhenInteractionEndsUsingBlock((context) => - { - if (!context.IsCancelled) - { - _self._popCompletionTask = new TaskCompletionSource(); - _self.SendPoppedOnCompletion(_self._popCompletionTask.Task); - } - }); + if (Forms.IsiOS10OrNewer) + coordinator.NotifyWhenInteractionChanges(OnInteractionChanged); + else + coordinator.NotifyWhenInteractionEndsUsingBlock(OnInteractionChanged); + } + } + + void OnInteractionChanged(IUIViewControllerTransitionCoordinatorContext context) + { + if (!context.IsCancelled) + { + _self._popCompletionTask = new TaskCompletionSource(); + _self.SendPoppedOnCompletion(_self._popCompletionTask.Task); } } }