File: Handlers\Items\iOS\ItemsViewController.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 CoreGraphics;
using Foundation;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using UIKit;
 
namespace Microsoft.Maui.Controls.Handlers.Items
{
	public abstract class ItemsViewController<TItemsView> : UICollectionViewController, MauiCollectionView.ICustomMauiCollectionViewDelegate
	where TItemsView : ItemsView
	{
		public const int EmptyTag = 333;
		readonly WeakReference<TItemsView> _itemsView;
 
		public 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 ItemsViewController (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 ItemsViewLayout ItemsViewLayout { get; set; }
 
		bool _initialized;
		bool _isEmpty = true;
		bool _emptyViewDisplayed;
		bool _disposed;
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")]
		Func<UICollectionViewCell> _getPrototype;
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")]
		Func<NSIndexPath, UICollectionViewCell> _getPrototypeForIndexPath;
		CGSize _previousContentSize = CGSize.Empty;
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")]
		UIView _emptyUIView;
		VisualElement _emptyViewFormsElement;
		Dictionary<object, TemplatedCell> _measurementCells = new Dictionary<object, TemplatedCell>();
		List<string> _cellReuseIds = new List<string>();
 
		[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Proven safe in test: MemoryTests.HandlerDoesNotLeak")]
		protected UICollectionViewDelegateFlowLayout Delegator { get; set; }
 
		protected ItemsViewController(TItemsView itemsView, ItemsViewLayout layout) : base(layout)
		{
			_itemsView = new(itemsView);
			ItemsViewLayout = layout;
		}
 
		public void UpdateLayout(ItemsViewLayout newLayout)
		{
			// Ignore calls to this method if the new layout is the same as the old one
			if (CollectionView.CollectionViewLayout == newLayout)
				return;
 
			ItemsViewLayout = newLayout;
			_initialized = false;
 
			EnsureLayoutInitialized();
 
			if (_initialized)
			{
				// Reload the data so the currently visible cells get arranged according to the new layout
				CollectionView.ReloadData();
			}
		}
 
		internal virtual void Disconnect()
		{
			DisposeItemsSource();
		}
 
		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;
 
			switch (cell)
			{
				case DefaultCell defaultCell:
					UpdateDefaultCell(defaultCell, indexPath);
					break;
				case TemplatedCell templatedCell:
					UpdateTemplatedCell(templatedCell, indexPath);
					break;
			}
 
			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 (_isEmpty)
			{
				_measurementCells?.Clear();
				ItemsViewLayout?.ClearCellSizeCache();
			}
 
			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
				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 MauiCollectionView(CGRect.Empty, ItemsViewLayout);
			collectionView.SetCustomDelegate(this);
			CollectionView = collectionView;
		}
 
		public override void ViewWillAppear(bool animated)
		{
			base.ViewWillAppear(animated);
			ConstrainItemsToBounds();
		}
 
		public override void ViewWillLayoutSubviews()
		{
			ConstrainItemsToBounds();
			base.ViewWillLayoutSubviews();
			InvalidateMeasureIfContentSizeChanged();
			LayoutEmptyView();
		}
 
 
 
		void MauiCollectionView.ICustomMauiCollectionViewDelegate.MovedToWindow(UIView view)
		{
			if (CollectionView?.Window != null)
			{
				AttachingToWindow();
			}
			else
			{
				DetachingFromWindow();
			}
		}
 
		void InvalidateMeasureIfContentSizeChanged()
		{
			var contentSize = CollectionView?.CollectionViewLayout?.CollectionViewContentSize;
 
			if (!contentSize.HasValue)
			{
				return;
			}
 
			bool widthChanged = _previousContentSize.Width != contentSize.Value.Width;
			bool heightChanged = _previousContentSize.Height != contentSize.Value.Height;
 
			if (_initialized && (widthChanged || heightChanged))
			{
				var screenFrame = CollectionView?.Window?.Frame;
 
				if (!screenFrame.HasValue)
				{
					return;
				}
 
				var screenWidth = screenFrame.Value.Width;
				var screenHeight = screenFrame.Value.Height;
				bool invalidate = false;
 
				// If both the previous content size and the current content size are larger
				// than the screen size, then we know that we're already maxed out and the 
				// CollectionView items are scrollable. There's no reason to force an invalidation
				// of the CollectionView to expand/contract it.
 
				// If either size is smaller than that, we need to invalidate to ensure that the 
				// CollectionView is re-measured and set to the correct size.
 
				if (widthChanged && (contentSize.Value.Width < screenWidth || _previousContentSize.Width < screenWidth))
				{
					invalidate = true;
				}
				else if (heightChanged && (contentSize.Value.Height < screenHeight || _previousContentSize.Height < screenHeight))
				{
					invalidate = true;
				}
 
				if (invalidate)
				{
					ItemsView.InvalidateMeasureInternal(InvalidationTrigger.MeasureChanged);
				}
			}
 
			_previousContentSize = contentSize.Value;
		}
 
 
		internal Size? GetSize()
		{
			if (_emptyViewDisplayed)
			{
				return _emptyUIView.Frame.Size.ToSize();
			}
 
			return CollectionView.CollectionViewLayout.CollectionViewContentSize.ToSize();
		}
 
		void ConstrainItemsToBounds()
		{
			var contentBounds = CollectionView.AdjustedContentInset.InsetRect(CollectionView.Bounds);
			var constrainedSize = contentBounds.Size;
			ItemsViewLayout.UpdateConstraints(constrainedSize);
		}
 
		void EnsureLayoutInitialized()
		{
			if (_initialized)
			{
				return;
			}
 
			_initialized = true;
 
			_getPrototype ??= GetPrototype;
			ItemsViewLayout.GetPrototype = _getPrototype;
 
			_getPrototypeForIndexPath ??= GetPrototypeForIndexPath;
			ItemsViewLayout.GetPrototypeForIndexPath = _getPrototypeForIndexPath;
 
			Delegator = CreateDelegator();
			CollectionView.Delegate = Delegator;
 
			ItemsViewLayout.SetInitialConstraints(CollectionView.Bounds.Size);
			CollectionView.SetCollectionViewLayout(ItemsViewLayout, false);
 
			UpdateEmptyView();
		}
 
		protected virtual UICollectionViewDelegateFlowLayout CreateDelegator()
		{
			return new ItemsViewDelegator<TItemsView, ItemsViewController<TItemsView>>(ItemsViewLayout, this);
		}
 
		protected virtual IItemsViewSource CreateItemsViewSource()
		{
			return ItemsSourceFactory.Create(ItemsView.ItemsSource, this);
		}
 
		public virtual void UpdateItemsSource()
		{
			_measurementCells?.Clear();
			ItemsViewLayout?.ClearCellSizeCache();
			ItemsSource?.Dispose();
			ItemsSource = CreateItemsViewSource();
 
			CollectionView.ReloadData();
			CollectionView.CollectionViewLayout.InvalidateLayout();
 
			(ItemsView as IView)?.InvalidateMeasure();
		}
 
		internal void DisposeItemsSource()
		{
			_measurementCells?.Clear();
			ItemsViewLayout?.ClearCellSizeCache();
			ItemsSource?.Dispose();
			ItemsSource = new EmptySource();
			CollectionView.ReloadData();
		}
 
		public virtual void UpdateFlowDirection()
		{
			CollectionView.UpdateFlowDirection(ItemsView);
 
			if (_emptyViewDisplayed)
			{
				AlignEmptyView();
			}
 
			Layout.InvalidateLayout();
		}
 
		public override nint NumberOfSections(UICollectionView collectionView)
		{
			CheckForEmptySource();
			return ItemsSource.GroupCount;
		}
 
		protected virtual void UpdateDefaultCell(DefaultCell cell, NSIndexPath indexPath)
		{
			cell.Label.Text = ItemsSource[indexPath].ToString();
 
			if (cell is ItemsViewCell constrainedCell)
			{
				ItemsViewLayout.PrepareCellForLayout(constrainedCell);
			}
		}
 
		protected virtual void UpdateTemplatedCell(TemplatedCell cell, NSIndexPath indexPath)
		{
			cell.ContentSizeChanged -= CellContentSizeChanged;
			cell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
 
			var bindingContext = ItemsSource[indexPath];
 
			// If we've already created a cell for this index path (for measurement), re-use the content
			if (_measurementCells != null && _measurementCells.TryGetValue(bindingContext, out TemplatedCell measurementCell))
			{
				_measurementCells.Remove(bindingContext);
				measurementCell.ContentSizeChanged -= CellContentSizeChanged;
				measurementCell.LayoutAttributesChanged -= CellLayoutAttributesChanged;
				cell.UseContent(measurementCell);
			}
			else
			{
				cell.Bind(ItemsView.ItemTemplate, ItemsSource[indexPath], ItemsView);
			}
 
			cell.ContentSizeChanged += CellContentSizeChanged;
			cell.LayoutAttributesChanged += CellLayoutAttributesChanged;
 
			ItemsViewLayout.PrepareCellForLayout(cell);
		}
 
		public virtual NSIndexPath GetIndexForItem(object item)
		{
			return ItemsSource.GetIndexForItem(item);
		}
 
		protected object GetItemAtIndex(NSIndexPath index)
		{
			return ItemsSource[index];
		}
 
		[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Proven safe in test: CollectionViewTests.ItemsSourceDoesNotLeak")]
		void CellContentSizeChanged(object sender, EventArgs e)
		{
			if (_disposed)
				return;
 
			if (!(sender is TemplatedCell cell))
			{
				return;
			}
 
			var visibleCells = CollectionView.VisibleCells;
 
			for (int n = 0; n < visibleCells.Length; n++)
			{
				if (cell == visibleCells[n])
				{
					ItemsViewLayout?.InvalidateLayout();
					return;
				}
			}
		}
 
		[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Proven safe in test: CollectionViewTests.ItemsSourceDoesNotLeak")]
		void CellLayoutAttributesChanged(object sender, LayoutAttributesChangedEventArgs args)
		{
			CacheCellAttributes(args.NewAttributes.IndexPath, args.NewAttributes.Size);
		}
 
		protected virtual void CacheCellAttributes(NSIndexPath indexPath, CGSize size)
		{
			if (!ItemsSource.IsIndexPathValid(indexPath))
			{
				// The upate might be coming from a cell that's being removed; don't cache it.
				return;
			}
 
			var item = ItemsSource[indexPath];
			if (item != null)
			{
				ItemsViewLayout.CacheCellSize(item, size);
			}
		}
 
		protected virtual string DetermineCellReuseId(NSIndexPath indexPath)
		{
			if (ItemsView.ItemTemplate != null)
			{
				var item = ItemsSource[indexPath];
 
				var dataTemplate = ItemsView.ItemTemplate.SelectDataTemplate(item, ItemsView);
 
				var cellOrientation = ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Vertical ? "v" : "h";
				var cellType = ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Vertical ? typeof(VerticalCell) : typeof(HorizontalCell);
 
				var reuseId = $"_maui_{cellOrientation}_{dataTemplate.Id}";
 
				if (!_cellReuseIds.Contains(reuseId))
				{
					CollectionView.RegisterClassForCell(cellType, new NSString(reuseId));
					_cellReuseIds.Add(reuseId);
				}
 
				return reuseId;
			}
 
			return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
				? HorizontalDefaultCell.ReuseId
				: VerticalDefaultCell.ReuseId;
		}
 
		[Obsolete("Use DetermineCellReuseId(NSIndexPath indexPath) instead.")]
		protected virtual string DetermineCellReuseId()
		{
			if (ItemsView.ItemTemplate != null)
			{
				return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
					? HorizontalCell.ReuseId
					: VerticalCell.ReuseId;
			}
 
			return ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal
				? HorizontalDefaultCell.ReuseId
				: VerticalDefaultCell.ReuseId;
		}
 
		UICollectionViewCell GetPrototype()
		{
			if (ItemsSource == null || ItemsSource.ItemCount == 0)
			{
				return null;
			}
 
			var group = 0;
 
			if (ItemsSource.GroupCount > 1)
			{
				// If we're in a grouping situation, then we need to make sure we find an actual data item
				// to use for our prototype cell. It's possible that we have empty groups.
				for (int n = 0; n < ItemsSource.GroupCount; n++)
				{
					if (ItemsSource.ItemCountInGroup(n) > 0)
					{
						group = n;
						break;
					}
				}
			}
 
			var indexPath = NSIndexPath.Create(group, 0);
 
			return GetPrototypeForIndexPath(indexPath);
		}
 
		internal UICollectionViewCell GetPrototypeForIndexPath(NSIndexPath indexPath)
		{
			return CreateMeasurementCell(indexPath);
		}
 
		protected virtual void RegisterViewTypes()
		{
			CollectionView.RegisterClassForCell(typeof(HorizontalDefaultCell), HorizontalDefaultCell.ReuseId);
			CollectionView.RegisterClassForCell(typeof(VerticalDefaultCell), VerticalDefaultCell.ReuseId);
			CollectionView.RegisterClassForCell(typeof(HorizontalCell), HorizontalCell.ReuseId);
			CollectionView.RegisterClassForCell(typeof(VerticalCell), VerticalCell.ReuseId);
		}
 
		protected abstract bool IsHorizontal { get; }
 
		protected virtual CGRect DetermineEmptyViewFrame()
		{
			nfloat emptyViewHeight = CollectionView.Frame.Height;
 
			if (_emptyViewFormsElement is IView emptyView)
			{
				emptyViewHeight = (nfloat)emptyView.Measure(CollectionView.Frame.Width, double.PositiveInfinity).Height;
			}
			return new CGRect(CollectionView.Frame.X, CollectionView.Frame.Y, CollectionView.Frame.Width, emptyViewHeight);
		}
 
		protected void RemeasureLayout(VisualElement formsElement)
		{
			if (IsHorizontal)
			{
				var request = formsElement.Measure(double.PositiveInfinity, CollectionView.Frame.Height);
				formsElement.Arrange(new Rect(0, 0, request.Width, CollectionView.Frame.Height));
			}
			else
			{
				var request = formsElement.Measure(CollectionView.Frame.Width, double.PositiveInfinity);
				formsElement.Arrange(new Rect(0, 0, CollectionView.Frame.Width, request.Height));
			}
		}
 
		protected void OnFormsElementMeasureInvalidated(object sender, EventArgs e)
		{
			if (sender is VisualElement formsElement)
			{
				HandleFormsElementMeasureInvalidated(formsElement);
			}
		}
 
		protected virtual void HandleFormsElementMeasureInvalidated(VisualElement formsElement)
		{
			RemeasureLayout(formsElement);
		}
 
		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?.Dispose();
				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) = 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());
		}
 
		TemplatedCell CreateAppropriateCellForLayout()
		{
			var frame = new CGRect(0, 0, ItemsViewLayout.EstimatedItemSize.Width, ItemsViewLayout.EstimatedItemSize.Height);
 
			if (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal)
			{
				return new HorizontalCell(frame);
			}
 
			return new VerticalCell(frame);
		}
 
		public UICollectionViewCell CreateMeasurementCell(NSIndexPath indexPath)
		{
			if (ItemsView.ItemTemplate == null)
			{
				var frame = new CGRect(0, 0, ItemsViewLayout.EstimatedItemSize.Width, ItemsViewLayout.EstimatedItemSize.Height);
 
				DefaultCell cell;
				if (ItemsViewLayout.ScrollDirection == UICollectionViewScrollDirection.Horizontal)
				{
					cell = new HorizontalDefaultCell(frame);
				}
				else
				{
					cell = new VerticalDefaultCell(frame);
				}
 
				UpdateDefaultCell(cell, indexPath);
				return cell;
			}
 
			TemplatedCell templatedCell = CreateAppropriateCellForLayout();
 
			UpdateTemplatedCell(templatedCell, indexPath);
 
			// Keep this cell around, we can transfer the contents to the actual cell when the UICollectionView creates it
			if (_measurementCells != null)
				_measurementCells[ItemsSource[indexPath]] = templatedCell;
 
			return templatedCell;
		}
 
		internal CGSize GetSizeForItem(NSIndexPath indexPath)
		{
			if (ItemsViewLayout.EstimatedItemSize.IsEmpty)
			{
				return ItemsViewLayout.ItemSize;
			}
 
			if (ItemsSource.IsIndexPathValid(indexPath))
			{
				var item = ItemsSource[indexPath];
 
				if (item != null && ItemsViewLayout.TryGetCachedCellSize(item, out CGSize size))
				{
					return size;
				}
			}
 
			return ItemsViewLayout.EstimatedItemSize;
		}
 
		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 TemplatedCell templatedCell &&
				(templatedCell.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 ||
					!itemsSource.IsIndexPathValid(indexPath) ||
					!Equals(itemsSource[indexPath], bindingContext))
				{
					templatedCell.Unbind();
				}
			}
		}
	}
}