File: Compatibility\Handlers\Shell\iOS\ShellRenderer.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Controls.Platform.Compatibility;
using Microsoft.Maui.Controls.PlatformConfiguration.iOSSpecific;
using Microsoft.Maui.Graphics;
using UIKit;
 
namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
	public class ShellRenderer : UIViewController, IShellContext, IPlatformViewHandler
	{
		public static IPropertyMapper<Shell, ShellRenderer> Mapper = new PropertyMapper<Shell, ShellRenderer>(ViewHandler.ViewMapper);
		public static CommandMapper<Shell, ShellRenderer> CommandMapper = new CommandMapper<Shell, ShellRenderer>(ViewHandler.ViewCommandMapper);
 
		[Microsoft.Maui.Controls.Internals.Preserve(Conditional = true)]
		public ShellRenderer()
		{
 
		}
 
		public override bool PrefersHomeIndicatorAutoHidden
			=> Shell?.CurrentPage?.OnThisPlatform()?.PrefersHomeIndicatorAutoHidden() ?? base.PrefersHomeIndicatorAutoHidden;
 
 
		public override bool PrefersStatusBarHidden()
			=> Shell?.CurrentPage?.OnThisPlatform()?.PrefersStatusBarHidden() == StatusBarHiddenMode.True;
 
		public override UIKit.UIStatusBarAnimation PreferredStatusBarUpdateAnimation
		{
			get
			{
				var mode = Shell?.CurrentPage?.OnThisPlatform()?.PreferredStatusBarUpdateAnimation();
				return mode switch
				{
					PlatformConfiguration.iOSSpecific.UIStatusBarAnimation.None => UIKit.UIStatusBarAnimation.None,
					PlatformConfiguration.iOSSpecific.UIStatusBarAnimation.Fade => UIKit.UIStatusBarAnimation.Fade,
					PlatformConfiguration.iOSSpecific.UIStatusBarAnimation.Slide => UIKit.UIStatusBarAnimation.Slide,
					_ => base.PreferredStatusBarUpdateAnimation,
				};
			}
		}
 
 
		#region IShellContext
 
		bool IShellContext.AllowFlyoutGesture
		{
			get
			{
				ShellSection shellSection = Shell?.CurrentItem?.CurrentItem;
				if (shellSection == null)
					return true;
				return shellSection.Stack.Count <= 1;
			}
		}
 
		IShellItemRenderer IShellContext.CurrentShellItemRenderer => _currentShellItemRenderer;
 
		IShellNavBarAppearanceTracker IShellContext.CreateNavBarAppearanceTracker()
		{
			return CreateNavBarAppearanceTracker();
		}
 
		IShellPageRendererTracker IShellContext.CreatePageRendererTracker()
		{
			return CreatePageRendererTracker();
		}
 
		IShellFlyoutContentRenderer IShellContext.CreateShellFlyoutContentRenderer()
		{
			return CreateShellFlyoutContentRenderer();
		}
 
		IShellSearchResultsRenderer IShellContext.CreateShellSearchResultsRenderer()
		{
			return CreateShellSearchResultsRenderer();
		}
 
		IShellSectionRenderer IShellContext.CreateShellSectionRenderer(ShellSection shellSection)
		{
			return CreateShellSectionRenderer(shellSection);
		}
 
		IShellTabBarAppearanceTracker IShellContext.CreateTabBarAppearanceTracker()
		{
			return CreateTabBarAppearanceTracker();
		}
 
		#endregion IShellContext
 
		IShellItemRenderer _currentShellItemRenderer;
		bool _disposed;
		IShellFlyoutRenderer _flyoutRenderer;
		Task _activeTransition = Task.CompletedTask;
		IShellItemRenderer _incomingRenderer;
		IMauiContext _mauiContext;
 
		IShellFlyoutRenderer FlyoutRenderer
		{
			get
			{
				if (_flyoutRenderer == null)
				{
					FlyoutRenderer = CreateFlyoutRenderer();
					FlyoutRenderer.AttachFlyout(this, this);
				}
				return _flyoutRenderer;
			}
			set { _flyoutRenderer = value; }
		}
 
		public event EventHandler<VisualElementChangedEventArgs> ElementChanged;
 
		public VisualElement Element { get; private set; }
		public UIView NativeView => FlyoutRenderer.View;
		public Shell Shell => (Shell)Element;
		public UIViewController ViewController => FlyoutRenderer.ViewController;
 
		public void SetElement(VisualElement element)
		{
			if (Element != null)
				throw new NotSupportedException("Reuse of the Shell Renderer is not supported");
			Element = element;
			OnElementSet((Shell)Element);
 
			ElementChanged?.Invoke(this, new VisualElementChangedEventArgs(null, Element));
			Mapper.UpdateProperties(this, Element);
		}
 
		public virtual void SetElementSize(Size size)
		{
			Element.Layout(new Rect(Element.X, Element.Y, size.Width, size.Height));
		}
 
		public override void ViewDidLayoutSubviews()
		{
			base.ViewDidLayoutSubviews();
			if (_currentShellItemRenderer != null)
				_currentShellItemRenderer.ViewController.View.Frame = View.Bounds;
 
			SetElementSize(new Size(View.Bounds.Width, View.Bounds.Height));
		}
 
		public override void ViewDidLoad()
		{
			base.ViewDidLoad();
 
			SetupCurrentShellItem();
		}
 
		protected virtual IShellFlyoutRenderer CreateFlyoutRenderer()
		{
			return new ShellFlyoutRenderer()
			{
				FlyoutTransition = new SlideFlyoutTransition()
			};
		}
 
		protected virtual IShellNavBarAppearanceTracker CreateNavBarAppearanceTracker()
		{
			return new SafeShellNavBarAppearanceTracker();
		}
 
		protected virtual IShellPageRendererTracker CreatePageRendererTracker()
		{
			return new ShellPageRendererTracker(this);
		}
 
		protected virtual IShellFlyoutContentRenderer CreateShellFlyoutContentRenderer()
		{
			return new ShellFlyoutContentRenderer(this);
		}
 
		protected virtual IShellItemRenderer CreateShellItemRenderer(ShellItem item)
		{
			return new ShellItemRenderer(this)
			{
				ShellItem = item
			};
		}
 
		protected virtual IShellItemTransition CreateShellItemTransition()
		{
			return new ShellItemTransition();
		}
 
		protected virtual IShellSearchResultsRenderer CreateShellSearchResultsRenderer()
		{
			return new ShellSearchResultsRenderer(this);
		}
 
		protected virtual IShellSectionRenderer CreateShellSectionRenderer(ShellSection shellSection)
		{
			return new ShellSectionRenderer(this);
		}
 
		protected virtual IShellTabBarAppearanceTracker CreateTabBarAppearanceTracker()
		{
			return new ShellTabBarAppearanceTracker();
		}
 
		protected override void Dispose(bool disposing)
		{
			base.Dispose(disposing);
 
			if (disposing && !_disposed)
			{
				_disposed = true;
				FlyoutRenderer?.Dispose();
			}
 
			FlyoutRenderer = null;
		}
 
		protected virtual async void OnCurrentItemChanged()
		{
			try
			{
				await OnCurrentItemChangedAsync();
			}
			catch (Exception exc)
			{
				_mauiContext?.CreateLogger<ShellRenderer>()?.LogWarning(exc, "Failed on changing current item");
			}
		}
 
		protected virtual async Task OnCurrentItemChangedAsync()
		{
			var currentItem = Shell.CurrentItem;
 
			var oldLayer = _currentShellItemRenderer
				?.ViewController
				?.View
				?.Layer;
 
			if (oldLayer?.AnimationKeys?.Length > 0)
				oldLayer.RemoveAllAnimations();
 
			await _activeTransition;
			if (_currentShellItemRenderer?.ShellItem != currentItem)
			{
				var newController = CreateShellItemRenderer(currentItem);
				await SetCurrentShellItemControllerAsync(newController);
			}
		}
 
		protected virtual void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if (e.PropertyName == Shell.CurrentItemProperty.PropertyName)
			{
				OnCurrentItemChanged();
			}
			else if (e.PropertyName == VisualElement.FlowDirectionProperty.PropertyName)
			{
				UpdateFlowDirection(true);
			}
		}
 
		void UpdateFlowDirection(bool readdViews = false)
		{
			if (_currentShellItemRenderer?.ViewController == null)
				return;
 
			var originalValue = _currentShellItemRenderer.ViewController.View.SemanticContentAttribute;
			var originalViewValue = View.SemanticContentAttribute;
 
			_currentShellItemRenderer.ViewController.View.UpdateFlowDirection(Element);
			View.UpdateFlowDirection(Element);
 
			bool update = originalValue == _currentShellItemRenderer.ViewController.View.SemanticContentAttribute ||
				originalViewValue == View.SemanticContentAttribute;
 
			if (update && readdViews)
			{
				_currentShellItemRenderer.ViewController.View.RemoveFromSuperview();
				View.AddSubview(_currentShellItemRenderer.ViewController.View);
				View.SendSubviewToBack(_currentShellItemRenderer.ViewController.View);
			}
		}
 
		protected virtual void OnElementSet(Shell element)
		{
			if (element == null)
				return;
 
			element.PropertyChanged += OnElementPropertyChanged;
		}
 
		protected async void SetCurrentShellItemController(IShellItemRenderer value)
		{
			try
			{
				await SetCurrentShellItemControllerAsync(value);
			}
			catch (Exception exc)
			{
				_mauiContext?.CreateLogger<ShellRenderer>()?.LogWarning(exc, "Failed to SetCurrentShellItemController");
			}
		}
 
		protected async Task SetCurrentShellItemControllerAsync(IShellItemRenderer value)
		{
			_incomingRenderer = value;
			await _activeTransition;
 
			// This means the selected item changed while the active transition
			// was finishing up
			if (_incomingRenderer != value ||
				value.ShellItem != this.Shell.CurrentItem)
			{
				(value as IDisconnectable)?.Disconnect();
				value?.Dispose();
				return;
			}
 
			var oldRenderer = _currentShellItemRenderer;
			(oldRenderer as IDisconnectable)?.Disconnect();
			var newRenderer = value;
 
			_currentShellItemRenderer = value;
 
			AddChildViewController(newRenderer.ViewController);
			View.AddSubview(newRenderer.ViewController.View);
			View.SendSubviewToBack(newRenderer.ViewController.View);
 
			newRenderer.ViewController.View.Frame = View.Bounds;
 
			if (oldRenderer != null)
			{
				var transition = CreateShellItemTransition();
 
				_activeTransition = transition.Transition(oldRenderer, newRenderer);
				await _activeTransition;
 
				oldRenderer.ViewController.RemoveFromParentViewController();
				oldRenderer.ViewController.View.RemoveFromSuperview();
				oldRenderer.Dispose();
			}
			else
			{
				View.AddSubview(newRenderer.ViewController.View);
			}
 
			// current renderer is still valid
			if (_currentShellItemRenderer == value)
			{
				UpdateBackgroundColor();
				UpdateFlowDirection();
			}
		}
 
		protected virtual void UpdateBackgroundColor()
		{
			var color = Shell.BackgroundColor?.ToPlatform();
			if (color == null)
				color = Microsoft.Maui.Platform.ColorExtensions.BackgroundColor;
 
			FlyoutRenderer.View.BackgroundColor = color;
		}
 
		void SetupCurrentShellItem()
		{
			if (Shell.CurrentItem == null)
			{
				return;
			}
			else if (_currentShellItemRenderer == null)
			{
				OnCurrentItemChanged();
			}
		}
 
		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;
 
		Size IViewHandler.GetDesiredSize(double widthConstraint, double heightConstraint) => new Size(100, 100);
 
		void IViewHandler.PlatformArrange(Rect rect)
		{
			//TODO I don't think we need this
		}
 
		void IElementHandler.SetMauiContext(IMauiContext mauiContext)
		{
			_mauiContext = mauiContext;
		}
 
		void IElementHandler.SetVirtualView(Maui.IElement view)
		{
			SetElement((VisualElement)view);
		}
 
		void IElementHandler.UpdateValue(string property)
		{
			Mapper.UpdateProperty(this, Element, property);
		}
 
		void IElementHandler.Invoke(string command, object args)
		{
			CommandMapper.Invoke(this, Element, command, args);
		}
 
		void IElementHandler.DisconnectHandler()
		{
		}
	}
}