diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs index c7c2ca1b730b..852505972c4f 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellToolbarTracker.cs @@ -264,7 +264,7 @@ protected virtual void OnPageChanged(Page oldPage, Page newPage) UpdateTitleView(); if (ShellContext.Shell.Toolbar is ShellToolbar shellToolbar && - newPage == ShellContext.Shell.CurrentPage) + newPage == ShellContext.Shell.GetCurrentShellPage()) { shellToolbar.ApplyChanges(); } @@ -276,8 +276,8 @@ void OnShellNavigated(object sender, ShellNavigatedEventArgs e) if (_disposed || Page == null) return; - if (ShellContext?.Shell?.Toolbar is ShellToolbar shellToolbar && - Page == ShellContext?.Shell?.CurrentPage) + if (ShellContext?.Shell?.Toolbar is ShellToolbar && + Page == ShellContext?.Shell?.GetCurrentShellPage()) { UpdateLeftBarButtonItem(); } @@ -579,7 +579,7 @@ void UpdateNavBarHasShadow(Page page) void OnToolbarPropertyChanged(object sender, PropertyChangedEventArgs e) { - if (_toolbar != null && ShellContext?.Shell?.CurrentPage == Page) + if (_toolbar != null && ShellContext?.Shell?.GetCurrentShellPage() == Page) { ApplyToolbarChanges((Toolbar)sender, (Toolbar)_toolbar); UpdateToolbarIconAccessibilityText(_platformToolbar, ShellContext.Shell); diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs index 323d5280baa6..4107beb49684 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellPageRendererTracker.cs @@ -64,6 +64,7 @@ public Page Page NSCache _nSCache; SearchHandlerAppearanceTracker _searchHandlerAppearanceTracker; IFontManager _fontManager; + bool _isVisiblePage; BackButtonBehavior BackButtonBehavior { get; set; } UINavigationItem NavigationItem { get; set; } @@ -75,16 +76,13 @@ public ShellPageRendererTracker(IShellContext context) _nSCache = new NSCache(); _context.Shell.PropertyChanged += HandleShellPropertyChanged; - if (_context.Shell.Toolbar != null) - _context.Shell.Toolbar.PropertyChanged += OnToolbarPropertyChanged; - _fontManager = context.Shell.RequireFontManager(); } public void OnFlyoutBehaviorChanged(FlyoutBehavior behavior) { _flyoutBehavior = behavior; - UpdateToolbarItems(); + UpdateToolbarItemsInternal(); } protected virtual void HandleShellPropertyChanged(object sender, PropertyChangedEventArgs e) @@ -142,6 +140,9 @@ protected virtual void UpdateTabBarVisible() void OnToolbarPropertyChanged(object sender, PropertyChangedEventArgs e) { + if (!ToolbarReady()) + return; + if (e.PropertyName == Shell.TitleViewProperty.PropertyName) { UpdateTitleView(); @@ -154,9 +155,21 @@ void OnToolbarPropertyChanged(object sender, PropertyChangedEventArgs e) protected virtual void UpdateTitle() { + if (!ToolbarReady()) + return; + NavigationItem.Title = _context.Shell.Toolbar.Title; } + + bool ToolbarReady() + { + if (_context.Shell.Toolbar is ShellToolbar st) + return st.CurrentPage == Page; + + return _isVisiblePage; + } + void UpdateShellToMyPage() { if (Page == null) @@ -167,7 +180,7 @@ void UpdateShellToMyPage() UpdateTitleView(); UpdateTitle(); UpdateTabBarVisible(); - UpdateToolbarItems(); + UpdateToolbarItemsInternal(); } protected virtual void OnPageSet(Page oldPage, Page newPage) @@ -175,6 +188,7 @@ protected virtual void OnPageSet(Page oldPage, Page newPage) if (oldPage != null) { oldPage.Appearing -= PageAppearing; + oldPage.Disappearing -= PageDisappearing; oldPage.PropertyChanged -= OnPagePropertyChanged; oldPage.Loaded -= OnPageLoaded; ((INotifyCollectionChanged)oldPage.ToolbarItems).CollectionChanged -= OnToolbarItemsChanged; @@ -183,14 +197,14 @@ protected virtual void OnPageSet(Page oldPage, Page newPage) if (newPage != null) { newPage.Appearing += PageAppearing; + newPage.Disappearing += PageDisappearing; newPage.PropertyChanged += OnPagePropertyChanged; if (!newPage.IsLoaded) newPage.Loaded += OnPageLoaded; ((INotifyCollectionChanged)newPage.ToolbarItems).CollectionChanged += OnToolbarItemsChanged; - - UpdateShellToMyPage(); + CheckAppeared(); if (oldPage == null) { @@ -215,11 +229,19 @@ protected virtual void OnRendererSet() protected virtual void UpdateTitleView() { + if (!ToolbarReady()) + return; + var titleView = _context.Shell.Toolbar.TitleView as View; if (NavigationItem.TitleView is TitleViewContainer tvc && tvc.View == titleView) { + // The MauiContext/handler/other may have changed on the `View` + // This tells the title view container to make sure + // the currently added platformview is still valid and doesn't need + // to be recreated + tvc.UpdatePlatformView(); return; } @@ -231,15 +253,36 @@ protected virtual void UpdateTitleView() } else { - var view = new TitleViewContainer(titleView); - NavigationItem.TitleView = view; + if (titleView.Parent != null) + { + var view = new TitleViewContainer(titleView); + NavigationItem.TitleView = view; + } + else + { + titleView.ParentSet += OnTitleViewParentSet; + } } } + void OnTitleViewParentSet(object sender, EventArgs e) + { + ((Element)sender).ParentSet -= OnTitleViewParentSet; + UpdateTitleView(); + } + + internal void UpdateToolbarItemsInternal(bool updateWhenLoaded = true) + { + if (updateWhenLoaded && Page.IsLoaded || !updateWhenLoaded) + UpdateToolbarItems(); + } + protected virtual void UpdateToolbarItems() { - if (NavigationItem == null || !Page.IsLoaded) + if (NavigationItem == null) + { return; + } if (NavigationItem.RightBarButtonItems != null) { @@ -302,14 +345,10 @@ void UpdateLeftToolbarItems() NavigationItem.LeftBarButtonItem = new UIBarButtonItem(icon, UIBarButtonItemStyle.Plain, (s, e) => LeftBarButtonItemHandler(ViewController, IsRootPage)) { Enabled = enabled }; } - else if (!String.IsNullOrWhiteSpace(text)) - { - NavigationItem.LeftBarButtonItem = - new UIBarButtonItem(text, UIBarButtonItemStyle.Plain, (s, e) => LeftBarButtonItemHandler(ViewController, IsRootPage)) { Enabled = enabled }; - } else { NavigationItem.LeftBarButtonItem = null; + UpdateBackButtonTitle(); } if (NavigationItem.LeftBarButtonItem != null) @@ -333,6 +372,40 @@ void UpdateLeftToolbarItems() } } }); + + UpdateBackButtonTitle(); + } + + + void UpdateBackButtonTitle() + { + var behavior = BackButtonBehavior; + var text = behavior.GetPropertyIfSet(BackButtonBehavior.TextOverrideProperty, null); + + var navController = ViewController?.NavigationController; + + if (navController != null) + { + var viewControllers = ViewController.NavigationController.ViewControllers; + var count = viewControllers.Length; + + if (count > 1 && viewControllers[count - 1] == ViewController) + { + var previousNavItem = viewControllers[count - 2].NavigationItem; + if (previousNavItem != null) + { + if (!String.IsNullOrWhiteSpace(text)) + { + var barButtonItem = (previousNavItem.BackBarButtonItem ??= new UIBarButtonItem()); + barButtonItem.Title = text; + } + else if (previousNavItem.BackBarButtonItem != null) + { + previousNavItem.BackBarButtonItem = null; + } + } + } + } } void LeftBarButtonItemHandler(UIViewController controller, bool isRootPage) @@ -396,7 +469,7 @@ UIImage DrawHamburger() void OnToolbarItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { - UpdateToolbarItems(); + UpdateToolbarItemsInternal(); } void SetBackButtonBehavior(BackButtonBehavior value) @@ -412,7 +485,7 @@ void SetBackButtonBehavior(BackButtonBehavior value) if (BackButtonBehavior != null) BackButtonBehavior.PropertyChanged += OnBackButtonBehaviorPropertyChanged; - UpdateToolbarItems(); + UpdateToolbarItemsInternal(); } void OnBackButtonCommandCanExecuteChanged(object sender, EventArgs e) @@ -460,7 +533,23 @@ public override CGRect Frame } } + public override void LayoutSubviews() + { + if (Height == null || Height == 0) + { + UpdateFrame(Superview); + } + + base.LayoutSubviews(); + } + public override void WillMoveToSuperview(UIView newSuper) + { + UpdateFrame(newSuper); + base.WillMoveToSuperview(newSuper); + } + + void UpdateFrame(UIView newSuper) { if (newSuper != null) { @@ -469,8 +558,6 @@ public override void WillMoveToSuperview(UIView newSuper) Height = newSuper.Bounds.Height; } - - base.WillMoveToSuperview(newSuper); } public override CGSize IntrinsicContentSize => UILayoutFittingExpandedSize; @@ -713,15 +800,46 @@ void OnPageLoaded(object sender, EventArgs e) if (sender is Page page) page.Loaded -= OnPageLoaded; - UpdateToolbarItems(); + UpdateToolbarItemsInternal(); + CheckAppeared(); + } + + void PageAppearing(object sender, EventArgs e) => + SetAppeared(); + + void PageDisappearing(object sender, EventArgs e) => + SetDisappeared(); + + void CheckAppeared() + { + if (_context.Shell.CurrentPage == Page) + SetAppeared(); } - void PageAppearing(object sender, EventArgs e) + void SetAppeared() { + if (_isVisiblePage) + return; + + _isVisiblePage = true; //UIKIt will try to override our colors when the SearchController is inside the NavigationBar //Best way was to force them to be set again when page is Appearing / ViewDidLoad _searchHandlerAppearanceTracker?.UpdateSearchBarColors(); UpdateShellToMyPage(); + + if (_context.Shell.Toolbar != null) + _context.Shell.Toolbar.PropertyChanged += OnToolbarPropertyChanged; + } + + void SetDisappeared() + { + if (!_isVisiblePage) + return; + + _isVisiblePage = false; + + if (_context.Shell.Toolbar != null) + _context.Shell.Toolbar.PropertyChanged -= OnToolbarPropertyChanged; } #endregion SearchHandler @@ -743,6 +861,7 @@ protected virtual void Dispose(bool disposing) _searchHandlerAppearanceTracker?.Dispose(); Page.Loaded -= OnPageLoaded; Page.Appearing -= PageAppearing; + Page.Disappearing -= PageDisappearing; Page.PropertyChanged -= OnPagePropertyChanged; ((INotifyCollectionChanged)Page.ToolbarItems).CollectionChanged -= OnToolbarItemsChanged; ((IShellController)_context.Shell).RemoveFlyoutBehaviorObserver(this); @@ -754,6 +873,9 @@ protected virtual void Dispose(bool disposing) if (_context.Shell.Toolbar != null) _context.Shell.Toolbar.PropertyChanged -= OnToolbarPropertyChanged; + + if (NavigationItem?.TitleView is TitleViewContainer tvc) + tvc.Disconnect(); } _context = null; diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRenderer.cs index 30cc3bc205fe..688f9765618c 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/ShellSectionRenderer.cs @@ -699,7 +699,6 @@ public override void DidShowViewController(UINavigationController navigationCont public override void WillShowViewController(UINavigationController navigationController, [Transient] UIViewController viewController, bool animated) { - System.Diagnostics.Debug.Write($"WillShowViewController {viewController.GetHashCode()}"); var element = _self.ElementForViewController(viewController); bool navBarVisible; @@ -716,6 +715,18 @@ public override void WillShowViewController(UINavigationController navigationCon // handle swipe to dismiss gesture coordinator.NotifyWhenInteractionChanges(OnInteractionChanged); } + + // Because the back button title needs to be set on the previous VC + // We want to set the BackButtonItem as early as possible so there is no flickering + var currentPage = _self._context?.Shell?.GetCurrentShellPage(); + var trackers = _self._trackers; + if (currentPage?.Handler is IPlatformViewHandler pvh && + pvh.ViewController == viewController && + trackers.TryGetValue(currentPage, out var tracker) && + tracker is ShellPageRendererTracker shellRendererTracker) + { + shellRendererTracker.UpdateToolbarItemsInternal(false); + } } void OnInteractionChanged(IUIViewControllerTransitionCoordinatorContext context) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SlideFlyoutTransition.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SlideFlyoutTransition.cs index ef01a9f44819..b731c6c961bb 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SlideFlyoutTransition.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/SlideFlyoutTransition.cs @@ -56,8 +56,8 @@ public virtual void LayoutViews(CGRect bounds, nfloat openPercent, UIView flyout if (shell.SemanticContentAttribute == UISemanticContentAttribute.ForceRightToLeft) { - var positionY = shellWidth - openPixels; - flyout.Frame = new CGRect(positionY, 0, flyoutWidth, flyoutHeight); + var positionX = shellWidth - openPixels; + flyout.Frame = new CGRect(positionX, 0, flyoutWidth, flyoutHeight); } else { diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs index 177c3c86f9e0..f6afd8261e45 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/iOS/UIContainerView.cs @@ -11,6 +11,7 @@ public class UIContainerView : UIView { readonly View _view; IPlatformViewHandler _renderer; + UIView _platformView; bool _disposed; internal event EventHandler HeaderSizeChanged; @@ -18,15 +19,29 @@ public UIContainerView(View view) { _view = view; - _renderer = (IPlatformViewHandler)view.ToHandler(view.FindMauiContext()); - - AddSubview(view.ToPlatform()); + UpdatePlatformView(); ClipsToBounds = true; - view.MeasureInvalidated += OnMeasureInvalidated; MeasuredHeight = double.NaN; Margin = new Thickness(0); } + internal void UpdatePlatformView() + { + _renderer = (IPlatformViewHandler)_view.ToHandler(_view.FindMauiContext()); + _platformView = _view.ToPlatform(); + + if (_platformView.Superview != this) + AddSubview(_platformView); + } + + bool IsPlatformViewValid() + { + if (View == null || _platformView == null || _renderer == null) + return false; + + return _platformView.Superview == this; + } + internal View View => _view; internal bool MatchHeight { get; set; } @@ -47,7 +62,7 @@ internal double? Width internal bool MeasureIfNeeded() { - if (View == null) + if (!IsPlatformViewValid()) return false; if (double.IsNaN(MeasuredHeight) || Frame.Width != View.Width) @@ -66,6 +81,9 @@ public virtual Thickness Margin void ReMeasure() { + if (!IsPlatformViewValid()) + return; + if (Height != null && MatchHeight) { MeasuredHeight = Height.Value; @@ -81,6 +99,9 @@ void ReMeasure() void OnMeasureInvalidated(object sender, System.EventArgs e) { + if (!IsPlatformViewValid()) + return; + ReMeasure(); LayoutSubviews(); } @@ -91,12 +112,45 @@ public override void WillMoveToSuperview(UIView newSuper) ReMeasure(); } + public override void WillRemoveSubview(UIView uiview) + { + Disconnect(); + base.WillRemoveSubview(uiview); + } + + public override void AddSubview(UIView view) + { + if (view == _platformView) + _view.MeasureInvalidated += OnMeasureInvalidated; + + base.AddSubview(view); + + } + public override void LayoutSubviews() { + if (!IsPlatformViewValid()) + return; + var platformFrame = new Rect(0, 0, Width ?? Frame.Width, Height ?? MeasuredHeight); + + var width = Width ?? Frame.Width; + var height = Height ?? MeasuredHeight; + + if (MatchHeight) + { + (_view as IView).Measure(width, height); + } + (_view as IView).Arrange(platformFrame); } + internal void Disconnect() + { + if (_view != null) + _view.MeasureInvalidated -= OnMeasureInvalidated; + } + protected override void Dispose(bool disposing) { if (_disposed) @@ -104,12 +158,13 @@ protected override void Dispose(bool disposing) if (disposing) { - if (_view != null) - _view.MeasureInvalidated -= OnMeasureInvalidated; + Disconnect(); - _renderer?.DisconnectHandler(); - _renderer = null; + if (_platformView.Superview == this) + _platformView.RemoveFromSuperview(); + _renderer = null; + _platformView = null; _disposed = true; } diff --git a/src/Controls/src/Core/Compatibility/iOS/Extensions/UIViewExtensions.cs b/src/Controls/src/Core/Compatibility/iOS/Extensions/UIViewExtensions.cs index 7ea17e81b64e..c06b9b729809 100644 --- a/src/Controls/src/Core/Compatibility/iOS/Extensions/UIViewExtensions.cs +++ b/src/Controls/src/Core/Compatibility/iOS/Extensions/UIViewExtensions.cs @@ -108,26 +108,6 @@ internal static void TransferbindablePropertiesToWrapper(this UIView target, Vie PlatformBindingHelpers.TransferBindablePropertiesToWrapper(target, wrapper); } - internal static T FindDescendantView(this UIView view) where T : UIView - { - var queue = new Queue(); - queue.Enqueue(view); - - while (queue.Count > 0) - { - var descendantView = queue.Dequeue(); - - var result = descendantView as T; - if (result != null) - return result; - - for (var i = 0; i < descendantView.Subviews?.Length; i++) - queue.Enqueue(descendantView.Subviews[i]); - } - - return null; - } - #if __MOBILE__ internal static UIView FindFirstResponder(this UIView view) { diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 2535e5410fcc..60498e853f1d 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1,8 +1,11 @@ #nullable enable *REMOVED*override Microsoft.Maui.Controls.RefreshView.MeasureOverride(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size +override Microsoft.Maui.Controls.Platform.Compatibility.ShellPageRendererTracker.TitleViewContainer.LayoutSubviews() -> void ~Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.FrameRenderer(Microsoft.Maui.IPropertyMapper mapper) -> void ~Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.FrameRenderer(Microsoft.Maui.IPropertyMapper mapper, Microsoft.Maui.CommandMapper commandMapper) -> void override Microsoft.Maui.Controls.View.ChangeVisualState() -> void +~override Microsoft.Maui.Controls.Platform.Compatibility.UIContainerView.AddSubview(UIKit.UIView view) -> void +~override Microsoft.Maui.Controls.Platform.Compatibility.UIContainerView.WillRemoveSubview(UIKit.UIView uiview) -> void *REMOVED*~Microsoft.Maui.Controls.IMessagingCenter.Send(TSender sender, string message, TArgs args) -> void *REMOVED*~Microsoft.Maui.Controls.IMessagingCenter.Send(TSender sender, string message) -> void *REMOVED*~Microsoft.Maui.Controls.IMessagingCenter.Subscribe(object subscriber, string message, System.Action callback, TSender source = null) -> void diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 2535e5410fcc..60498e853f1d 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1,8 +1,11 @@ #nullable enable *REMOVED*override Microsoft.Maui.Controls.RefreshView.MeasureOverride(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size +override Microsoft.Maui.Controls.Platform.Compatibility.ShellPageRendererTracker.TitleViewContainer.LayoutSubviews() -> void ~Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.FrameRenderer(Microsoft.Maui.IPropertyMapper mapper) -> void ~Microsoft.Maui.Controls.Handlers.Compatibility.FrameRenderer.FrameRenderer(Microsoft.Maui.IPropertyMapper mapper, Microsoft.Maui.CommandMapper commandMapper) -> void override Microsoft.Maui.Controls.View.ChangeVisualState() -> void +~override Microsoft.Maui.Controls.Platform.Compatibility.UIContainerView.AddSubview(UIKit.UIView view) -> void +~override Microsoft.Maui.Controls.Platform.Compatibility.UIContainerView.WillRemoveSubview(UIKit.UIView uiview) -> void *REMOVED*~Microsoft.Maui.Controls.IMessagingCenter.Send(TSender sender, string message, TArgs args) -> void *REMOVED*~Microsoft.Maui.Controls.IMessagingCenter.Send(TSender sender, string message) -> void *REMOVED*~Microsoft.Maui.Controls.IMessagingCenter.Subscribe(object subscriber, string message, System.Action callback, TSender source = null) -> void diff --git a/src/Controls/src/Core/Shell/Shell.cs b/src/Controls/src/Core/Shell/Shell.cs index 11c59fd788b5..6305472994b9 100644 --- a/src/Controls/src/Core/Shell/Shell.cs +++ b/src/Controls/src/Core/Shell/Shell.cs @@ -24,7 +24,7 @@ namespace Microsoft.Maui.Controls public partial class Shell : Page, IShellController, IPropertyPropagationController, IPageContainer { /// - public Page CurrentPage => (CurrentSection as IShellSectionController)?.PresentedPage; + public Page CurrentPage => GetVisiblePage() as Page; /// public static readonly BindableProperty BackButtonBehaviorProperty = @@ -1364,7 +1364,7 @@ internal T GetEffectiveValue( Element element = null, bool ignoreImplicit = false) { - element = element ?? GetCurrentShellPage() ?? CurrentContent; + element = element ?? (Element)GetCurrentShellPage() ?? CurrentContent; while (element != this && element != null) { observer?.Invoke(element); @@ -1506,9 +1506,17 @@ internal Element GetVisiblePage() return null; } + internal void SendPageAppearing(Page page) + { + if (Toolbar is ShellToolbar shellToolbar) + shellToolbar.ApplyChanges(); + + page.SendAppearing(); + } + // This returns the current shell page that's visible // without including the modal stack - internal Element GetCurrentShellPage() + internal Page GetCurrentShellPage() { var navStack = CurrentSection?.Navigation?.NavigationStack; Page currentPage = null; diff --git a/src/Controls/src/Core/Shell/ShellContent.cs b/src/Controls/src/Core/Shell/ShellContent.cs index 036370249517..d2f9fec62b4f 100644 --- a/src/Controls/src/Core/Shell/ShellContent.cs +++ b/src/Controls/src/Core/Shell/ShellContent.cs @@ -139,13 +139,13 @@ void SendPageAppearing(Page page) page.ParentSet += OnPresentedPageParentSet; void OnPresentedPageParentSet(object sender, EventArgs e) { - page.SendAppearing(); + this.FindParentOfType().SendPageAppearing(page); (sender as Page).ParentSet -= OnPresentedPageParentSet; } } else if (IsVisibleContent && page.IsVisible) { - page.SendAppearing(); + this.FindParentOfType().SendPageAppearing(page); } } diff --git a/src/Controls/src/Core/Shell/ShellSection.cs b/src/Controls/src/Core/Shell/ShellSection.cs index 0e72183f7ad3..432b29326b70 100644 --- a/src/Controls/src/Core/Shell/ShellSection.cs +++ b/src/Controls/src/Core/Shell/ShellSection.cs @@ -962,7 +962,8 @@ void OnPresentedPageParentSet(object sender, EventArgs e) } else { - presentedPage.SendAppearing(); + + this.FindParentOfType().SendPageAppearing(presentedPage); } } } diff --git a/src/Controls/src/Core/ShellToolbar.cs b/src/Controls/src/Core/ShellToolbar.cs index 55210cb6702a..2516dff8ac7a 100644 --- a/src/Controls/src/Core/ShellToolbar.cs +++ b/src/Controls/src/Core/ShellToolbar.cs @@ -20,7 +20,7 @@ internal class ShellToolbar : Toolbar bool _drawerToggleVisible; public override bool DrawerToggleVisible { get => _drawerToggleVisible; set => SetProperty(ref _drawerToggleVisible, value); } - + public Page? CurrentPage => _currentPage; public ShellToolbar(Shell shell) : base(shell) { _drawerToggleVisible = true; @@ -53,9 +53,9 @@ public ShellToolbar(Shell shell) : base(shell) internal void ApplyChanges() { - var currentPage = _shell.CurrentPage; + var currentPage = _shell.GetCurrentShellPage(); - if (_currentPage != _shell.CurrentPage) + if (_currentPage != currentPage) { if (_currentPage != null) _currentPage.PropertyChanged -= OnCurrentPagePropertyChanged; @@ -171,7 +171,7 @@ internal void UpdateTitle() return; } - Page? currentPage = _shell.GetCurrentShellPage() as Page; + Page? currentPage = _shell.GetCurrentShellPage(); if (currentPage?.IsSet(Page.TitleProperty) == true) { Title = currentPage.Title ?? String.Empty; diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs index 9b07a8065319..9474511e87a1 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Android.cs @@ -11,6 +11,7 @@ using Google.Android.Material.AppBar; using Microsoft.Maui.Controls; using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using Xunit; @@ -117,10 +118,15 @@ protected MaterialToolbar GetPlatformToolbar(IElementHandler handler) { var shell = handler.VirtualView as Shell; var currentPage = shell.CurrentPage; - var pagePlatformView = currentPage.Handler.PlatformView as AView; - var parentContainer = pagePlatformView.GetParentOfType(); - var toolbar = parentContainer.GetFirstChildOfType(); - return toolbar; + + if (currentPage?.Handler?.PlatformView is AView pagePlatformView) + { + var parentContainer = pagePlatformView.GetParentOfType(); + var toolbar = parentContainer?.GetFirstChildOfType(); + return toolbar; + } + + return null; } else { @@ -128,6 +134,9 @@ protected MaterialToolbar GetPlatformToolbar(IElementHandler handler) } } + protected string GetToolbarTitle(IElementHandler handler) => + GetPlatformToolbar(handler).Title; + protected MaterialToolbar GetPlatformToolbar(IMauiContext mauiContext) { var navManager = mauiContext.GetNavigationRootManager(); @@ -150,6 +159,13 @@ protected MaterialToolbar GetPlatformToolbar(IMauiContext mauiContext) return toolBar; } + protected Size GetTitleViewExpectedSize(IElementHandler handler) + { + var context = handler.MauiContext.Context; + var toolbar = GetPlatformToolbar(handler.MauiContext).GetFirstChildOfType(); + return new Size(context.FromPixels(toolbar.MeasuredWidth), context.FromPixels(toolbar.MeasuredHeight)); + } + public bool IsNavigationBarVisible(IElementHandler handler) => IsNavigationBarVisible(handler.MauiContext); diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs index 89fc6b5b67bb..87ab469ce799 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.Windows.cs @@ -5,6 +5,7 @@ using Microsoft.Maui; using Microsoft.Maui.Controls; using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Graphics; using Microsoft.Maui.Platform; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation.Peers; @@ -124,6 +125,12 @@ protected MauiToolbar GetPlatformToolbar(IMauiContext mauiContext) protected MauiToolbar GetPlatformToolbar(IElementHandler handler) => GetPlatformToolbar(handler.MauiContext); + protected Size GetTitleViewExpectedSize(IElementHandler handler) + { + var headerView = GetPlatformToolbar(handler.MauiContext); + return new Size(headerView.ActualWidth, headerView.ActualHeight); + } + public bool ToolbarItemsMatch( IElementHandler handler, params ToolbarItem[] toolbarItems) @@ -143,10 +150,13 @@ public bool ToolbarItemsMatch( return true; } - protected object GetTitleView(IElementHandler handler) + protected FrameworkElement GetTitleView(IElementHandler handler) { var toolbar = GetPlatformToolbar(handler); - return toolbar.TitleView; + return (FrameworkElement)toolbar.TitleView; } + + protected string GetToolbarTitle(IElementHandler handler) => + GetPlatformToolbar(handler).Title; } } diff --git a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.iOS.cs b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.iOS.cs index 8cc8eb55f905..b925ef98a348 100644 --- a/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.iOS.cs +++ b/src/Controls/tests/DeviceTests/ControlsHandlerTestBase.iOS.cs @@ -5,6 +5,8 @@ using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Controls.Platform.Compatibility; using Microsoft.Maui.DeviceTests.Stubs; +using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; using Microsoft.Maui.Platform; using UIKit; @@ -64,13 +66,19 @@ protected bool IsBackButtonVisible(IElementHandler handler) return !vcs[vcs.Length - 1].NavigationItem.HidesBackButton; } + protected bool IsNavigationBarVisible(IElementHandler handler) + { + var platformToolbar = GetPlatformToolbar(handler); + return platformToolbar?.Window != null; + } + protected object GetTitleView(IElementHandler handler) { var activeVC = GetVisibleViewController(handler); if (activeVC.NavigationItem.TitleView is ShellPageRendererTracker.TitleViewContainer tvc) { - return tvc.View.Handler.PlatformView; + return tvc.Subviews[0]; } return null; @@ -78,18 +86,32 @@ protected object GetTitleView(IElementHandler handler) UIViewController[] GetActiveChildViewControllers(IElementHandler handler) { + if (handler is IWindowHandler wh) + { + handler = wh.VirtualView.Content.Handler; + } + if (handler is ShellRenderer renderer) { if (renderer.ChildViewControllers[0] is ShellItemRenderer sir) { - if (sir.ChildViewControllers[0] is ShellSectionRenderer ssr) - { - return ssr.ChildViewControllers; - } + return sir.SelectedViewController.ChildViewControllers; } } - throw new NotImplementedException(); + var containerVC = (handler as IPlatformViewHandler).ViewController; + var view = handler.VirtualView.Parent; + + while (containerVC == null && view != null) + { + containerVC = (view?.Handler as IPlatformViewHandler).ViewController; + view = view?.Parent; + } + + if (containerVC == null) + return new UIViewController[0]; + + return new[] { containerVC }; } UIViewController GetVisibleViewController(IElementHandler handler) @@ -97,5 +119,47 @@ UIViewController GetVisibleViewController(IElementHandler handler) var vcs = GetActiveChildViewControllers(handler); return vcs[vcs.Length - 1]; } + + protected UINavigationBar GetPlatformToolbar(IElementHandler handler) + { + var visibleController = GetVisibleViewController(handler); + if (visibleController is UINavigationController nc) + return nc.NavigationBar; + + var navController = visibleController.NavigationController; + return navController?.NavigationBar; + } + + protected Size GetTitleViewExpectedSize(IElementHandler handler) + { + var titleContainer = GetPlatformToolbar(handler).FindDescendantView(result => + { + return result.Class.Name?.Contains("UINavigationBarTitleControl", StringComparison.OrdinalIgnoreCase) == true; + }); + + if (!OperatingSystem.IsIOSVersionAtLeast(16)) + { + titleContainer = titleContainer ?? GetPlatformToolbar(handler).FindDescendantView(result => + { + return result.Class.Name?.Contains("TitleViewContainer", StringComparison.OrdinalIgnoreCase) == true; + }); + } + + _ = titleContainer ?? throw new Exception("Unable to Locate TitleView Container"); + + return new Size(titleContainer.Frame.Width, titleContainer.Frame.Height); + } + + protected string GetToolbarTitle(IElementHandler handler) + { + var toolbar = GetPlatformToolbar(handler); + return AssertionExtensions.GetToolbarTitle(toolbar); + } + + protected string GetBackButtonText(IElementHandler handler) + { + var toolbar = GetPlatformToolbar(handler); + return AssertionExtensions.GetBackButtonText(toolbar); + } } } diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Android.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Android.cs index 3ed5c2009727..c216ebe20bec 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Android.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Android.cs @@ -47,8 +47,5 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async Assert.False(failed); }); } - - string GetToolbarTitle(IElementHandler handler) => - GetPlatformToolbar(handler).Title; } } diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Windows.cs index 19f602dff9c5..e62c59598717 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Windows.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.Windows.cs @@ -21,11 +21,6 @@ namespace Microsoft.Maui.DeviceTests [Category(TestCategory.NavigationPage)] public partial class NavigationPageTests : ControlsHandlerTestBase { - - string GetToolbarTitle(IElementHandler handler) => - GetPlatformToolbar(handler).Title; - - [Fact(DisplayName = "Back Button Enabled Changes with push/pop")] public async Task BackButtonEnabledChangesWithPushPop() { diff --git a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs index 0edaafa57131..5bb46b44b54d 100644 --- a/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/NavigationPage/NavigationPageTests.cs @@ -25,12 +25,13 @@ void SetupBuilder() handlers.AddHandler(typeof(Toolbar), typeof(ToolbarHandler)); #if IOS || MACCATALYST handlers.AddHandler(typeof(NavigationPage), typeof(NavigationRenderer)); + handlers.AddHandler(typeof(TabbedPage), typeof(TabbedRenderer)); #else handlers.AddHandler(typeof(NavigationPage), typeof(NavigationViewHandler)); + handlers.AddHandler(typeof(TabbedPage), typeof(TabbedViewHandler)); #endif handlers.AddHandler(); handlers.AddHandler(); - handlers.AddHandler(typeof(TabbedPage), typeof(TabbedViewHandler)); handlers.AddHandler(); }); }); @@ -50,7 +51,6 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async } #if !IOS && !MACCATALYST - [Fact(DisplayName = "Back Button Visibility Changes with push/pop")] public async Task BackButtonVisibilityChangesWithPushPop() { @@ -82,21 +82,21 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async Assert.True(IsBackButtonVisible(handler)); }); } +#endif [Fact(DisplayName = "Set Has Navigation Bar")] public async Task SetHasNavigationBar() { SetupBuilder(); - var navPage = new NavigationPage(new ContentPage()); + var navPage = new NavigationPage(new ContentPage() { Title = "Nav Bar" }); - await CreateHandlerAndAddToWindow(new Window(navPage), (handler) => + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => { - Assert.True(IsNavigationBarVisible(handler)); + Assert.True(await AssertionExtensions.Wait(() => IsNavigationBarVisible(handler))); NavigationPage.SetHasNavigationBar(navPage.CurrentPage, false); - Assert.False(IsNavigationBarVisible(handler)); + Assert.True(await AssertionExtensions.Wait(() => !IsNavigationBarVisible(handler))); NavigationPage.SetHasNavigationBar(navPage.CurrentPage, true); - Assert.True(IsNavigationBarVisible(handler)); - return Task.CompletedTask; + Assert.True(await AssertionExtensions.Wait(() => IsNavigationBarVisible(handler))); }); } @@ -112,10 +112,11 @@ await CreateHandlerAndAddToWindow(window, async (handler) => var contentPage = new ContentPage(); window.Page = contentPage; await OnLoadedAsync(contentPage); - Assert.False(IsNavigationBarVisible(handler)); + Assert.True(await AssertionExtensions.Wait(() => !IsNavigationBarVisible(handler))); }); } +#if !IOS && !MACCATALYST [Fact(DisplayName = "Toolbar Items Map Correctly")] public async Task ToolbarItemsMapCorrectly() { @@ -135,6 +136,7 @@ await CreateHandlerAndAddToWindow(new Window(navPage), (handl return Task.CompletedTask; }); } +#endif [Fact(DisplayName = "Toolbar Title")] public async Task ToolbarTitle() @@ -170,6 +172,8 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async // Just verifying that nothing crashes } + +#if !IOS && !MACCATALYST [Fact(DisplayName = "Insert Page Before RootPage ShowsBackButton")] public async Task InsertPageBeforeRootPageShowsBackButton() { @@ -190,6 +194,7 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async Assert.True(IsBackButtonVisible(navPage.Handler)); }); } +#endif [Fact(DisplayName = "Remove Root Page Hides Back Button")] public async Task RemoveRootPageHidesBackButton() @@ -244,6 +249,5 @@ await CreateHandlerAndAddToWindow(new Window(navPage), async }; }); } -#endif } } \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellFlyoutTests.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellFlyoutTests.cs index 13257682a799..7e302c75ec7b 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellFlyoutTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellFlyoutTests.cs @@ -65,7 +65,6 @@ await RunShellTest(shell => }); } -#if ANDROID || WINDOWS [Fact] public async Task FlyoutHeaderAdaptsToMinimumHeight() { @@ -88,10 +87,11 @@ await RunShellTest(shell => AssertionExtensions.CloseEnough(flyoutFrame.Height, 30); }); } -#endif -#if ANDROID +#if !WINDOWS + +#if ANDROID [Theory] [ClassData(typeof(ShellFlyoutHeaderBehaviorTestCases))] public async Task FlyoutHeaderMinimumHeight(FlyoutHeaderBehavior behavior) @@ -134,12 +134,13 @@ await RunShellTest(shell => [Fact] public async Task FlyoutContentSetsCorrectBottomPaddingWhenMinHeightIsSetForFlyoutHeader() { + var layout = new VerticalStackLayout() + { + new Label() { Text = "Flyout Header" } + }; + await RunShellTest(shell => { - var layout = new VerticalStackLayout() - { - new Label() { Text = "Flyout Header" } - }; layout.MinimumHeightRequest = 30; shell.FlyoutHeader = layout; @@ -157,17 +158,21 @@ await RunShellTest(shell => var footerFrame = GetFrameRelativeToFlyout(handler, (IView)shell.FlyoutFooter); // validate footer position - AssertionExtensions.CloseEnough(footerFrame.Y, headerFrame.Height + contentFrame.Height); + AssertionExtensions.CloseEnough(footerFrame.Y + layout.Margin.Top, headerFrame.Height + contentFrame.Height); }); } +#endif [Theory] [ClassData(typeof(ShellFlyoutHeaderBehaviorTestCases))] public async Task FlyoutHeaderContentAndFooterAllMeasureCorrectly(FlyoutHeaderBehavior behavior) { + // flyoutHeader.Margin.Top gets set to the SafeAreaPadding + // so we have to account for that in the default setup + var flyoutHeader = new Label() { Text = "Flyout Header" }; await RunShellTest(shell => { - shell.FlyoutHeader = new Label() { Text = "Flyout Header" }; + shell.FlyoutHeader = flyoutHeader; shell.FlyoutFooter = new Label() { Text = "Flyout Footer" }; shell.FlyoutContent = new VerticalStackLayout() { new Label() { Text = "Flyout Content" } }; shell.FlyoutHeaderBehavior = behavior; @@ -183,24 +188,25 @@ await RunShellTest(shell => // validate header position AssertionExtensions.CloseEnough(0, headerFrame.X, message: "Header X"); - AssertionExtensions.CloseEnough(0, headerFrame.Y, message: "Header Y"); + AssertionExtensions.CloseEnough(flyoutHeader.Margin.Top, headerFrame.Y, message: "Header Y"); AssertionExtensions.CloseEnough(flyoutFrame.Width, headerFrame.Width, message: "Header Width"); // validate content position AssertionExtensions.CloseEnough(0, contentFrame.X, message: "Content X"); - AssertionExtensions.CloseEnough(headerFrame.Height, contentFrame.Y, epsilon: 0.5, message: "Content Y"); + AssertionExtensions.CloseEnough(headerFrame.Height + flyoutHeader.Margin.Top, contentFrame.Y, epsilon: 0.5, message: "Content Y"); AssertionExtensions.CloseEnough(flyoutFrame.Width, contentFrame.Width, message: "Content Width"); // validate footer position AssertionExtensions.CloseEnough(0, footerFrame.X, message: "Footer X"); - AssertionExtensions.CloseEnough(headerFrame.Height + contentFrame.Height, footerFrame.Y, epsilon: 0.5, message: "Footer Y"); + AssertionExtensions.CloseEnough(headerFrame.Height + contentFrame.Height + flyoutHeader.Margin.Top, footerFrame.Y, epsilon: 0.5, message: "Footer Y"); AssertionExtensions.CloseEnough(flyoutFrame.Width, footerFrame.Width, message: "Footer Width"); //All three views should measure to the height of the flyout - AssertionExtensions.CloseEnough(headerFrame.Height + contentFrame.Height + footerFrame.Height, flyoutFrame.Height, epsilon: 0.5, message: "Total Height"); + AssertionExtensions.CloseEnough(headerFrame.Height + contentFrame.Height + footerFrame.Height + flyoutHeader.Margin.Top, flyoutFrame.Height, epsilon: 0.5, message: "Total Height"); }); } +#if ANDROID [Fact] public async Task FlyoutHeaderCollapsesOnScroll() { @@ -232,7 +238,6 @@ await RunShellTest(shell => async (shell, handler) => { await OpenFlyout(handler); - await Task.Delay(10); var initialBox = (shell.FlyoutHeader as IView).GetBoundingBox(); @@ -289,6 +294,8 @@ await RunShellTest(shell => ); }); } +#endif + #endif async Task RunShellTest(Action action, Func testAction) diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs index 6240f5bb1475..4c700c22342f 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.cs @@ -7,6 +7,7 @@ using Microsoft.Maui; using Microsoft.Maui.Controls; using Microsoft.Maui.Controls.Handlers; +using Microsoft.Maui.DeviceTests.Stubs; using Microsoft.Maui.Handlers; using Microsoft.Maui.Hosting; using Microsoft.Maui.Platform; @@ -428,9 +429,8 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } - - [Fact(DisplayName = "TitleView Causes a Crash When Switching Tabs")] - public async Task TitleViewCausesACrashWhenSwitchingTabs() + [Fact(DisplayName = "TitleView Updates to Currently Visible Page")] + public async Task TitleViewUpdateToCurrentlyVisiblePage() { SetupBuilder(); @@ -474,30 +474,91 @@ public async Task TitleViewCausesACrashWhenSwitchingTabs() await CreateHandlerAndAddToWindow(shell, async (handler) => { await OnLoadedAsync(page1); - // GotoAsync which switching tabs/flyout items currently - // doesn't resolve after navigated has finished which is why we have the - // delays - // https://github.com/dotnet/maui/issues/6193 Assert.Equal(titleView1.ToPlatform(), GetTitleView(handler)); await shell.GoToAsync("//Item2"); - await Task.Delay(200); - await OnLoadedAsync(page2); - Assert.Equal(titleView2.ToPlatform(), GetTitleView(handler)); + + Assert.True(await AssertionExtensions.Wait(() => titleView2.Handler != null && titleView2.ToPlatform() == GetTitleView(handler))); await shell.GoToAsync("//Item1"); - await Task.Delay(200); - await OnLoadedAsync(page1); - Assert.Equal(titleView1.ToPlatform(), GetTitleView(handler)); + + Assert.True(await AssertionExtensions.Wait(() => titleView1.Handler != null && titleView1.ToPlatform() == GetTitleView(handler))); await shell.GoToAsync("//Item2"); - await Task.Delay(200); - await OnLoadedAsync(page2); - Assert.Equal(titleView2.ToPlatform(), GetTitleView(handler)); + + Assert.True(await AssertionExtensions.Wait(() => titleView2.Handler != null && titleView2.ToPlatform() == GetTitleView(handler))); await shell.GoToAsync("//Item3"); - await Task.Delay(200); - await OnLoadedAsync(page3); - Assert.Equal(shellTitleView.ToPlatform(), GetTitleView(handler)); + + Assert.True(await AssertionExtensions.Wait(() => shellTitleView.Handler != null && shellTitleView.ToPlatform() == GetTitleView(handler))); }); } +#if IOS || MACCATALYST + [Fact(DisplayName = "TitleView Set On Shell Works After Navigation")] + public async Task TitleViewSetOnShellWorksAfterNavigation() + { + SetupBuilder(); + + var page1 = new ContentPage(); + var page2 = new ContentPage(); + var page3 = new ContentPage(); + + var shellTitleView = new Editor(); + + var shell = await CreateShellAsync((shell) => + { + Shell.SetTitleView(shell, shellTitleView); + shell.Items.Add(new TabBar() + { + Items = + { + new ShellContent() + { + Route = "Item1", + Content = page1 + }, + new ShellContent() + { + Route = "Item2", + Content = page2 + }, + } + }); + }); + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + await OnLoadedAsync(page1); + Assert.True(await AssertionExtensions.Wait(WaitCondition)); + + await shell.GoToAsync("//Item2"); + Assert.True(await AssertionExtensions.Wait(WaitCondition)); + + await shell.GoToAsync("//Item1"); + Assert.True(await AssertionExtensions.Wait(WaitCondition)); + + await shell.GoToAsync("//Item2"); + Assert.True(await AssertionExtensions.Wait(WaitCondition)); + + await shell.Navigation.PushAsync(page3); + Assert.True(await AssertionExtensions.Wait(WaitCondition)); + + await shell.Navigation.PopAsync(); + Assert.True(await AssertionExtensions.Wait(WaitCondition)); + + bool WaitCondition() + { + if (shellTitleView.Handler == null) + return false; + + var titleView = GetTitleView(handler); + + if (titleView == null) + return false; + + return shellTitleView.ToPlatform() == titleView; + } + }); + } +#endif + [Fact(DisplayName = "Handlers not recreated when changing tabs")] public async Task HandlersNotRecreatedWhenChangingTabs() { @@ -674,6 +735,126 @@ await CreateHandlerAndAddToWindow(shell, async (handler) => }); } + [Fact(DisplayName = "Toolbar Title Initializes")] + public async Task ToolbarTitleIntializes() + { + SetupBuilder(); + var navPage = new Shell() + { + CurrentItem = new ContentPage() + { + Title = "Page Title" + } + }; + + await CreateHandlerAndAddToWindow(new Window(navPage), (handler) => + { + string title = GetToolbarTitle(handler); + Assert.Equal("Page Title", title); + }); + } + + [Fact(DisplayName = "Toolbar Title View Updates")] + public async Task ToolbarTitleViewUpdates() + { + SetupBuilder(); + var page1 = new ContentPage() + { + Title = "Page 1" + }; + + var page2 = new ContentPage() + { + Title = "Page 2" + }; + + var navPage = new Shell() + { + CurrentItem = page1 + }; + + var titleView1 = new VerticalStackLayout(); + var titleView2 = new Label(); + + Shell.SetTitleView(page2, titleView1); + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + await navPage.Navigation.PushAsync(page2); + Assert.True(await AssertionExtensions.Wait(() => titleView1.Handler != null && titleView1.ToPlatform() == GetTitleView(handler))); + Shell.SetTitleView(page2, titleView2); + Assert.True(await AssertionExtensions.Wait(() => titleView2.Handler != null && titleView2.ToPlatform() == GetTitleView(handler))); + }); + } + + [Fact(DisplayName = "Toolbar Title Updates")] + public async Task ToolbarTitleUpdates() + { + SetupBuilder(); + var page1 = new ContentPage() + { + Title = "Page 1" + }; + + var page2 = new ContentPage() + { + Title = "Page 2" + }; + + var navPage = new Shell() + { + CurrentItem = page1 + }; + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + await navPage.Navigation.PushAsync(page2); + Assert.True(await AssertionExtensions.Wait(() => "Page 2" == GetToolbarTitle(handler))); + page2.Title = "New Title"; + page1.Title = "Previous Page Title"; // Ensuring this doesn't influence title + Assert.True(await AssertionExtensions.Wait(() => "New Title" == GetToolbarTitle(handler))); + }); + } + +#if !WINDOWS + [Fact(DisplayName = "Title View Measures")] + public async Task TitleViewMeasures() + { + SetupBuilder(); + var page1 = new ContentPage() + { + Title = "Page 1" + }; + + var navPage = new Shell() + { + CurrentItem = page1 + }; + + var titleView1 = new VerticalStackLayout() + { + new Label() + { + Text = "Title View" + } + }; + + Shell.SetTitleView(page1, titleView1); + + await CreateHandlerAndAddToWindow(new Window(navPage), async (handler) => + { + await OnFrameSetToNotEmpty(titleView1); + var containerSize = GetTitleViewExpectedSize(handler); + var titleView1PlatformSize = titleView1.GetBoundingBox(); + Assert.Equal(containerSize.Width, titleView1PlatformSize.Width); + Assert.Equal(containerSize.Height, titleView1PlatformSize.Height); + Assert.True(containerSize.Height > 0); + Assert.True(containerSize.Width > 0); + + }); + } +#endif + protected Task CreateShellAsync(Action action) => InvokeOnMainThreadAsync(() => { diff --git a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs index 75e8a9a48ab4..d979edac569a 100644 --- a/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs +++ b/src/Controls/tests/DeviceTests/Elements/Shell/ShellTests.iOS.cs @@ -14,15 +14,18 @@ using Microsoft.Maui.Platform; using UIKit; using Xunit; +using UIModalPresentationStyle = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIModalPresentationStyle; +using CoreGraphics; + +#if ANDROID || IOS || MACCATALYST +using ShellHandler = Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer; +#endif namespace Microsoft.Maui.DeviceTests { [Category(TestCategory.Shell)] public partial class ShellTests { - protected Task CheckFlyoutState(ShellRenderer renderer, bool result) => - throw new NotImplementedException(); - [Fact(DisplayName = "Swiping Away Modal Propagates to Shell")] public async Task SwipingAwayModalPropagatesToShell() { @@ -200,6 +203,109 @@ void ShellNavigating(object sender, ShellNavigatingEventArgs e) }); } + protected async Task OpenFlyout(ShellRenderer shellRenderer, TimeSpan? timeOut = null) + { + var flyoutView = GetFlyoutPlatformView(shellRenderer); + shellRenderer.Shell.FlyoutIsPresented = true; + + await AssertionExtensions.Wait(() => + { + return flyoutView.Frame.X == 0; + }, timeOut?.Milliseconds ?? 1000); + + return; + } + + internal Graphics.Rect GetFrameRelativeToFlyout(ShellRenderer shellRenderer, IView view) + { + var platformView = (view.Handler as IPlatformViewHandler).PlatformView; + return platformView.GetFrameRelativeTo(GetFlyoutPlatformView(shellRenderer)); + } + + protected Task CheckFlyoutState(ShellRenderer renderer, bool result) + { + var platformView = GetFlyoutPlatformView(renderer); + Assert.Equal(result, platformView.Frame.X == 0); + return Task.CompletedTask; + } + + protected UIView GetFlyoutPlatformView(ShellRenderer shellRenderer) + { + var vcs = shellRenderer.ViewController; + var flyoutContent = vcs.ChildViewControllers.OfType().First(); + return flyoutContent.View; + } + + internal Graphics.Rect GetFlyoutFrame(ShellRenderer shellRenderer) + { + var boundingbox = GetFlyoutPlatformView(shellRenderer).GetBoundingBox(); + + return new Graphics.Rect( + 0, + 0, + boundingbox.Width, + boundingbox.Height); + } + + + protected async Task ScrollFlyoutToBottom(ShellRenderer shellRenderer) + { + var platformView = GetFlyoutPlatformView(shellRenderer); + var tableView = platformView.FindDescendantView(); + var bottomOffset = new CGPoint(0, tableView.ContentSize.Height - tableView.Bounds.Height + tableView.ContentInset.Bottom); + tableView.SetContentOffset(bottomOffset, false); + await Task.Delay(1); + + return; + } +#if IOS + [Fact(DisplayName = "Back Button Text Has Correct Default")] + public async Task BackButtonTextHasCorrectDefault() + { + SetupBuilder(); + var shell = await CreateShellAsync(shell => + { + shell.CurrentItem = new ContentPage() { Title = "Page 1" }; + }); + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + await shell.Navigation.PushAsync(new ContentPage() { Title = "Page 2" }); + await OnNavigatedToAsync(shell.CurrentPage); + + Assert.True(await AssertionExtensions.Wait(() => GetBackButtonText(handler) == "Page 1")); + }); + } + + + [Fact(DisplayName = "Back Button Behavior Text")] + public async Task BackButtonBehaviorText() + { + SetupBuilder(); + var shell = await CreateShellAsync(shell => + { + shell.CurrentItem = new ContentPage() { Title = "Page 1" }; + }); + + await CreateHandlerAndAddToWindow(shell, async (handler) => + { + await OnLoadedAsync(shell.CurrentPage); + + var page2 = new ContentPage() { Title = "Page 2" }; + var page3 = new ContentPage() { Title = "Page 3" }; + + Shell.SetBackButtonBehavior(page3, new BackButtonBehavior() { TextOverride = "Text Override" }); + await shell.Navigation.PushAsync(page2); + await shell.Navigation.PushAsync(page3); + + Assert.True(await AssertionExtensions.Wait(() => GetBackButtonText(handler) == "Text Override")); + await shell.Navigation.PopAsync(); + Assert.True(await AssertionExtensions.Wait(() => GetBackButtonText(handler) == "Page 1")); + }); + } +#endif + async Task TapToSelect(ContentPage page) { var shellContent = page.Parent as ShellContent; diff --git a/src/Core/src/Platform/iOS/ViewExtensions.cs b/src/Core/src/Platform/iOS/ViewExtensions.cs index 0874ea676d9b..0a91ee6323a7 100644 --- a/src/Core/src/Platform/iOS/ViewExtensions.cs +++ b/src/Core/src/Platform/iOS/ViewExtensions.cs @@ -238,7 +238,7 @@ public static void UpdateBorder(this UIView platformView, IView view) wrapperView.Border = border; } - public static T? FindDescendantView(this UIView view) where T : UIView + internal static T? FindDescendantView(this UIView view, Func predicate) where T : UIView { var queue = new Queue(); queue.Enqueue(view); @@ -247,7 +247,7 @@ public static void UpdateBorder(this UIView platformView, IView view) { var descendantView = queue.Dequeue(); - if (descendantView is T result) + if (descendantView is T result && predicate.Invoke(result)) return result; for (var i = 0; i < descendantView.Subviews?.Length; i++) @@ -257,6 +257,9 @@ public static void UpdateBorder(this UIView platformView, IView view) return null; } + public static T? FindDescendantView(this UIView view) where T : UIView => + FindDescendantView(view, (_) => true); + public static void UpdateBackgroundLayerFrame(this UIView view) { if (view == null || view.Frame.IsEmpty) @@ -483,6 +486,18 @@ internal static Graphics.Rect GetBoundingBox(this UIView? platformView) return new Rect(nvb.X, nvb.Y, nvb.Width, nvb.Height); } + internal static Rect GetFrameRelativeTo(this UIView view, UIView relativeTo) + { + var viewWindowLocation = view.GetLocationOnScreen(); + var relativeToLocation = relativeTo.GetLocationOnScreen(); + + return + new Rect( + new Point(viewWindowLocation.X - relativeToLocation.X, viewWindowLocation.Y - relativeToLocation.Y), + new Graphics.Size(view.Bounds.Width, view.Bounds.Height) + ); + } + internal static UIView? GetParent(this UIView? view) { return view?.Superview; diff --git a/src/TestUtils/src/DeviceTests/AssertionExtensions.iOS.cs b/src/TestUtils/src/DeviceTests/AssertionExtensions.iOS.cs index aa50e15d0bbd..1ba890ccfabb 100644 --- a/src/TestUtils/src/DeviceTests/AssertionExtensions.iOS.cs +++ b/src/TestUtils/src/DeviceTests/AssertionExtensions.iOS.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using CoreAnimation; using CoreGraphics; @@ -554,5 +555,56 @@ public static UIView GetAccessiblePlatformView(this UIView platformView) return platformView; } + + static UIView GetBackButton(this UINavigationBar uINavigationBar) + { + var item = uINavigationBar.FindDescendantView(result => + { + return result.Class.Name?.Contains("UIButtonBarButton", StringComparison.OrdinalIgnoreCase) == true; + }); + + return item ?? throw new Exception("Unable to locate back button view"); + } + + public static void TapBackButton(this UINavigationBar uINavigationBar) + { + var item = uINavigationBar.GetBackButton(); + + var recognizer = item!.GestureRecognizers!.OfType().FirstOrDefault(); + _ = recognizer ?? throw new Exception("Unable to Back Button TapGestureRecognizer"); + + recognizer.State = UIGestureRecognizerState.Ended; + } + + public static string? GetToolbarTitle(this UINavigationBar uINavigationBar) + { + var item = uINavigationBar.FindDescendantView(result => + { + return result.Class.Name?.Contains("UINavigationBarTitleControl", StringComparison.OrdinalIgnoreCase) == true; + }); + + //Pre iOS 15 + item = item ?? uINavigationBar.FindDescendantView(result => + { + return result.Class.Name?.Contains("UINavigationBarContentView", StringComparison.OrdinalIgnoreCase) == true; + }); + + _ = item ?? throw new Exception("Unable to locate TitleBar Control"); + + var titleLabel = item.FindDescendantView(); + + _ = item ?? throw new Exception("Unable to locate UILabel Inside UINavigationBar"); + return titleLabel?.Text; + } + + public static string? GetBackButtonText(this UINavigationBar uINavigationBar) + { + var item = uINavigationBar.GetBackButton(); + + var titleLabel = item.FindDescendantView(); + + _ = item ?? throw new Exception("Unable to locate BackButton UILabel Inside UINavigationBar"); + return titleLabel?.Text; + } } } \ No newline at end of file