File: Compatibility\Handlers\TabbedPage\iOS\TabbedRenderer.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Graphics;
using UIKit;
using static Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific.Page;
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.Handlers.Compatibility
{
	public class TabbedRenderer : UITabBarController, IPlatformViewHandler
	{
		bool _barBackgroundColorWasSet;
		bool _barTextColorWasSet;
		UIColor _defaultBarTextColor;
		bool _defaultBarTextColorSet;
		UIColor _defaultBarColor;
		bool _defaultBarColorSet;
		bool? _defaultBarTranslucent;
		IMauiContext _mauiContext;
		UITabBarAppearance _tabBarAppearance;
		WeakReference<VisualElement> _element;
 
		IMauiContext MauiContext => _mauiContext;
		public static IPropertyMapper<TabbedPage, TabbedRenderer> Mapper = new PropertyMapper<TabbedPage, TabbedRenderer>(TabbedViewHandler.ViewMapper);
		public static CommandMapper<TabbedPage, TabbedRenderer> CommandMapper = new CommandMapper<TabbedPage, TabbedRenderer>(TabbedViewHandler.ViewCommandMapper);
 
		ViewHandlerDelegator<TabbedPage> _viewHandlerWrapper;
		Page Page => Element as Page;
 
		[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
		public TabbedRenderer()
		{
			this.DisableiOS18ToolbarTabs();
			_viewHandlerWrapper = new ViewHandlerDelegator<TabbedPage>(Mapper, CommandMapper, this);
		}
 
		public override UIViewController SelectedViewController
		{
			get { return base.SelectedViewController; }
			set
			{
				base.SelectedViewController = value;
				UpdateCurrentPage();
			}
		}
 
		protected TabbedPage Tabbed
		{
			get { return (TabbedPage)Element; }
		}
 
		public VisualElement Element => _viewHandlerWrapper.Element ?? _element?.GetTargetOrDefault();
 
		public event EventHandler<VisualElementChangedEventArgs> ElementChanged;
 
		public Size GetDesiredSize(double widthConstraint, double heightConstraint) =>
			this.GetDesiredSizeFromHandler(widthConstraint, heightConstraint);
 
		public UIView NativeView
		{
			get { return View; }
		}
 
		public void SetElement(VisualElement element)
		{
			_viewHandlerWrapper.SetVirtualView(element, OnElementChanged, false);
			_element = element is null ? null : new(element);
 
			FinishedCustomizingViewControllers += HandleFinishedCustomizingViewControllers;
			if (element is TabbedPage tabbed)
			{
				tabbed.PropertyChanged += OnPropertyChanged;
				tabbed.PagesChanged += OnPagesChanged;
			}
 
			OnPagesChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
 
			//disable edit/reorder of tabs
			CustomizableViewControllers = null;
 
			UpdateBarBackgroundColor();
			UpdateBarBackground();
			UpdateBarTextColor();
			UpdateSelectedTabColors();
			UpdateBarTranslucent();
			UpdatePageSpecifics();
		}
 
		public UIViewController ViewController
		{
			get { return this; }
		}
 
		[System.Runtime.Versioning.UnsupportedOSPlatform("ios8.0")]
		[System.Runtime.Versioning.UnsupportedOSPlatform("tvos")]
		public override void DidRotate(UIInterfaceOrientation fromInterfaceOrientation)
		{
			base.DidRotate(fromInterfaceOrientation);
 
			View.SetNeedsLayout();
		}
 
		public override void ViewDidAppear(bool animated)
		{
			Page?.SendAppearing();
			base.ViewDidAppear(animated);
		}
 
		public override void ViewDidDisappear(bool animated)
		{
			base.ViewDidDisappear(animated);
			Page?.SendDisappearing();
		}
 
		public override void ViewDidLayoutSubviews()
		{
			base.ViewDidLayoutSubviews();
 
			if (Element is IView view)
				view.Arrange(View.Bounds.ToRectangle());
		}
 
		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				_tabBarAppearance?.Dispose();
				_tabBarAppearance = null;
 
				Page?.SendDisappearing();
 
				if (Tabbed is TabbedPage tabbed)
				{
					tabbed.PropertyChanged -= OnPropertyChanged;
					tabbed.PagesChanged -= OnPagesChanged;
				}
 
				FinishedCustomizingViewControllers -= HandleFinishedCustomizingViewControllers;
			}
 
			base.Dispose(disposing);
		}
 
		protected virtual void OnElementChanged(VisualElementChangedEventArgs e)
		{
			var changed = ElementChanged;
			if (changed != null)
				changed(this, e);
		}
 
		UIViewController GetViewController(Page page)
		{
			if (page?.Handler is not IPlatformViewHandler nvh)
				return null;
 
			return nvh.ViewController;
		}
 
		void HandleFinishedCustomizingViewControllers(object sender, UITabBarCustomizeChangeEventArgs e)
		{
			if (e.Changed)
				UpdateChildrenOrderIndex(e.ViewControllers);
		}
 
		void OnPagePropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			// Setting TabBarItem.Title in iOS 10 causes rendering bugs
			// Work around this by creating a new UITabBarItem on each change
			if (e.PropertyName == Page.IconImageSourceProperty.PropertyName || e.PropertyName == Page.TitleProperty.PropertyName)
			{
				var page = (Page)sender;
 
				IPlatformViewHandler renderer = page.ToHandler(_mauiContext);
 
				if (renderer?.ViewController.TabBarItem == null)
					return;
 
				SetTabBarItem(renderer);
			}
		}
 
		void OnPagesChanged(object sender, NotifyCollectionChangedEventArgs e)
		{
			e.Apply((o, i, c) => SetupPage((Page)o, i), (o, i) => TeardownPage((Page)o, i), Reset);
 
			SetControllers();
 
			UIViewController controller = null;
			if (Tabbed?.CurrentPage is Page currentPage)
				controller = GetViewController(currentPage);
			if (controller != null && controller != base.SelectedViewController)
				base.SelectedViewController = controller;
 
			UpdateBarBackgroundColor();
			UpdateBarTextColor();
			UpdateSelectedTabColors();
		}
 
		void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (e.PropertyName == nameof(TabbedPage.CurrentPage))
			{
				var current = Tabbed?.CurrentPage;
				if (current == null)
					return;
 
				var controller = GetViewController(current);
				if (controller == null)
					return;
 
				SetNeedsUpdateOfHomeIndicatorAutoHidden();
				SetNeedsStatusBarAppearanceUpdate();
				SelectedViewController = controller;
			}
			else if (e.PropertyName == TabbedPage.BarBackgroundColorProperty.PropertyName)
				UpdateBarBackgroundColor();
			else if (e.PropertyName == TabbedPage.BarBackgroundProperty.PropertyName)
				UpdateBarBackground();
			else if (e.PropertyName == TabbedPage.BarTextColorProperty.PropertyName)
				UpdateBarTextColor();
			else if (e.PropertyName == PrefersStatusBarHiddenProperty.PropertyName)
				UpdatePrefersStatusBarHiddenOnPages();
			else if (e.PropertyName == PreferredStatusBarUpdateAnimationProperty.PropertyName)
				UpdateCurrentPagePreferredStatusBarUpdateAnimation();
			else if (e.PropertyName == TabbedPage.SelectedTabColorProperty.PropertyName || e.PropertyName == TabbedPage.UnselectedTabColorProperty.PropertyName)
				UpdateSelectedTabColors();
			else if (e.PropertyName == PrefersHomeIndicatorAutoHiddenProperty.PropertyName || e.PropertyName == PrefersStatusBarHiddenProperty.PropertyName)
				UpdatePageSpecifics();
			else if (e.PropertyName == TabbedPageConfiguration.TranslucencyModeProperty.PropertyName)
				UpdateBarTranslucent();
 
		}
 
		public override UIViewController ChildViewControllerForStatusBarHidden()
		{
			var current = Tabbed?.CurrentPage;
			if (current == null)
				return null;
 
			return GetViewController(current);
		}
 
		void UpdateCurrentPagePreferredStatusBarUpdateAnimation()
		{
			if (Page is Page page)
			{
				PageUIStatusBarAnimation animation = page.OnThisPlatform().PreferredStatusBarUpdateAnimation();
				Tabbed?.CurrentPage?.OnThisPlatform().SetPreferredStatusBarUpdateAnimation(animation);
			}
		}
 
		void UpdatePrefersStatusBarHiddenOnPages()
		{
			if (Tabbed is not TabbedPage tabbed)
				return;
			for (var i = 0; i < ViewControllers.Length; i++)
			{
				tabbed.GetPageByIndex(i).OnThisPlatform().SetPrefersStatusBarHidden(tabbed.OnThisPlatform().PrefersStatusBarHidden());
			}
		}
 
		public override UIViewController ChildViewControllerForHomeIndicatorAutoHidden
		{
			get
			{
				var current = Tabbed?.CurrentPage;
				if (current == null)
					return null;
 
				return GetViewController(current);
			}
		}
 
		void UpdatePageSpecifics()
		{
			ChildViewControllerForHomeIndicatorAutoHidden?.SetNeedsUpdateOfHomeIndicatorAutoHidden();
			ChildViewControllerForStatusBarHidden()?.SetNeedsStatusBarAppearanceUpdate();
		}
 
		void Reset()
		{
			if (Tabbed is not TabbedPage tabbed)
				return;
			var i = 0;
			foreach (var page in tabbed.Children)
				SetupPage(page, i++);
		}
 
		void SetControllers()
		{
			if (Tabbed is not TabbedPage tabbed)
				return;
			var list = new List<UIViewController>();
			var pages = tabbed.InternalChildren;
			for (var i = 0; i < pages.Count; i++)
			{
				var child = pages[i];
				var v = child as Page;
				if (v == null)
					continue;
				if (GetViewController(v) != null)
					list.Add(GetViewController(v));
			}
			ViewControllers = list.ToArray();
		}
 
		void SetupPage(Page page, int index)
		{
			var renderer = (IPlatformViewHandler)page.ToHandler(_mauiContext);
 
			page.PropertyChanged += OnPagePropertyChanged;
 
			SetTabBarItem(renderer);
		}
 
		void TeardownPage(Page page, int index)
		{
			page.PropertyChanged -= OnPagePropertyChanged;
 
			page.Handler?.DisconnectHandler();
		}
 
		void UpdateBarBackgroundColor()
		{
			if (Tabbed is not TabbedPage tabbed || TabBar == null)
				return;
 
			var barBackgroundColor = tabbed.BarBackgroundColor;
			var isDefaultColor = barBackgroundColor == null;
 
			if (isDefaultColor && !_barBackgroundColorWasSet)
				return;
 
			if (!_defaultBarColorSet)
			{
				_defaultBarColor = TabBar.BarTintColor;
 
				_defaultBarColorSet = true;
			}
 
			if (!isDefaultColor)
				_barBackgroundColorWasSet = true;
 
			if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15))
				UpdateiOS15TabBarAppearance();
			else
				TabBar.BarTintColor = isDefaultColor ? _defaultBarColor : barBackgroundColor.ToPlatform();
		}
 
		void UpdateBarBackground()
		{
			if (Tabbed is not TabbedPage tabbed || TabBar == null)
				return;
 
			var barBackground = tabbed.BarBackground;
 
			TabBar.UpdateBackground(barBackground);
		}
 
		void UpdateBarTextColor()
		{
			if (Tabbed is not TabbedPage tabbed || TabBar == null || TabBar.Items == null)
				return;
 
			var barTextColor = tabbed.BarTextColor;
			var isDefaultColor = barTextColor == null;
 
			if (isDefaultColor && !_barTextColorWasSet)
				return;
 
			if (!_defaultBarTextColorSet)
			{
				_defaultBarTextColor = TabBar.TintColor;
				_defaultBarTextColorSet = true;
			}
 
			if (!isDefaultColor)
				_barTextColorWasSet = true;
 
			UIColor tabBarTextColor;
			if (isDefaultColor)
				tabBarTextColor = _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
			// setting the unselected icon tint is not supported by iOS
			if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15))
				UpdateiOS15TabBarAppearance();
			else
			{
				TabBar.TintColor = isDefaultColor ? _defaultBarTextColor : barTextColor.ToPlatform();
			}
		}
 
		void UpdateBarTranslucent()
		{
			if (Tabbed == null || TabBar == null || Element is not VisualElement element)
				return;
 
			_defaultBarTranslucent = _defaultBarTranslucent ?? TabBar.Translucent;
			switch (TabbedPageConfiguration.GetTranslucencyMode(element))
			{
				case TranslucencyMode.Translucent:
					TabBar.Translucent = true;
					return;
				case TranslucencyMode.Opaque:
					TabBar.Translucent = false;
					return;
				default:
					TabBar.Translucent = _defaultBarTranslucent.GetValueOrDefault();
					return;
			}
		}
 
		void UpdateChildrenOrderIndex(UIViewController[] viewControllers)
		{
			if (Tabbed is not TabbedPage tabbed)
				return;
			for (var i = 0; i < viewControllers.Length; i++)
			{
				var originalIndex = -1;
				if (int.TryParse(viewControllers[i].TabBarItem.Tag.ToString(), out originalIndex))
				{
					var page = (Page)tabbed.InternalChildren[originalIndex];
					TabbedPage.SetIndex(page, i);
				}
			}
		}
 
		void UpdateCurrentPage()
		{
			if (Tabbed is TabbedPage tabbed)
			{
				var count = tabbed.InternalChildren.Count;
				var index = (int)SelectedIndex;
				tabbed.CurrentPage = index >= 0 && index < count ? tabbed.GetPageByIndex(index) : null;
			}
		}
 
		async void SetTabBarItem(IPlatformViewHandler renderer)
		{
			var page = renderer.VirtualView as Page;
			if (page == null)
				throw new InvalidCastException($"{nameof(renderer)} must be a {nameof(Page)} renderer.");
 
			var icons = await GetIcon(page);
			renderer.ViewController.TabBarItem = new UITabBarItem(page.Title, icons?.Item1, icons?.Item2)
			{
				Tag = Tabbed?.Children.IndexOf(page) ?? -1,
				AccessibilityIdentifier = page.AutomationId
			};
			icons?.Item1?.Dispose();
			icons?.Item2?.Dispose();
		}
 
		void UpdateSelectedTabColors()
		{
			if (Tabbed is not TabbedPage tabbed || TabBar == null || TabBar.Items == null)
				return;
 
			if (tabbed.IsSet(TabbedPage.SelectedTabColorProperty) && tabbed.SelectedTabColor != null)
			{
				TabBar.TintColor = tabbed.SelectedTabColor.ToPlatform();
			}
			else
			{
				TabBar.TintColor = UITabBar.Appearance.TintColor;
			}
 
			if (OperatingSystem.IsIOSVersionAtLeast(15) || OperatingSystem.IsTvOSVersionAtLeast(15))
				UpdateiOS15TabBarAppearance();
			else
			{
				if (tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) && tabbed.UnselectedTabColor != null)
					TabBar.UnselectedItemTintColor = tabbed.UnselectedTabColor.ToPlatform();
				else
					TabBar.UnselectedItemTintColor = UITabBar.Appearance.TintColor;
			}
		}
 
		/// <summary>
		/// Get the icon for the tab bar item of this page
		/// </summary>
		/// <returns>
		/// A tuple containing as item1: the unselected version of the icon, item2: the selected version of the icon (item2 can be null),
		/// or null if no icon should be set.
		/// </returns>
		protected virtual Task<Tuple<UIImage, UIImage>> GetIcon(Page page)
		{
			TaskCompletionSource<Tuple<UIImage, UIImage>> source =
				new TaskCompletionSource<Tuple<UIImage, UIImage>>();
 
			page.IconImageSource.LoadImage(MauiContext, result =>
			{
				if (result?.Value == null)
					source.SetResult(null);
				else
					source.SetResult(Tuple.Create(result.Value, (UIImage)null));
			});
 
			return source.Task;
		}
 
		[System.Runtime.Versioning.SupportedOSPlatform("ios15.0")]
		[System.Runtime.Versioning.SupportedOSPlatform("tvos15.0")]
		void UpdateiOS15TabBarAppearance()
		{
			if (Tabbed is not TabbedPage tabbed)
				return;
			TabBar.UpdateiOS15TabBarAppearance(
				ref _tabBarAppearance,
				_defaultBarColor,
				_defaultBarTextColor,
				tabbed.IsSet(TabbedPage.SelectedTabColorProperty) ? tabbed.SelectedTabColor : null,
				tabbed.IsSet(TabbedPage.UnselectedTabColorProperty) ? tabbed.UnselectedTabColor : null,
				tabbed.IsSet(TabbedPage.BarBackgroundColorProperty) ? tabbed.BarBackgroundColor : null,
				tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null,
				tabbed.IsSet(TabbedPage.BarTextColorProperty) ? tabbed.BarTextColor : null);
		}
 
		#region IPlatformViewHandler
		bool IViewHandler.HasContainer { get => false; set { } }
 
		object IViewHandler.ContainerView => null;
 
		IView IViewHandler.VirtualView => Element;
 
		object IElementHandler.PlatformView => NativeView;
 
		Maui.IElement IElementHandler.VirtualView => Element;
 
		IMauiContext IElementHandler.MauiContext => _mauiContext;
 
		UIView IPlatformViewHandler.PlatformView => NativeView;
 
		UIView IPlatformViewHandler.ContainerView => null;
 
		UIViewController IPlatformViewHandler.ViewController => this;
 
		void IViewHandler.PlatformArrange(Rect rect) =>
			_viewHandlerWrapper.PlatformArrange(rect);
 
		void IElementHandler.SetMauiContext(IMauiContext mauiContext)
		{
			_mauiContext = mauiContext;
		}
 
		void IElementHandler.SetVirtualView(Maui.IElement view)
		{
			SetElement((VisualElement)view);
		}
 
		void IElementHandler.UpdateValue(string property)
		{
			_viewHandlerWrapper.UpdateProperty(property);
		}
 
		void IElementHandler.Invoke(string command, object args)
		{
			_viewHandlerWrapper.Invoke(command, args);
		}
 
		void IElementHandler.DisconnectHandler()
		{
			_viewHandlerWrapper.DisconnectHandler();
		}
		#endregion
	}
}