File: Handlers\Items\iOS\StructuredItemsViewController.cs
Web Access
Project: src\src\Controls\src\Core\Controls.Core.csproj (Microsoft.Maui.Controls)
#nullable disable
using System;
using CoreGraphics;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Controls.Handlers.Items
{
	public class StructuredItemsViewController<TItemsView> : ItemsViewController<TItemsView>
		where TItemsView : StructuredItemsView
	{
		public const int HeaderTag = 111;
		public const int FooterTag = 222;
 
		bool _disposed;
 
		UIView _headerUIView;
		VisualElement _headerViewFormsElement;
 
		UIView _footerUIView;
		VisualElement _footerViewFormsElement;
 
		public StructuredItemsViewController(TItemsView structuredItemsView, ItemsViewLayout layout)
			: base(structuredItemsView, layout)
		{
		}
 
 
		internal override void Disconnect()
		{
			base.Disconnect();
 
			if (_headerViewFormsElement is not null)
			{
				_headerViewFormsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
			}
 
			if (_footerViewFormsElement is not null)
			{
				_footerViewFormsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
			}
 
			if (_headerUIView is MauiView hv)
			{
				hv.LayoutChanged -= HeaderViewLayoutChanged;
			}
 
			if (_footerUIView is MauiView fv)
			{
				fv.LayoutChanged -= FooterViewLayoutChanged;
			}
 
			_headerUIView = null;
			_headerViewFormsElement = null;
			_footerUIView = null;
			_footerViewFormsElement = null;
		}
 
		protected override void Dispose(bool disposing)
		{
			if (_disposed)
			{
				return;
			}
 
			_disposed = true;
 
			if (disposing)
			{
				Disconnect();
			}
 
			base.Dispose(disposing);
		}
 
		protected override bool IsHorizontal => (ItemsView?.ItemsLayout as ItemsLayout)?.Orientation == ItemsLayoutOrientation.Horizontal;
 
		protected override CGRect DetermineEmptyViewFrame()
		{
			nfloat headerHeight = 0;
			var headerView = CollectionView.ViewWithTag(HeaderTag);
 
			if (headerView is not null)
				headerHeight = headerView.Frame.Height;
 
			nfloat footerHeight = 0;
			var footerView = CollectionView.ViewWithTag(FooterTag);
 
			if (footerView is not null)
				footerHeight = footerView.Frame.Height;
 
			var emptyView = CollectionView.ViewWithTag(EmptyTag);
 
			if (emptyView is not null)
				return base.DetermineEmptyViewFrame();
 
			return new CGRect(CollectionView.Frame.X, CollectionView.Frame.Y, CollectionView.Frame.Width,
				Math.Abs(CollectionView.Frame.Height - (headerHeight + footerHeight)));
		}
 
		public override void ViewWillLayoutSubviews()
		{
			base.ViewWillLayoutSubviews();
 
			// This update is only relevant if you have a footer view because it's used to place the footer view
			// based on the ContentSize so we just update the positions if the ContentSize has changed
			if (_footerUIView != null)
			{
				var emptyView = CollectionView.ViewWithTag(EmptyTag);
 
				if (IsHorizontal)
				{
					if (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width ||
						_footerUIView.Frame.X < emptyView?.Frame.X)
						UpdateHeaderFooterPosition();
				}
				else
				{
					if (_footerUIView.Frame.Y != ItemsViewLayout.CollectionViewContentSize.Height ||
						_footerUIView.Frame.Y < (emptyView?.Frame.Y + emptyView?.Frame.Height))
						UpdateHeaderFooterPosition();
				}
			}
		}
 
		internal void UpdateFooterView()
		{
			if (_footerUIView is MauiView mvPrevious)
			{
				mvPrevious.LayoutChanged -= FooterViewLayoutChanged;
			}
 
			UpdateSubview(ItemsView?.Footer, ItemsView?.FooterTemplate, FooterTag,
				ref _footerUIView, ref _footerViewFormsElement);
			UpdateHeaderFooterPosition();
 
			if (_footerUIView is MauiView mv)
			{
				mv.LayoutChanged += FooterViewLayoutChanged;
			}
		}
 
		internal void UpdateHeaderView()
		{
			if (_headerUIView is MauiView mvPrevious)
			{
				mvPrevious.LayoutChanged -= HeaderViewLayoutChanged;
			}
 
			UpdateSubview(ItemsView?.Header, ItemsView?.HeaderTemplate, HeaderTag,
				ref _headerUIView, ref _headerViewFormsElement);
			UpdateHeaderFooterPosition();
 
			if (_headerUIView is MauiView mv)
			{
				mv.LayoutChanged += HeaderViewLayoutChanged;
			}
		}
 
 
		internal void UpdateSubview(object view, DataTemplate viewTemplate, nint viewTag, ref UIView uiView, ref VisualElement formsElement)
		{
			uiView?.RemoveFromSuperview();
 
			if (formsElement != null)
			{
				ItemsView.RemoveLogicalChild(formsElement);
				formsElement.MeasureInvalidated -= OnFormsElementMeasureInvalidated;
			}
 
			UpdateView(view, viewTemplate, ref uiView, ref formsElement);
 
			if (uiView != null)
			{
				uiView.Tag = viewTag;
				CollectionView.AddSubview(uiView);
			}
 
			if (formsElement != null)
			{
				ItemsView.AddLogicalChild(formsElement);
			}
 
			if (formsElement != null)
			{
				RemeasureLayout(formsElement);
				formsElement.MeasureInvalidated += OnFormsElementMeasureInvalidated;
			}
			else
			{
				uiView?.SizeToFit();
			}
		}
 
		void UpdateHeaderFooterPosition()
		{
			var emptyView = CollectionView.ViewWithTag(EmptyTag);
 
			if (IsHorizontal)
			{
				var currentInset = CollectionView.ContentInset;
 
				nfloat headerWidth = _headerUIView?.Frame.Width ?? 0f;
				nfloat footerWidth = _footerUIView?.Frame.Width ?? 0f;
				nfloat emptyWidth = emptyView?.Frame.Width ?? 0f;
 
				if (_headerUIView != null && _headerUIView.Frame.X != headerWidth)
				{
					_headerUIView.Frame = new CoreGraphics.CGRect(-headerWidth, 0, headerWidth, CollectionView.Frame.Height);
				}
 
				if (_footerUIView != null && (_footerUIView.Frame.X != ItemsViewLayout.CollectionViewContentSize.Width || emptyWidth > 0))
					_footerUIView.Frame = new CoreGraphics.CGRect(ItemsViewLayout.CollectionViewContentSize.Width + emptyWidth, 0, footerWidth, CollectionView.Frame.Height);
 
				if (CollectionView.ContentInset.Left != headerWidth || CollectionView.ContentInset.Right != footerWidth)
				{
					var currentOffset = CollectionView.ContentOffset;
					CollectionView.ContentInset = new UIEdgeInsets(0, headerWidth, 0, footerWidth);
 
					var xOffset = currentOffset.X + (currentInset.Left - CollectionView.ContentInset.Left);
 
					if (CollectionView.ContentSize.Width + headerWidth <= CollectionView.Bounds.Width)
						xOffset = -headerWidth;
 
					// if the header grows it will scroll off the screen because if you change the content inset iOS adjusts the content offset so the list doesn't move
					// this changes the offset of the list by however much the header size has changed
					CollectionView.ContentOffset = new CoreGraphics.CGPoint(xOffset, CollectionView.ContentOffset.Y);
				}
			}
			else
			{
				var currentInset = CollectionView.ContentInset;
				nfloat headerHeight = _headerUIView?.Frame.Height ?? 0f;
				nfloat footerHeight = _footerUIView?.Frame.Height ?? 0f;
				nfloat emptyHeight = emptyView?.Frame.Height ?? 0f;
 
				if (CollectionView.ContentInset.Top != headerHeight || CollectionView.ContentInset.Bottom != footerHeight)
				{
					var currentOffset = CollectionView.ContentOffset;
					CollectionView.ContentInset = new UIEdgeInsets(headerHeight, 0, footerHeight, 0);
 
					// if the header grows it will scroll off the screen because if you change the content inset iOS adjusts the content offset so the list doesn't move
					// this changes the offset of the list by however much the header size has changed
 
					var yOffset = currentOffset.Y + (currentInset.Top - CollectionView.ContentInset.Top);
 
					if (CollectionView.ContentSize.Height + headerHeight <= CollectionView.Bounds.Height)
						yOffset = -headerHeight;
 
					CollectionView.ContentOffset = new CoreGraphics.CGPoint(CollectionView.ContentOffset.X, yOffset);
				}
 
				if (_headerUIView != null && _headerUIView.Frame.Y != headerHeight)
				{
					_headerUIView.Frame = new CoreGraphics.CGRect(0, -headerHeight, CollectionView.Frame.Width, headerHeight);
				}
 
				nfloat height = 0;
 
				if (IsViewLoaded && View.Window != null)
				{
					height = ItemsViewLayout.CollectionViewContentSize.Height;
				}
 
				if (_footerUIView != null && (_footerUIView.Frame.Y != height || emptyHeight > 0))
				{
					_footerUIView.Frame = new CoreGraphics.CGRect(0, height + emptyHeight, CollectionView.Frame.Width, footerHeight);
				}
			}
		}
 
		protected override void HandleFormsElementMeasureInvalidated(VisualElement formsElement)
		{
			base.HandleFormsElementMeasureInvalidated(formsElement);
			UpdateHeaderFooterPosition();
		}
 
		internal void UpdateLayoutMeasurements()
		{
			if (_headerViewFormsElement != null)
				HandleFormsElementMeasureInvalidated(_headerViewFormsElement);
 
			if (_footerViewFormsElement != null)
				HandleFormsElementMeasureInvalidated(_footerViewFormsElement);
		}
 
		void HeaderViewLayoutChanged(object sender, EventArgs e)
		{
			HandleFormsElementMeasureInvalidated(_headerViewFormsElement);
		}
 
		void FooterViewLayoutChanged(object sender, EventArgs e)
		{
			HandleFormsElementMeasureInvalidated(_footerViewFormsElement);
		}
	}
}