File: Handlers\Items2\iOS\ItemsViewController2.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.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using PassKit;
using UIKit;
 
namespace Microsoft.Maui.Controls.Handlers.Items2
{
	public abstract class ItemsViewController2<TItemsView> : UICollectionViewController, Items.MauiCollectionView.ICustomMauiCollectionViewDelegate
	where TItemsView : ItemsView
	{
		public const int EmptyTag = 333;
		readonly WeakReference<TItemsView> _itemsView;
 
		public Items.IItemsViewSource ItemsSource { get; protected set; }
		public TItemsView ItemsView => _itemsView.GetTargetOrDefault();
 
		// ItemsViewLayout provides an accessor to the typed UICollectionViewLayout. It's also important to note that the
		// initial UICollectionViewLayout which is passed in to the ItemsViewController2 (and accessed via the Layout property)
		// _does not_ get updated when the layout is updated for the CollectionView. That property only refers to the
		// original layout. So it's unlikely that you would ever want to use .Layout; use .ItemsViewLayout instead.
		// See https://developer.apple.com/documentation/uikit/uicollectionviewcontroller/1623980-collectionviewlayout
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")]
		protected UICollectionViewLayout ItemsViewLayout { get; set; }
 
		bool _initialized;
		bool _isEmpty = true;
		bool _emptyViewDisplayed;
		bool _disposed;
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")]
		UIView _emptyUIView;
		VisualElement _emptyViewFormsElement;
		List<string> _cellReuseIds = new List<string>();
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")]
		protected UICollectionViewDelegateFlowLayout Delegator { get; set; }
 
		protected UICollectionViewScrollDirection ScrollDirection { get; private set; } =
			UICollectionViewScrollDirection.Vertical;
 
		protected ItemsViewController2(TItemsView itemsView, UICollectionViewLayout layout) : base(layout)
		{
			_itemsView = new(itemsView);
			ItemsViewLayout = layout;
		}
 
		public void UpdateLayout(UICollectionViewLayout newLayout)
		{
			// Ignore calls to this method if the new layout is the same as the old one
			if (CollectionView.CollectionViewLayout == newLayout)
				return;
 
			if (newLayout is UICollectionViewCompositionalLayout compositionalLayout)
			{
				ScrollDirection = compositionalLayout.Configuration.ScrollDirection;
			}
 
			ItemsViewLayout = newLayout;
			_initialized = false;
 
			EnsureLayoutInitialized();
 
			if (_initialized)
			{
				// Reload the data so the currently visible cells get laid out according to the new layout
				CollectionView.ReloadData();
			}
		}
 
		protected override void Dispose(bool disposing)
		{
			if (_disposed)
				return;
 
			_disposed = true;
 
			if (disposing)
			{
				ItemsSource?.Dispose();
 
				CollectionView.Delegate = null;
				Delegator?.Dispose();
 
				_emptyUIView?.Dispose();
				_emptyUIView = null;
 
				_emptyViewFormsElement = null;
 
				ItemsViewLayout?.Dispose();
				CollectionView?.Dispose();
			}
 
			base.Dispose(disposing);
		}
 
		public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
		{
			var cell = collectionView.DequeueReusableCell(DetermineCellReuseId(indexPath), indexPath) as UICollectionViewCell;
 
			// We need to get the index path that is adjusted for the item source
			// Some ItemsView like CarouselView have a loop feature that will make the index path different from the item source
			var indexpathAdjusted = GetAdjustedIndexPathForItemSource(indexPath);
 
			if (cell is TemplatedCell2 TemplatedCell2)
			{
				TemplatedCell2.ScrollDirection = ScrollDirection;
 
				TemplatedCell2.Bind(ItemsView.ItemTemplate, ItemsSource[indexpathAdjusted], ItemsView);
			}
			else if (cell is DefaultCell2 DefaultCell2)
			{
				DefaultCell2.Label.Text = ItemsSource[indexpathAdjusted].ToString();
			}
 
			return cell;
		}
 
		public override nint GetItemsCount(UICollectionView collectionView, nint section)
		{
			CheckForEmptySource();
 
			return ItemsSource.ItemCountInGroup(section);
		}
 
		void CheckForEmptySource()
		{
			var wasEmpty = _isEmpty;
 
			_isEmpty = ItemsSource.ItemCount == 0;
 
			if (wasEmpty != _isEmpty)
			{
				UpdateEmptyViewVisibility(_isEmpty);
			}
 
			if (wasEmpty && !_isEmpty)
			{
				// If we're going from empty to having stuff, it's possible that we've never actually measured
				// a prototype cell and our itemSize or estimatedItemSize are wrong/unset
				// So trigger a constraint update; if we need a measurement, that will make it happen
				// TODO: Fix ItemsViewLayout.ConstrainTo(CollectionView.Bounds.Size);
			}
		}
 
		public override void ViewDidLoad()
		{
			base.ViewDidLoad();
 
			ItemsSource = CreateItemsViewSource();
 
			if (!(OperatingSystem.IsIOSVersionAtLeast(11) || OperatingSystem.IsMacCatalystVersionAtLeast(11)
#if TVOS
				|| OperatingSystem.IsTvOSVersionAtLeast(11)
#endif
			))
			{
				AutomaticallyAdjustsScrollViewInsets = false;
			}
			else
			{
				// We set this property to keep iOS from trying to be helpful about insetting all the 
				// CollectionView content when we're in landscape mode (to avoid the notch)
				// The SetUseSafeArea Platform Specific is already taking care of this for us 
				// That said, at some point it's possible folks will want a PS for controlling this behavior
				CollectionView.ContentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.Never;
			}
 
			RegisterViewTypes();
 
			EnsureLayoutInitialized();
		}
 
		public override void LoadView()
		{
			base.LoadView();
			var collectionView = new Items.MauiCollectionView(CGRect.Empty, ItemsViewLayout);
			collectionView.SetCustomDelegate(this);
			CollectionView = collectionView;
		}
 
		public override void ViewWillLayoutSubviews()
		{
			base.ViewWillLayoutSubviews();
			LayoutEmptyView();
		}
 
		void Items.MauiCollectionView.ICustomMauiCollectionViewDelegate.MovedToWindow(UIView view)
		{
			if (CollectionView?.Window != null)
			{
				AttachingToWindow();
			}
			else
			{
				DetachingFromWindow();
			}
		}
 
		internal void DisposeItemsSource()
		{
			ItemsSource?.Dispose();
			ItemsSource = new Items.EmptySource();
			CollectionView.ReloadData();
		}
 
		void EnsureLayoutInitialized()
		{
			if (_initialized)
			{
				return;
			}
 
			_initialized = true;
 
			Delegator = CreateDelegator();
			CollectionView.Delegate = Delegator;
 
			CollectionView.SetCollectionViewLayout(ItemsViewLayout, false);
 
			UpdateEmptyView();
		}
 
		protected virtual UICollectionViewDelegateFlowLayout CreateDelegator()
		{
			return new ItemsViewDelegator2<TItemsView, ItemsViewController2<TItemsView>>(ItemsViewLayout, this);
		}
 
		protected virtual Items.IItemsViewSource CreateItemsViewSource()
		{
			return Items.ItemsSourceFactory.Create(ItemsView.ItemsSource, this);
		}
 
		public virtual void UpdateItemsSource()
		{
			ItemsSource?.Dispose();
			ItemsSource = CreateItemsViewSource();
 
			CollectionView.ReloadData();
			CollectionView.CollectionViewLayout.InvalidateLayout();
 
			(ItemsView as IView)?.InvalidateMeasure();
		}
 
		public virtual void UpdateFlowDirection()
		{
			CollectionView.UpdateFlowDirection(ItemsView);
 
			if (_emptyViewDisplayed)
			{
				AlignEmptyView();
			}
 
			Layout.InvalidateLayout();
		}
 
		public override nint NumberOfSections(UICollectionView collectionView)
		{
			CheckForEmptySource();
			return ItemsSource.GroupCount;
		}
 
 
		public virtual NSIndexPath GetIndexForItem(object item)
		{
			return ItemsSource.GetIndexForItem(item);
		}
 
		protected object GetItemAtIndex(NSIndexPath index)
		{
			return ItemsSource[index];
		}
 
		protected virtual string DetermineCellReuseId(NSIndexPath indexPath)
		{
			if (ItemsView.ItemTemplate != null)
			{
				var item = ItemsSource[indexPath];
 
				var dataTemplate = ItemsView.ItemTemplate.SelectDataTemplate(item, ItemsView);
 
				var cellType = typeof(TemplatedCell2);
 
				var orientation = ScrollDirection == UICollectionViewScrollDirection.Horizontal ? "Horizontal" : "Vertical";
				var reuseId = $"{TemplatedCell2.ReuseId}.{orientation}.{dataTemplate.Id}";
 
				if (!_cellReuseIds.Contains(reuseId))
				{
					CollectionView.RegisterClassForCell(cellType, new NSString(reuseId));
					_cellReuseIds.Add(reuseId);
				}
 
				return reuseId;
			}
 
			return ScrollDirection == UICollectionViewScrollDirection.Horizontal ? HorizontalDefaultCell2.ReuseId : VerticalDefaultCell2.ReuseId;
		}
 
		protected virtual void RegisterViewTypes()
		{
			CollectionView.RegisterClassForCell(typeof(HorizontalDefaultCell2), HorizontalDefaultCell2.ReuseId);
			CollectionView.RegisterClassForCell(typeof(VerticalDefaultCell2), VerticalDefaultCell2.ReuseId);
			CollectionView.RegisterClassForCell(typeof(HorizontalCell2), HorizontalCell2.ReuseId);
			CollectionView.RegisterClassForCell(typeof(VerticalCell2), VerticalCell2.ReuseId);
		}
 
		protected abstract bool IsHorizontal { get; }
 
		protected virtual CGRect DetermineEmptyViewFrame()
		{
			return new CGRect(CollectionView.Frame.X, CollectionView.Frame.Y,
				CollectionView.Frame.Width, CollectionView.Frame.Height);
		}
 
 
		internal void UpdateView(object view, DataTemplate viewTemplate, ref UIView uiView, ref VisualElement formsElement)
		{
			// Is view set on the ItemsView?
			if (view is null && (viewTemplate is null || viewTemplate is DataTemplateSelector))
			{
				if (formsElement != null)
				{
					//Platform.GetRenderer(formsElement)?.DisposeRendererAndChildren();
				}
 
				uiView = null;
				formsElement?.Handler?.DisconnectHandler();
				formsElement = null;
			}
			else
			{
				// Create the native renderer for the view, and keep the actual Forms element (if any)
				// around for updating the layout later
				(uiView, formsElement) = Items.TemplateHelpers.RealizeView(view, viewTemplate, ItemsView);
			}
		}
 
		internal void UpdateEmptyView()
		{
			if (!_initialized)
			{
				return;
			}
 
			// Get rid of the old view
			TearDownEmptyView();
 
			// Set up the new empty view
			UpdateView(ItemsView?.EmptyView, ItemsView?.EmptyViewTemplate, ref _emptyUIView, ref _emptyViewFormsElement);
 
			// We may need to show the updated empty view
			UpdateEmptyViewVisibility(ItemsSource?.ItemCount == 0);
		}
 
		void UpdateEmptyViewVisibility(bool isEmpty)
		{
			if (!_initialized)
			{
				return;
			}
 
			if (isEmpty)
			{
				ShowEmptyView();
			}
			else
			{
				HideEmptyView();
			}
		}
 
		void AlignEmptyView()
		{
			if (_emptyUIView == null)
			{
				return;
			}
 
			bool isRtl;
 
			if (OperatingSystem.IsIOSVersionAtLeast(10) || OperatingSystem.IsTvOSVersionAtLeast(10))
				isRtl = CollectionView.EffectiveUserInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.RightToLeft;
			else
				isRtl = CollectionView.SemanticContentAttribute == UISemanticContentAttribute.ForceRightToLeft;
 
			if (isRtl)
			{
				if (_emptyUIView.Transform.A == -1)
				{
					return;
				}
 
				FlipEmptyView();
			}
			else
			{
				if (_emptyUIView.Transform.A == -1)
				{
					FlipEmptyView();
				}
			}
		}
 
		void FlipEmptyView()
		{
			// Flip the empty view 180 degrees around the X axis 
			_emptyUIView.Transform = CGAffineTransform.Scale(_emptyUIView.Transform, -1, 1);
		}
 
		void ShowEmptyView()
		{
			if (_emptyViewDisplayed || _emptyUIView == null)
			{
				return;
			}
 
			_emptyUIView.Tag = EmptyTag;
			CollectionView.AddSubview(_emptyUIView);
 
			if (((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) == -1)
			{
				ItemsView.AddLogicalChild(_emptyViewFormsElement);
			}
 
			LayoutEmptyView();
 
			AlignEmptyView();
			_emptyViewDisplayed = true;
		}
 
		void HideEmptyView()
		{
			if (!_emptyViewDisplayed || _emptyUIView == null)
			{
				return;
			}
 
			_emptyUIView.RemoveFromSuperview();
 
			_emptyViewDisplayed = false;
		}
 
		void TearDownEmptyView()
		{
			HideEmptyView();
 
			// RemoveLogicalChild will trigger a disposal of the native view and its content
			ItemsView.RemoveLogicalChild(_emptyViewFormsElement);
 
			_emptyUIView = null;
			_emptyViewFormsElement = null;
		}
 
		void LayoutEmptyView()
		{
			if (!_initialized || _emptyUIView == null || _emptyUIView.Superview == null)
			{
				return;
			}
 
			var frame = DetermineEmptyViewFrame();
 
			_emptyUIView.Frame = frame;
 
			if (_emptyViewFormsElement != null && ((IElementController)ItemsView).LogicalChildren.IndexOf(_emptyViewFormsElement) != -1)
				_emptyViewFormsElement.Layout(frame.ToRectangle());
		}
 
		internal protected virtual void UpdateVisibility()
		{
			if (ItemsView.IsVisible)
			{
				if (CollectionView.Hidden)
				{
					CollectionView.ReloadData();
					CollectionView.Hidden = false;
					Layout.InvalidateLayout();
					CollectionView.LayoutIfNeeded();
				}
			}
			else
			{
				CollectionView.Hidden = true;
			}
		}
 
		private protected virtual void AttachingToWindow()
		{
 
		}
 
		private protected virtual void DetachingFromWindow()
		{
		}
 
		private protected virtual NSIndexPath GetAdjustedIndexPathForItemSource(NSIndexPath indexPath)
		{
			return indexPath;
		}
 
		internal virtual void CellDisplayingEndedFromDelegate(UICollectionViewCell cell, NSIndexPath indexPath)
		{
			if (cell is TemplatedCell2 TemplatedCell2 &&
				(TemplatedCell2.PlatformHandler?.VirtualView as View)?.BindingContext is object bindingContext)
			{
				// We want to unbind a cell that is no longer present in the items source. Unfortunately
				// it's too expensive to check directly, so let's check that the current binding context
				// matches the item at a given position.
 
				indexPath = GetAdjustedIndexPathForItemSource(indexPath);
 
				var itemsSource = ItemsSource;
				if (itemsSource is null ||
					!Items.IndexPathHelpers.IsIndexPathValid(itemsSource, indexPath) ||
					!Equals(itemsSource[indexPath], bindingContext))
				{
					TemplatedCell2.Unbind();
				}
			}
		}
	}
}