diff --git a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs index 701cdaeefca6..5faa88bf7490 100644 --- a/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs +++ b/src/Controls/src/Core/Hosting/AppHostBuilderExtensions.cs @@ -204,7 +204,7 @@ internal static IMauiHandlersCollection AddControlsHandlers(this IMauiHandlersCo #if IOS || MACCATALYST handlersCollection.AddHandler(typeof(NavigationPage), typeof(Handlers.Compatibility.NavigationRenderer)); - handlersCollection.AddHandler(typeof(TabbedPage), typeof(Handlers.Compatibility.TabbedRenderer)); + handlersCollection.AddHandler(); handlersCollection.AddHandler(typeof(FlyoutPage), typeof(Handlers.Compatibility.PhoneFlyoutPageRenderer)); #endif diff --git a/src/Controls/src/Core/TabbedPage/TabbedPage.Mapper.cs b/src/Controls/src/Core/TabbedPage/TabbedPage.Mapper.cs index 89d49d5d073b..aae6b0a02545 100644 --- a/src/Controls/src/Core/TabbedPage/TabbedPage.Mapper.cs +++ b/src/Controls/src/Core/TabbedPage/TabbedPage.Mapper.cs @@ -28,13 +28,16 @@ public partial class TabbedPage #endif -#if WINDOWS || ANDROID || TIZEN +#if PLATFORM TabbedViewHandler.PlatformViewFactory = OnCreatePlatformView; #endif -#if IOS - TabbedViewHandler.Mapper.ReplaceMapping(nameof(PlatformConfiguration.iOSSpecific.Page.PrefersHomeIndicatorAutoHiddenProperty), MapPrefersHomeIndicatorAutoHiddenProperty); - TabbedViewHandler.Mapper.ReplaceMapping(nameof(PlatformConfiguration.iOSSpecific.Page.PrefersStatusBarHiddenProperty), MapPrefersPrefersStatusBarHiddenProperty); +#if IOS || MACCATALYST + TabbedViewHandler.Mapper.ReplaceMapping(nameof(FlowDirection), MapFlowDirection); + TabbedViewHandler.Mapper.ReplaceMapping(PlatformConfiguration.iOSSpecific.Page.PrefersHomeIndicatorAutoHiddenProperty.PropertyName, MapPrefersHomeIndicatorAutoHiddenProperty); + TabbedViewHandler.Mapper.ReplaceMapping(PlatformConfiguration.iOSSpecific.Page.PrefersStatusBarHiddenProperty.PropertyName, MapPrefersPrefersStatusBarHiddenProperty); + TabbedViewHandler.Mapper.ReplaceMapping(PlatformConfiguration.iOSSpecific.Page.PreferredStatusBarUpdateAnimationProperty.PropertyName, MapPreferredStatusBarUpdateAnimation); + TabbedViewHandler.Mapper.ReplaceMapping(PlatformConfiguration.iOSSpecific.TabbedPage.TranslucencyModeProperty.PropertyName, MapTranslucencyMode); #endif } } diff --git a/src/Controls/src/Core/TabbedPage/TabbedPage.cs b/src/Controls/src/Core/TabbedPage/TabbedPage.cs index 6e1ec956363d..1385dda3a170 100644 --- a/src/Controls/src/Core/TabbedPage/TabbedPage.cs +++ b/src/Controls/src/Core/TabbedPage/TabbedPage.cs @@ -28,6 +28,18 @@ public partial class TabbedPage : MultiPage, IBarElement, IElementConfigur readonly Lazy> _platformConfigurationRegistry; + // Stores the collection change args from OnPagesChanged so MapItemsSource + // can handle Add/Remove incrementally instead of full rebuild. + internal NotifyCollectionChangedEventArgs _pendingPagesChangedArgs; + + // Stores the page whose Title/Icon changed so the mapper can refresh + // only that page's tab bar item instead of all children. + internal Page _pendingPropertyChangedPage; + + // Tracks pages with active PropertyChanged subscriptions so they can be + // unsubscribed on Reset (where Children is already empty and e.OldItems is null). + HashSet _subscribedPages; + /// Gets or sets the background color of the tab bar. This is a bindable property. public Color BarBackgroundColor { @@ -110,7 +122,38 @@ private protected override void OnHandlerChangingCore(HandlerChangingEventArgs a void OnPagesChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { WireUnwireChanges(false); + + // Unsubscribe removed pages — they're no longer in Children after mutation. + // On Reset, e.OldItems is null and Children is already empty, so use _subscribedPages. + if (e.OldItems is not null) + { + foreach (var item in e.OldItems) + { + if (item is Page page) + { + page.PropertyChanged -= OnPagePropertyChanged; + _subscribedPages?.Remove(page); + } + } + } + else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Reset + && _subscribedPages is not null) + { + // Reset path: Children is already empty, e.OldItems is null. + // Unsubscribe all previously tracked pages. + foreach (var page in _subscribedPages) + { + page.PropertyChanged -= OnPagePropertyChanged; + } + _subscribedPages.Clear(); + } + + _pendingPagesChangedArgs = e; Handler?.UpdateValue(TabbedPage.ItemsSourceProperty.PropertyName); + + // Clear after UpdateValue — iOS mapper consumes it synchronously during the call above. + // On other platforms the mapper doesn't use it, so clear to avoid retaining removed pages. + _pendingPagesChangedArgs = null; WireUnwireChanges(true); } @@ -119,16 +162,28 @@ void WireUnwireChanges(bool wire) foreach (var page in Children) { if (wire) + { page.PropertyChanged += OnPagePropertyChanged; + _subscribedPages ??= new HashSet(); + _subscribedPages.Add(page); + } else + { page.PropertyChanged -= OnPagePropertyChanged; + _subscribedPages?.Remove(page); + } } } void OnPagePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { - if (e.PropertyName == Page.TitleProperty.PropertyName) + if (e.PropertyName == Page.TitleProperty.PropertyName || + e.PropertyName == Page.IconImageSourceProperty.PropertyName) + { + _pendingPropertyChangedPage = sender as Page; Handler?.UpdateValue(TabbedPage.ItemsSourceProperty.PropertyName); + _pendingPropertyChangedPage = null; + } } } diff --git a/src/Controls/src/Core/TabbedPage/TabbedPage.iOS.cs b/src/Controls/src/Core/TabbedPage/TabbedPage.iOS.cs index 68b3da2a505c..805e0a69771b 100644 --- a/src/Controls/src/Core/TabbedPage/TabbedPage.iOS.cs +++ b/src/Controls/src/Core/TabbedPage/TabbedPage.iOS.cs @@ -1,52 +1,893 @@ #nullable disable using System; using System.Collections.Generic; -using System.Text; -using Microsoft.Maui.Handlers; +using System.Collections.Specialized; +using System.Threading.Tasks; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific; +using Microsoft.Maui.Platform; using UIKit; +using PageUIStatusBarAnimation = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.UIStatusBarAnimation; +using TabbedPageConfiguration = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.TabbedPage; +using TranslucencyMode = Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.TranslucencyMode; namespace Microsoft.Maui.Controls { public partial class TabbedPage { + // Instance state for tracking tab bar defaults (used by appearance mappers) + bool _barBackgroundColorWasSet; + bool _barTextColorWasSet; + UIColor _defaultBarTextColor; + bool _defaultBarTextColorSet; + UIColor _defaultBarColor; + bool _defaultBarColorSet; + bool? _defaultBarTranslucent; + UITabBarAppearance _tabBarAppearance; + Brush _currentBarBackground; + + // Tracks pages from the previous MapItemsSource call so we can + // disconnect handlers for pages that were removed. + HashSet _previousPages; + + // Per-page generation counter to detect stale async icon loads in SetTabBarItem + Dictionary _tabBarItemGeneration; + + static UIView OnCreatePlatformView(ViewHandler arg) + { + if (arg.VirtualView is TabbedPage tabbedPage && arg is TabbedViewHandler handler) + { + return tabbedPage.CreatePlatformView(handler); + } + + throw new InvalidOperationException("TabbedViewHandler.PlatformViewFactory requires a TabbedPage and TabbedViewHandler."); + } + + partial void OnHandlerChangingPartial(HandlerChangingEventArgs args) + { + if (args.OldHandler is null) + { + return; + } + + // Handler is being removed or replaced — clean up old resources + + // Unsubscribe manager events to prevent leaks + if (args.OldHandler is TabbedViewHandler oldHandler) + { + var manager = oldHandler.Manager; + + if (manager is not null) + { + manager.ViewDidAppear -= OnManagerViewDidAppear; + manager.ViewDidDisappear -= OnManagerViewDidDisappear; + manager.TabsReordered -= OnManagerTabsReordered; + manager.GetCurrentPageViewControllerFunc = null; + manager.Dispose(); + } + } + + // Dispose UITabBarAppearance + _tabBarAppearance?.Dispose(); + _tabBarAppearance = null; + + // Reset native defaults so they're recaptured from the new tab bar on reconnect + _defaultBarColorSet = false; + _defaultBarTextColorSet = false; + _defaultBarTranslucent = null; + _barBackgroundColorWasSet = false; + _barTextColorWasSet = false; + _defaultBarColor = null; + _defaultBarTextColor = null; + + // Clean up gradient brush subscription + if (_currentBarBackground is GradientBrush gradientBrush) + { + gradientBrush.Parent = null; + gradientBrush.InvalidateGradientBrushRequested -= OnBarBackgroundChanged; + } + _currentBarBackground = null; + + // Clear previous pages tracking + _previousPages = null; + _tabBarItemGeneration = null; + } + + UIView CreatePlatformView(TabbedViewHandler handler) + { + var manager = new TabBarControllerManager(handler); + handler.SetManager(manager); + + // Subscribe to lifecycle events from the manager + manager.ViewDidAppear += OnManagerViewDidAppear; + manager.ViewDidDisappear += OnManagerViewDidDisappear; + + // Subscribe to tab reorder events + manager.TabsReordered += OnManagerTabsReordered; + + // Provide current page VC callback for status bar / home indicator delegation + manager.GetCurrentPageViewControllerFunc = () => GetViewController(CurrentPage); + + return manager.View; + } + + void OnManagerViewDidAppear(object sender, EventArgs e) + { + SendAppearing(); + } + + void OnManagerViewDidDisappear(object sender, EventArgs e) + { + SendDisappearing(); + } + + void OnManagerTabsReordered(UIViewController[] viewControllers) + { + UpdateChildrenOrderIndex(viewControllers); + } + + static TabBarControllerManager GetManager(ITabbedViewHandler handler) + { + if (handler is TabbedViewHandler tvh) + { + return tvh.Manager; + } + + return null; + } + + static UITabBar GetTabBar(ITabbedViewHandler handler) + { + return GetManager(handler)?.TabBar; + } + + static UIViewController GetViewController(Page page) + { + if (page?.Handler is not IPlatformViewHandler nvh) + { + return null; + } + + return nvh.ViewController; + } + + internal static void MapFlowDirection(ITabbedViewHandler handler, TabbedPage view) + { + var manager = GetManager(handler); + + if (manager is null) + { + return; + } + + // Apply FlowDirection to the controller's View (enables child MatchParent resolution) + manager.View.UpdateFlowDirection(view); + + // Prevent tab item reversal — the handler's extracted view propagates + // SemanticContentAttribute to the TabBar unlike the renderer + manager.TabBar.SemanticContentAttribute = UISemanticContentAttribute.Unspecified; + + foreach (var child in view.InternalChildren) + { + if (child is Page page && page.Handler?.PlatformView is UIView childView) + { + childView.UpdateFlowDirection(page); + } + } + } + internal static void MapBarBackground(ITabbedViewHandler handler, TabbedPage view) { + var tabBar = GetTabBar(handler); + + if (tabBar is null) + { + return; + } + + if (view._currentBarBackground is GradientBrush oldGradientBrush) + { + oldGradientBrush.Parent = null; + oldGradientBrush.InvalidateGradientBrushRequested -= view.OnBarBackgroundChanged; + } + + view._currentBarBackground = view.BarBackground; + + if (view._currentBarBackground is GradientBrush newGradientBrush) + { + newGradientBrush.Parent = view; + newGradientBrush.InvalidateGradientBrushRequested += view.OnBarBackgroundChanged; + } + + tabBar.UpdateBackground(view._currentBarBackground); } + + void OnBarBackgroundChanged(object sender, EventArgs e) + { + var tabBar = GetTabBar(Handler as ITabbedViewHandler); + tabBar?.UpdateBackground(_currentBarBackground); + } + internal static void MapBarBackgroundColor(ITabbedViewHandler handler, TabbedPage view) { + var tabBar = GetTabBar(handler); + + if (tabBar is null) + { + return; + } + + var barBackgroundColor = view.BarBackgroundColor; + var isDefaultColor = barBackgroundColor is null; + + if (isDefaultColor && !view._barBackgroundColorWasSet) + { + return; + } + + if (!view._defaultBarColorSet) + { + view._defaultBarColor = tabBar.BarTintColor; + view._defaultBarColorSet = true; + } + + if (!isDefaultColor) + { + view._barBackgroundColorWasSet = true; + } + + if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15)) + { + view.UpdateiOS15TabBarAppearance(tabBar); + } + else + { + tabBar.BarTintColor = isDefaultColor ? view._defaultBarColor : barBackgroundColor.ToPlatform(); + } } + internal static void MapBarTextColor(ITabbedViewHandler handler, TabbedPage view) { + var tabBar = GetTabBar(handler); + if (tabBar is null || tabBar.Items is null) + { + return; + } + + var barTextColor = view.BarTextColor; + var isDefaultColor = barTextColor is null; + + if (isDefaultColor && !view._barTextColorWasSet) + { + return; + } + + if (!view._defaultBarTextColorSet) + { + view._defaultBarTextColor = tabBar.TintColor; + view._defaultBarTextColorSet = true; + } + + if (!isDefaultColor) + { + view._barTextColorWasSet = true; + } + + UIColor tabBarTextColor; + + if (isDefaultColor) + { + tabBarTextColor = view._defaultBarTextColor; + } + else + { + tabBarTextColor = barTextColor.ToPlatform(); + } + + foreach (UITabBarItem item in tabBar.Items) + { + item.SetTitleTextAttributes(new UIStringAttributes() { ForegroundColor = tabBarTextColor }, UIControlState.Normal); + item.SetTitleTextAttributes(new UIStringAttributes() { ForegroundColor = tabBarTextColor }, UIControlState.Selected); + item.SetTitleTextAttributes(new UIStringAttributes() { ForegroundColor = tabBarTextColor }, UIControlState.Disabled); + } + + // Set TintColor for selected icon + if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15)) + { + view.UpdateiOS15TabBarAppearance(tabBar); + } + else + { + tabBar.TintColor = isDefaultColor ? view._defaultBarTextColor : barTextColor.ToPlatform(); + } } + internal static void MapUnselectedTabColor(ITabbedViewHandler handler, TabbedPage view) { + var tabBar = GetTabBar(handler); + if (tabBar is null || tabBar.Items is null) + { + return; + } + + if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15)) + { + view.UpdateiOS15TabBarAppearance(tabBar); + } + else + { + if (view.IsSet(UnselectedTabColorProperty) && view.UnselectedTabColor is not null) + { + tabBar.UnselectedItemTintColor = view.UnselectedTabColor.ToPlatform(); + } + else + { + tabBar.UnselectedItemTintColor = UITabBar.Appearance.TintColor; + } + } } + internal static void MapSelectedTabColor(ITabbedViewHandler handler, TabbedPage view) { + var tabBar = GetTabBar(handler); + + if (tabBar is null || tabBar.Items is null) + { + return; + } + + if (view.IsSet(SelectedTabColorProperty) && view.SelectedTabColor is not null) + { + tabBar.TintColor = view.SelectedTabColor.ToPlatform(); + } + else + { + tabBar.TintColor = UITabBar.Appearance.TintColor; + } + + if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15)) + { + view.UpdateiOS15TabBarAppearance(tabBar); + } } internal static void MapItemsSource(ITabbedViewHandler handler, TabbedPage view) { + var manager = GetManager(handler); + + if (manager is null) + { + view._pendingPagesChangedArgs = null; + return; + } + + var mauiContext = handler.MauiContext; + if (mauiContext is null) + { + view._pendingPagesChangedArgs = null; + return; + } + + // Consume the pending args (if any) + var args = view._pendingPagesChangedArgs; + view._pendingPagesChangedArgs = null; + + // Try incremental update for simple Add/Remove + if (args is not null && view._previousPages is not null) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add when args.NewItems is not null: + HandleIncrementalAdd(handler, view, manager, mauiContext, args); + return; + + case NotifyCollectionChangedAction.Remove when args.OldItems is not null: + HandleIncrementalRemove(handler, view, manager, args); + return; + } + } + + // No pending args + pages already set up = Title/Icon change only. + // Refresh tab bar items without full rebuild (matches renderer's UpdateTabBarItem approach). + if (args is null && view._previousPages is not null) + { + HandleTabBarItemRefresh(view, manager); + return; + } + + // Full rebuild for Reset, Replace, Move, or initial load + HandleFullRebuild(handler, view, manager, mauiContext); + } + + static void HandleIncrementalAdd(ITabbedViewHandler handler, TabbedPage view, + TabBarControllerManager manager, IMauiContext mauiContext, NotifyCollectionChangedEventArgs args) + { + // Setup only the new pages + foreach (var item in args.NewItems) + { + if (item is not Page page) + { + continue; + } + + var pageHandler = (IPlatformViewHandler)page.ToHandler(mauiContext); + view.SetTabBarItem(pageHandler, manager); + view._previousPages?.Add(page); + } + + // Rebuild the VC array from current children + var list = new List(); + foreach (var child in view.InternalChildren) + { + if (child is Page p) + { + var vc = GetViewController(p); + + if (vc is not null) + { + list.Add(vc); + } + } + } + + var controllersArray = list.ToArray(); + manager.ViewControllers = controllersArray; + manager.UpdateTabBarVisibility(); + + // Refresh Tags so UpdateChildrenOrderIndex maps to correct pages + RefreshTabBarItemTags(view); + + // Restore SelectedViewController from CurrentPage (UIKit can reset selection on VC reassignment) + UIViewController controller = null; + if (view.CurrentPage is Page currentPage) + { + controller = GetViewController(currentPage); + } + if (controller is not null && controller != manager.SelectedViewController + && Array.IndexOf(controllersArray, controller) >= 0) + { + manager.SelectedViewController = controller; + } + + // Re-apply appearance for new items + MapBarBackgroundColor(handler, view); + MapBarTextColor(handler, view); + MapSelectedTabColor(handler, view); + MapUnselectedTabColor(handler, view); + } + + static void HandleIncrementalRemove(ITabbedViewHandler handler, TabbedPage view, + TabBarControllerManager manager, NotifyCollectionChangedEventArgs args) + { + // Teardown only the removed pages + foreach (var item in args.OldItems) + { + if (item is not Page page) + { + continue; + } + + view._previousPages?.Remove(page); + view._tabBarItemGeneration?.Remove(page); + page.Handler?.DisconnectHandler(); + } + + // Rebuild the VC array from current children + var list = new List(); + foreach (var child in view.InternalChildren) + { + if (child is Page p) + { + var vc = GetViewController(p); + + if (vc is not null) + { + list.Add(vc); + } + } + } + + var controllersArray = list.ToArray(); + manager.ViewControllers = controllersArray; + manager.UpdateTabBarVisibility(); + + // Refresh Tags so UpdateChildrenOrderIndex maps to correct pages + RefreshTabBarItemTags(view); + + // Ensure selected VC is still valid + UIViewController controller = null; + if (view.CurrentPage is Page currentPage) + { + controller = GetViewController(currentPage); + } + if (controller is not null && controller != manager.SelectedViewController + && Array.IndexOf(controllersArray, controller) >= 0) + { + manager.SelectedViewController = controller; + } + + MapBarBackgroundColor(handler, view); + MapBarTextColor(handler, view); + MapSelectedTabColor(handler, view); + MapUnselectedTabColor(handler, view); + } + + static void HandleFullRebuild(ITabbedViewHandler handler, TabbedPage view, + TabBarControllerManager manager, IMauiContext mauiContext) + { + var currentPages = new HashSet(); + var list = new List(); + var pages = view.InternalChildren; + for (var i = 0; i < pages.Count; i++) + { + var child = pages[i]; + + if (child is not Page page) + { + continue; + } + + currentPages.Add(page); + var pageHandler = (IPlatformViewHandler)page.ToHandler(mauiContext); + view.SetTabBarItem(pageHandler, manager); + + var vc = GetViewController(page); + + if (vc is not null) + { + list.Add(vc); + } + } + + // Disconnect handlers for pages that were removed since last rebuild + if (view._previousPages is not null) + { + foreach (var oldPage in view._previousPages) + { + if (!currentPages.Contains(oldPage)) + { + view._tabBarItemGeneration?.Remove(oldPage); + oldPage.Handler?.DisconnectHandler(); + } + } + } + + view._previousPages = currentPages; + + var controllersArray = list.ToArray(); + manager.ViewControllers = controllersArray; + + manager.UpdateTabBarVisibility(); + + UIViewController controller = null; + + if (view.CurrentPage is Page currentPage) + { + controller = GetViewController(currentPage); + } + if (controller is not null && controller != manager.SelectedViewController + && Array.IndexOf(controllersArray, controller) >= 0) + { + manager.SelectedViewController = controller; + } + + MapBarBackgroundColor(handler, view); + MapBarTextColor(handler, view); + MapSelectedTabColor(handler, view); + MapUnselectedTabColor(handler, view); + } + + static void HandleTabBarItemRefresh(TabbedPage view, TabBarControllerManager manager) + { + // If a specific page changed, update only that page's tab bar item + var changedPage = view._pendingPropertyChangedPage; + if (changedPage is not null && changedPage.Handler is IPlatformViewHandler changedHandler) + { + view.SetTabBarItem(changedHandler, manager); + return; + } + + // Fallback: update all tab bar items (e.g. trait collection change) + foreach (var child in view.InternalChildren) + { + if (child is Page page && page.Handler is IPlatformViewHandler pageHandler) + { + view.SetTabBarItem(pageHandler, manager); + } + } } + internal static void MapItemTemplate(ITabbedViewHandler handler, TabbedPage view) { + // ItemTemplate changes trigger a full rebuild via MapItemsSource + MapItemsSource(handler, view); } + internal static void MapSelectedItem(ITabbedViewHandler handler, TabbedPage view) { + // SelectedItem is synced via CurrentPage + MapCurrentPage(handler, view); } + internal static void MapCurrentPage(ITabbedViewHandler handler, TabbedPage view) { + var manager = GetManager(handler); + if (manager is null) + { + return; + } + + // Determine sync direction using the handler's flag. + // When a native tab tap fires OnTabSelected, NativeSelectionInProgress is true + // and we sync native→virtual. When CurrentPage is set programmatically, + // the flag is false and we sync virtual→native. + bool isNativeSelection = handler is TabbedViewHandler tvh && tvh.NativeSelectionInProgress; + + if (isNativeSelection) + { + // Native → virtual: user tapped a tab, update CurrentPage to match + var nativeIndex = (int)manager.SelectedIndex; + var count = view.InternalChildren.Count; + + if (nativeIndex >= 0 && nativeIndex < count) + { + var nativeCurrentPage = view.GetPageByIndex(nativeIndex); + + if (nativeCurrentPage is not null && nativeCurrentPage != view.CurrentPage) + { + view.CurrentPage = nativeCurrentPage; + } + } + } + else + { + // Virtual → native: CurrentPage set programmatically, update SelectedViewController + var current = view.CurrentPage; + if (current is null) + { + return; + } + + // Don't set SelectedViewController if CurrentPage is no longer in Children + if (view.Children.IndexOf(current) < 0) + { + return; + } + + var controller = GetViewController(current); + if (controller is null) + { + return; + } + + // Verify the controller is in ViewControllers before setting + var viewControllers = manager.ViewControllers; + if (viewControllers is null || Array.IndexOf(viewControllers, controller) < 0) + { + return; + } + + // Update status bar / home indicator for the new current page + var tabBarController = manager.TabBarController; + tabBarController?.SetNeedsUpdateOfHomeIndicatorAutoHidden(); + tabBarController?.SetNeedsStatusBarAppearanceUpdate(); + if (controller != manager.SelectedViewController) + { + manager.SelectedViewController = controller; + } + } } internal static void MapPrefersHomeIndicatorAutoHiddenProperty(ITabbedViewHandler handler, TabbedPage view) { - view.CurrentPage.Handler.UpdateValue(nameof(PlatformConfiguration.iOSSpecific.Page.PrefersHomeIndicatorAutoHiddenProperty)); + var manager = GetManager(handler); + if (manager is null) + { + return; + } + + manager.TabBarController?.SetNeedsUpdateOfHomeIndicatorAutoHidden(); } internal static void MapPrefersPrefersStatusBarHiddenProperty(ITabbedViewHandler handler, TabbedPage view) { - view.CurrentPage.Handler.UpdateValue(nameof(PlatformConfiguration.iOSSpecific.Page.PrefersStatusBarHiddenProperty)); + var manager = GetManager(handler); + if (manager is null) + { + return; + } + + // Propagate status bar hidden preference to all child pages (matching renderer) + var viewControllers = manager.ViewControllers; + + if (viewControllers is not null) + { + for (var i = 0; i < viewControllers.Length; i++) + { + view.GetPageByIndex(i).OnThisPlatform().SetPrefersStatusBarHidden( + view.OnThisPlatform().PrefersStatusBarHidden()); + } + } + + // Propagate preferred status bar update animation to current page + PageUIStatusBarAnimation animation = view.OnThisPlatform().PreferredStatusBarUpdateAnimation(); + view.CurrentPage?.OnThisPlatform().SetPreferredStatusBarUpdateAnimation(animation); + + manager.TabBarController?.SetNeedsStatusBarAppearanceUpdate(); + } + + internal static void MapPreferredStatusBarUpdateAnimation(ITabbedViewHandler handler, TabbedPage view) + { + var manager = GetManager(handler); + if (manager is null) + return; + + PageUIStatusBarAnimation animation = view.OnThisPlatform().PreferredStatusBarUpdateAnimation(); + view.CurrentPage?.OnThisPlatform().SetPreferredStatusBarUpdateAnimation(animation); + manager.TabBarController?.SetNeedsStatusBarAppearanceUpdate(); + } + + internal static void MapTranslucencyMode(ITabbedViewHandler handler, TabbedPage view) + { + var tabBar = GetTabBar(handler); + + if (tabBar is null) + { + return; + } + + view._defaultBarTranslucent = view._defaultBarTranslucent ?? tabBar.Translucent; + switch (TabbedPageConfiguration.GetTranslucencyMode(view)) + { + case TranslucencyMode.Translucent: + tabBar.Translucent = true; + return; + case TranslucencyMode.Opaque: + tabBar.Translucent = false; + return; + default: + tabBar.Translucent = view._defaultBarTranslucent.GetValueOrDefault(); + return; + } + } + + // Tab bar item creation — ported from TabbedRenderer.SetTabBarItem + async void SetTabBarItem(IPlatformViewHandler renderer, TabBarControllerManager manager) + { + var page = renderer.VirtualView as Page; + + if (page is null) + { + return; + } + + // Increment generation to invalidate any in-flight icon loads for this page + _tabBarItemGeneration ??= new Dictionary(); + _tabBarItemGeneration.TryGetValue(page, out var previousGen); + var currentGen = previousGen + 1; + _tabBarItemGeneration[page] = currentGen; + + var icons = await GetIcon(page); + + // Post-await guard: page or TabbedPage handler may have been removed during icon load + if (page.Handler is null || renderer.ViewController is null || Children.IndexOf(page) < 0 + || Handler is not TabbedViewHandler tvh || tvh.Manager is not TabBarControllerManager currentManager + || currentManager != manager) + { + icons?.Item1?.Dispose(); + icons?.Item2?.Dispose(); + return; + } + + // Stale icon guard: a newer SetTabBarItem call superseded this one + if (_tabBarItemGeneration.TryGetValue(page, out var latestGen) && latestGen != currentGen) + { + icons?.Item1?.Dispose(); + icons?.Item2?.Dispose(); + return; + } + + var resizedImage = TabbedViewExtensions.AutoResizeTabBarImage(currentManager.TraitCollection, icons?.Item1); + var resizedSelectedImage = TabbedViewExtensions.AutoResizeTabBarImage(currentManager.TraitCollection, icons?.Item2); + + renderer.ViewController.TabBarItem = new UITabBarItem(page.Title, resizedImage, resizedSelectedImage) + { + Tag = Children.IndexOf(page), + AccessibilityIdentifier = page.AutomationId + }; + + resizedImage?.Dispose(); + resizedSelectedImage?.Dispose(); + icons?.Item1?.Dispose(); + icons?.Item2?.Dispose(); + } + + Task> GetIcon(Page page) + { + var source = new TaskCompletionSource>(); + + var mauiContext = Handler?.MauiContext; + if (mauiContext is null || page.IconImageSource is null) + { + source.SetResult(null); + return source.Task; + } + + try + { + page.IconImageSource.LoadImage(mauiContext, result => + { + if (result?.Value is null) + { + source.TrySetResult(null); + } + else + { + source.TrySetResult(Tuple.Create(result.Value, (UIImage)null)); + } + }); + } + catch + { + source.TrySetResult(null); + } + + return source.Task; + } + + [System.Runtime.Versioning.SupportedOSPlatform("ios15.0")] + [System.Runtime.Versioning.SupportedOSPlatform("tvos15.0")] + void UpdateiOS15TabBarAppearance(UITabBar tabBar) + { + tabBar.UpdateiOS15TabBarAppearance( + ref _tabBarAppearance, + _defaultBarColor, + _defaultBarTextColor, + IsSet(SelectedTabColorProperty) ? SelectedTabColor : null, + IsSet(UnselectedTabColorProperty) ? UnselectedTabColor : null, + IsSet(BarBackgroundColorProperty) ? BarBackgroundColor : null, + IsSet(BarTextColorProperty) ? BarTextColor : null, + IsSet(BarTextColorProperty) ? BarTextColor : null); + } + + void UpdateChildrenOrderIndex(UIViewController[] viewControllers) + { + for (var i = 0; i < viewControllers.Length; i++) + { + var tabBarItem = viewControllers[i]?.TabBarItem; + + if (tabBarItem is null) + { + continue; + } + + var originalIndex = (int)tabBarItem.Tag; + + if (originalIndex < 0 || originalIndex >= InternalChildren.Count) + { + continue; + } + + var page = InternalChildren[originalIndex] as Page; + + if (page is not null) + { + SetIndex(page, i); + } + } + } + + static void RefreshTabBarItemTags(TabbedPage view) + { + foreach (var child in view.InternalChildren) + { + if (child is Page page && page.Handler is IPlatformViewHandler pvh + && pvh.ViewController?.TabBarItem is UITabBarItem tabBarItem) + { + tabBarItem.Tag = view.Children.IndexOf(page); + } + } } } } diff --git a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/DynamicFontImageSourceColorShouldApplyOnTabIcon.png b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/DynamicFontImageSourceColorShouldApplyOnTabIcon.png index 636ced77f537..28eca0ca5fd8 100644 Binary files a/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/DynamicFontImageSourceColorShouldApplyOnTabIcon.png and b/src/Controls/tests/TestCases.Mac.Tests/snapshots/mac/DynamicFontImageSourceColorShouldApplyOnTabIcon.png differ diff --git a/src/Core/src/Handlers/TabbedView/TabbedViewHandler.iOS.cs b/src/Core/src/Handlers/TabbedView/TabbedViewHandler.iOS.cs new file mode 100644 index 000000000000..7a32f61efea1 --- /dev/null +++ b/src/Core/src/Handlers/TabbedView/TabbedViewHandler.iOS.cs @@ -0,0 +1,135 @@ +#nullable disable +using System; +using Microsoft.Maui.Platform; +using UIKit; + +namespace Microsoft.Maui.Handlers +{ + public partial class TabbedViewHandler : IPlatformViewHandler, ITabBarManagerDelegate + { + TabBarControllerManager _manager; + + /// + /// Gets the UITabBarController managed by this handler. + /// + internal TabBarControllerManager Manager => _manager; + + /// + /// Gets the TabBarController so the Controls layer can access UITabBar for appearance. + /// + internal UITabBarController TabBarController => _manager?.TabBarController; + + /// + /// Sets the TabBarControllerManager. Called from the Controls layer PlatformViewFactory. + /// + internal void SetManager(TabBarControllerManager manager) + { + _manager = manager; + } + + protected override void ConnectHandler(UIView platformView) + { + base.ConnectHandler(platformView); + + // Set the ViewController so this handler participates in the VC hierarchy + ViewController = _manager?.TabBarController; + } + + protected override void DisconnectHandler(UIView platformView) + { + // Don't dispose the manager here — DisconnectHandler can be a transient + // detach/re-attach. The Controls layer handles cleanup via OnHandlerChangingPartial, + // and the manager is disposed when the handler is fully removed. + ViewController = null; + base.DisconnectHandler(platformView); + } + + #region ITabBarManagerDelegate + + /// + /// Indicates that the current MapCurrentPage call originated from a native tab tap, + /// not from a programmatic CurrentPage change. Used by the Controls layer to + /// determine sync direction (native→virtual vs virtual→native). + /// + internal bool NativeSelectionInProgress { get; private set; } + + void ITabBarManagerDelegate.OnTabSelected(int index) + { + // Set flag so MapCurrentPage knows to do native→virtual sync + NativeSelectionInProgress = true; + try + { + // Use interface property — typed VirtualView throws after DisconnectHandler + if (((IElementHandler)this).VirtualView is IElement element) + { + element.Handler?.UpdateValue("CurrentPage"); + } + } + finally + { + NativeSelectionInProgress = false; + } + } + + void ITabBarManagerDelegate.OnTabsReordered(UIViewController[] viewControllers) + { + // Raise the manager's event so the Controls layer can update children order + _manager?.RaiseTabsReordered(viewControllers); + } + + UIViewController ITabBarManagerDelegate.GetCurrentPageViewController() + { + // No-op fallback — the Controls layer provides the real lookup via + // GetCurrentPageViewControllerFunc which is always set before this is reachable. + return null; + } + + void ITabBarManagerDelegate.OnViewDidAppear() + { + // Raise the manager's event so the Controls layer can call SendAppearing + _manager?.RaiseViewDidAppear(); + } + + void ITabBarManagerDelegate.OnViewDidDisappear() + { + // Raise the manager's event so the Controls layer can call SendDisappearing + _manager?.RaiseViewDidDisappear(); + } + + void ITabBarManagerDelegate.OnViewDidLayoutSubviews() + { + // Use interface property — typed VirtualView throws after DisconnectHandler + if (((IElementHandler)this).VirtualView is IView view && _manager?.View is UIView platformView) + view.Arrange(platformView.Bounds.ToRectangle()); + } + + void ITabBarManagerDelegate.OnTraitCollectionDidChange(UITraitCollection previousTraitCollection) + { + // Use interface property — typed VirtualView throws after DisconnectHandler + if (((IElementHandler)this).VirtualView is not IElement element || _manager is null) + { + return; + } + + if (previousTraitCollection?.VerticalSizeClass == _manager.TraitCollection?.VerticalSizeClass) + { + return; + } + + // Trigger icon resize by refreshing all tab bar items + element.Handler?.UpdateValue("ItemsSource"); + } + + #endregion + + #region IPlatformViewHandler + + UIView IPlatformViewHandler.PlatformView => PlatformView; + + UIView IPlatformViewHandler.ContainerView => ContainerView; + + UIViewController IPlatformViewHandler.ViewController => ViewController; + + #endregion + } +} diff --git a/src/Core/src/Platform/iOS/TabBarControllerManager/ITabBarManagerDelegate.cs b/src/Core/src/Platform/iOS/TabBarControllerManager/ITabBarManagerDelegate.cs new file mode 100644 index 000000000000..86bf941448c5 --- /dev/null +++ b/src/Core/src/Platform/iOS/TabBarControllerManager/ITabBarManagerDelegate.cs @@ -0,0 +1,51 @@ +#nullable disable +using UIKit; + +namespace Microsoft.Maui.Platform +{ + /// + /// Delegate interface for consumer-specific tab bar behavior. + /// Implemented by TabbedPage's handler to customize + /// behavior. + /// + internal interface ITabBarManagerDelegate + { + /// + /// Called when a tab is selected by the user. + /// The consumer should update its virtual view's current page. + /// + void OnTabSelected(int index); + + /// + /// Called when the tab bar finishes customization (reordering). + /// The consumer should update children order indices. + /// + void OnTabsReordered(UIViewController[] viewControllers); + + /// + /// Returns the current page's view controller for status bar/home indicator delegation. + /// + UIViewController GetCurrentPageViewController(); + + /// + /// Called when the UITabBarController's view has appeared. + /// + void OnViewDidAppear(); + + /// + /// Called when the UITabBarController's view has disappeared. + /// + void OnViewDidDisappear(); + + /// + /// Called when the UITabBarController's view needs layout. + /// + void OnViewDidLayoutSubviews(); + + /// + /// Called when the trait collection changes (e.g. iPad rotation). + /// Used to resize tab bar icons. + /// + void OnTraitCollectionDidChange(UITraitCollection previousTraitCollection); + } +} diff --git a/src/Core/src/Platform/iOS/TabBarControllerManager/TabBarControllerManager.cs b/src/Core/src/Platform/iOS/TabBarControllerManager/TabBarControllerManager.cs new file mode 100644 index 000000000000..0a03bb29942c --- /dev/null +++ b/src/Core/src/Platform/iOS/TabBarControllerManager/TabBarControllerManager.cs @@ -0,0 +1,272 @@ +#nullable disable +using System; +using CoreGraphics; +using Microsoft.Maui.Graphics; +using UIKit; + +namespace Microsoft.Maui.Platform +{ + /// + /// Manages a UITabBarController for TabbedPage's handler architecture on iOS. + /// Owns the UITabBarController and provides tab management operations, + /// tab bar appearance, iOS 18 compatibility fixes, and lifecycle handling. + /// + internal class TabBarControllerManager : IDisposable + { + readonly ITabBarManagerDelegate _delegate; + readonly MauiTabBarController _tabBarController; + bool _disposed; + + /// + /// Gets the managed UITabBarController instance. + /// + public UITabBarController TabBarController => _tabBarController; + + /// + /// Gets the TabBar from the managed UITabBarController. + /// + public UITabBar TabBar => _tabBarController.TabBar; + + /// + /// Gets the View from the managed UITabBarController. + /// + public UIView View => _tabBarController.View; + + /// + /// Gets or sets the view controllers displayed as tabs. + /// + public UIViewController[] ViewControllers + { + get => _tabBarController.ViewControllers; + set + { + _tabBarController.ViewControllers = value; + + // UIKit resets CustomizableViewControllers to all VCs on each assignment. + // Disable tab reordering to match renderer behavior. + _tabBarController.CustomizableViewControllers = null; + } + } + + /// + /// Gets or sets the currently selected view controller. + /// + public UIViewController SelectedViewController + { + get => _tabBarController.SelectedViewController; + set => _tabBarController.SelectedViewController = value; + } + + /// + /// Gets the index of the currently selected tab. + /// + public nint SelectedIndex => _tabBarController.SelectedIndex; + + /// + /// Gets or sets the customizable view controllers (set to null to disable tab reordering). + /// + public UIViewController[] CustomizableViewControllers + { + get => _tabBarController.CustomizableViewControllers; + set => _tabBarController.CustomizableViewControllers = value; + } + + /// + /// Gets the MoreNavigationController for tab overflow (>5 tabs). + /// + public UINavigationController MoreNavigationController => _tabBarController.MoreNavigationController; + + /// + /// Gets the trait collection from the UITabBarController. + /// + public UITraitCollection TraitCollection => _tabBarController.TraitCollection; + + public TabBarControllerManager(ITabBarManagerDelegate managerDelegate) + { + _delegate = managerDelegate ?? throw new ArgumentNullException(nameof(managerDelegate)); + _tabBarController = new MauiTabBarController(this); + } + + /// + /// Raised when the UITabBarController's ViewDidAppear is called. + /// The Controls layer subscribes to this to send Page.Appearing. + /// + internal event EventHandler ViewDidAppear; + + /// + /// Raised when the UITabBarController's ViewDidDisappear is called. + /// The Controls layer subscribes to this to send Page.Disappearing. + /// + internal event EventHandler ViewDidDisappear; + + /// + /// Callback set by the Controls layer to return the current page's ViewController. + /// Used for status bar and home indicator delegation. + /// + internal Func GetCurrentPageViewControllerFunc { get; set; } + + /// + /// Raised when tabs are reordered by the user. + /// The Controls layer subscribes to update children order indices. + /// + internal event Action TabsReordered; + + /// + /// Ensures the tab bar remains visible on MacCatalyst 18+. + /// DisableiOS18ToolbarTabs() sets Mode = TabSidebar which causes iOS + /// to set TabBar.Hidden = true and Alpha = 0. This overrides that behavior. + /// + public void UpdateTabBarVisibility() + { + if (TabBar is null) + { + return; + } + + if (OperatingSystem.IsMacCatalystVersionAtLeast(18) || OperatingSystem.IsIOSVersionAtLeast(18)) + { +#if MACCATALYST + if (TabBar.Hidden || TabBar.Alpha != 1.0f) + { + TabBar.Alpha = 1.0f; + TabBar.Hidden = false; + } +#endif + } + } + + internal void RaiseViewDidAppear() => ViewDidAppear?.Invoke(this, EventArgs.Empty); + internal void RaiseViewDidDisappear() => ViewDidDisappear?.Invoke(this, EventArgs.Empty); + internal void RaiseTabsReordered(UIViewController[] viewControllers) => TabsReordered?.Invoke(viewControllers); + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _tabBarController?.Dispose(); + } + + sealed class MauiTabBarController : UITabBarController + { + readonly WeakReference _managerRef; + + public MauiTabBarController(TabBarControllerManager manager) + { + _managerRef = new WeakReference(manager); + + // Apply iOS 18 tab bar fixes + this.DisableiOS18ToolbarTabs(); + + // Subscribe to tab reordering events + FinishedCustomizingViewControllers += HandleFinishedCustomizingViewControllers; + } + + public override UIViewController SelectedViewController + { + get => base.SelectedViewController; + set + { + base.SelectedViewController = value; + + // If the selected view controller is the "More" navigation controller, + // do not update the current page + if (value == MoreNavigationController) + { + return; + } + + if (_managerRef is not null && _managerRef.TryGetTarget(out var manager)) + { + var index = (int)SelectedIndex; + manager._delegate.OnTabSelected(index); + } + } + } + + public override UIViewController ChildViewControllerForStatusBarHidden() + { + if (_managerRef is not null && _managerRef.TryGetTarget(out var manager)) + return manager.GetCurrentPageViewControllerFunc?.Invoke() + ?? manager._delegate.GetCurrentPageViewController(); + + return null; + } + + public override UIViewController ChildViewControllerForHomeIndicatorAutoHidden + { + get + { + if (_managerRef is not null && _managerRef.TryGetTarget(out var manager)) + return manager.GetCurrentPageViewControllerFunc?.Invoke() + ?? manager._delegate.GetCurrentPageViewController(); + + return null; + } + } + + public override void ViewDidAppear(bool animated) + { + base.ViewDidAppear(animated); + + if (_managerRef is not null && _managerRef.TryGetTarget(out var manager)) + { + manager._delegate.OnViewDidAppear(); + } + } + + public override void ViewDidDisappear(bool animated) + { + base.ViewDidDisappear(animated); + + if (_managerRef is not null && _managerRef.TryGetTarget(out var manager)) + { + manager._delegate.OnViewDidDisappear(); + } + } + + public override void ViewDidLayoutSubviews() + { + base.ViewDidLayoutSubviews(); + + if (_managerRef is not null && _managerRef.TryGetTarget(out var manager)) + { + manager._delegate.OnViewDidLayoutSubviews(); + } + } + + public override void TraitCollectionDidChange(UITraitCollection previousTraitCollection) + { +#pragma warning disable CA1422 // Validate platform compatibility + base.TraitCollectionDidChange(previousTraitCollection); +#pragma warning restore CA1422 + + if (_managerRef is not null && _managerRef.TryGetTarget(out var manager)) + { + manager._delegate.OnTraitCollectionDidChange(previousTraitCollection); + } + } + + void HandleFinishedCustomizingViewControllers(object sender, UITabBarCustomizeChangeEventArgs e) + { + if (e.Changed && _managerRef is not null && _managerRef.TryGetTarget(out var manager)) + { + manager._delegate.OnTabsReordered(e.ViewControllers); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + FinishedCustomizingViewControllers -= HandleFinishedCustomizingViewControllers; + } + + base.Dispose(disposing); + } + } + } +} diff --git a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 095f8961dec0..ce3522c1b350 100644 --- a/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -8,6 +8,8 @@ abstract Microsoft.Maui.HybridWebViewInvoker.InvokeMethodAsync(string! methodNam override Microsoft.Maui.Handlers.LabelHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Handlers.ShapeViewHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Handlers.StepperHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size +~override Microsoft.Maui.Handlers.TabbedViewHandler.ConnectHandler(UIKit.UIView platformView) -> void +~override Microsoft.Maui.Handlers.TabbedViewHandler.DisconnectHandler(UIKit.UIView platformView) -> void override Microsoft.Maui.Platform.MauiTextView.TextAlignment.get -> UIKit.UITextAlignment override Microsoft.Maui.Platform.MauiTextView.TextAlignment.set -> void override Microsoft.Maui.Platform.MauiView.DidUpdateFocus(UIKit.UIFocusUpdateContext! context, UIKit.UIFocusAnimationCoordinator! coordinator) -> void diff --git a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index 095f8961dec0..ce3522c1b350 100644 --- a/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Core/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -8,6 +8,8 @@ abstract Microsoft.Maui.HybridWebViewInvoker.InvokeMethodAsync(string! methodNam override Microsoft.Maui.Handlers.LabelHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Handlers.ShapeViewHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Handlers.StepperHandler.GetDesiredSize(double widthConstraint, double heightConstraint) -> Microsoft.Maui.Graphics.Size +~override Microsoft.Maui.Handlers.TabbedViewHandler.ConnectHandler(UIKit.UIView platformView) -> void +~override Microsoft.Maui.Handlers.TabbedViewHandler.DisconnectHandler(UIKit.UIView platformView) -> void override Microsoft.Maui.Platform.MauiTextView.TextAlignment.get -> UIKit.UITextAlignment override Microsoft.Maui.Platform.MauiTextView.TextAlignment.set -> void override Microsoft.Maui.Platform.MauiView.DidUpdateFocus(UIKit.UIFocusUpdateContext! context, UIKit.UIFocusAnimationCoordinator! coordinator) -> void