File: iOS\CollectionView\ObservableGroupedSource.cs
Web Access
Project: src\src\Compatibility\Core\src\Compatibility.csproj (Microsoft.Maui.Controls.Compatibility)
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using Foundation;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
{
	internal class ObservableGroupedSource : IItemsViewSource
	{
		readonly UICollectionView _collectionView;
		readonly UICollectionViewController _collectionViewController;
		readonly IList _groupSource;
		bool _disposed;
		List<ObservableItemsSource> _groups = new List<ObservableItemsSource>();
 
		public ObservableGroupedSource(IEnumerable groupSource, UICollectionViewController collectionViewController)
		{
			_collectionViewController = collectionViewController;
			_collectionView = _collectionViewController.CollectionView;
			_groupSource = groupSource as IList ?? new ListSource(groupSource);
 
			if (_groupSource is INotifyCollectionChanged incc)
			{
				incc.CollectionChanged += CollectionChanged;
			}
 
			ResetGroupTracking();
		}
 
		public object this[NSIndexPath indexPath]
		{
			get
			{
				return GetGroupItemAt(indexPath.Section, (int)indexPath.Item);
			}
		}
 
		public int GroupCount => _groupSource.Count;
 
		public int ItemCount
		{
			get
			{
				var total = 0;
 
				for (int n = 0; n < _groupSource.Count; n++)
				{
					total += GetGroupCount(n);
				}
 
				return total;
			}
		}
 
		public NSIndexPath GetIndexForItem(object item)
		{
			for (int i = 0; i < _groupSource.Count; i++)
			{
				var j = IndexInGroup(item, _groupSource[i]);
 
				if (j == -1)
				{
					continue;
				}
 
				return NSIndexPath.Create(i, j);
			}
 
			return NSIndexPath.Create(-1, -1);
		}
 
		public object Group(NSIndexPath indexPath)
		{
			return _groupSource[indexPath.Section];
		}
 
		public int ItemCountInGroup(nint group)
		{
			return GetGroupCount((int)group);
		}
 
		public void Dispose()
		{
			Dispose(true);
		}
 
		protected virtual void Dispose(bool disposing)
		{
			if (_disposed)
			{
				return;
			}
 
			_disposed = true;
 
			if (disposing)
			{
				ClearGroupTracking();
				if (_groupSource is INotifyCollectionChanged incc)
				{
					incc.CollectionChanged -= CollectionChanged;
				}
			}
		}
 
		void ClearGroupTracking()
		{
			for (int n = _groups.Count - 1; n >= 0; n--)
			{
				_groups[n].Dispose();
				_groups.RemoveAt(n);
			}
		}
 
		void ResetGroupTracking()
		{
			ClearGroupTracking();
 
			for (int n = 0; n < _groupSource.Count; n++)
			{
				if (_groupSource[n] is INotifyCollectionChanged && _groupSource[n] is IEnumerable list)
				{
					_groups.Add(new ObservableItemsSource(list, _collectionViewController, n));
				}
			}
		}
 
		void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
		{
			_collectionView.BeginInvokeOnMainThread(() => CollectionChanged(args));
		}
 
		void CollectionChanged(NotifyCollectionChangedEventArgs args)
		{
			switch (args.Action)
			{
				case NotifyCollectionChangedAction.Add:
					Add(args);
					break;
				case NotifyCollectionChangedAction.Remove:
					Remove(args);
					break;
				case NotifyCollectionChangedAction.Replace:
					Replace(args);
					break;
				case NotifyCollectionChangedAction.Move:
					Move(args);
					break;
				case NotifyCollectionChangedAction.Reset:
					Reload();
					break;
				default:
					throw new ArgumentOutOfRangeException();
			}
		}
 
		void Reload()
		{
			ResetGroupTracking();
 
			_collectionView.ReloadData();
			_collectionView.CollectionViewLayout.InvalidateLayout();
		}
 
		NSIndexSet CreateIndexSetFrom(int startIndex, int count)
		{
			return NSIndexSet.FromNSRange(new NSRange(startIndex, count));
		}
 
		bool NotLoadedYet()
		{
			// If the UICollectionView hasn't actually been loaded, then calling InsertSections or DeleteSections is 
			// going to crash or get in an unusable state; instead, ReloadData should be used
			return !_collectionViewController.IsViewLoaded || _collectionViewController.View.Window == null;
		}
 
		void Add(NotifyCollectionChangedEventArgs args)
		{
			if (ReloadRequired())
			{
				Reload();
				return;
			}
 
			var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
			var count = args.NewItems.Count;
 
			// Adding a group will change the section index for all subsequent groups, so the easiest thing to do
			// is to reset all the group tracking to get it up-to-date
			ResetGroupTracking();
 
			// Queue up the updates to the UICollectionView
			Update(() => _collectionView.InsertSections(CreateIndexSetFrom(startIndex, count)));
		}
 
		void Remove(NotifyCollectionChangedEventArgs args)
		{
			var startIndex = args.OldStartingIndex;
 
			if (startIndex < 0)
			{
				// INCC implementation isn't giving us enough information to know where the removed items were in the
				// collection. So the best we can do is a complete reload
				Reload();
				return;
			}
 
			if (ReloadRequired())
			{
				Reload();
				return;
			}
 
			// Removing a group will change the section index for all subsequent groups, so the easiest thing to do
			// is to reset all the group tracking to get it up-to-date
			ResetGroupTracking();
 
			// Since we have a start index, we can be more clever about removing the item(s) (and get the nifty animations)
			var count = args.OldItems.Count;
 
			// Queue up the updates to the UICollectionView
			Update(() => _collectionView.DeleteSections(CreateIndexSetFrom(startIndex, count)));
		}
 
		void Replace(NotifyCollectionChangedEventArgs args)
		{
			var newCount = args.NewItems.Count;
 
			if (newCount == args.OldItems.Count)
			{
				ResetGroupTracking();
 
				var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _groupSource.IndexOf(args.NewItems[0]);
 
				// We are replacing one set of items with a set of equal size; we can do a simple item range update
				Update(() => _collectionView.ReloadSections(CreateIndexSetFrom(startIndex, newCount)));
				return;
			}
 
			// The original and replacement sets are of unequal size; this means that everything currently in view will 
			// have to be updated. So we just have to use ReloadData and let the UICollectionView update everything
			Reload();
		}
 
		void Move(NotifyCollectionChangedEventArgs args)
		{
			var count = args.NewItems.Count;
 
			ResetGroupTracking();
 
			if (count == 1)
			{
				// For a single item, we can use MoveSection and get the animation
				Update(() => _collectionView.MoveSection(args.OldStartingIndex, args.NewStartingIndex));
				return;
			}
 
			var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
			var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
 
			Update(() => _collectionView.ReloadSections(CreateIndexSetFrom(start, end)));
		}
 
		int GetGroupCount(int groupIndex)
		{
			switch (_groupSource[groupIndex])
			{
				case IList list:
					return list.Count;
				case IEnumerable enumerable:
					var count = 0;
					var enumerator = enumerable.GetEnumerator();
					while (enumerator.MoveNext())
					{
						count += 1;
					}
					return count;
			}
 
			return 0;
		}
 
		object GetGroupItemAt(int groupIndex, int index)
		{
			switch (_groupSource[groupIndex])
			{
				case IList list:
					return list[index];
				case IEnumerable enumerable:
					var count = -1;
					var enumerator = enumerable.GetEnumerator();
 
					do
					{
						enumerator.MoveNext();
						count += 1;
					}
					while (count < index);
 
					return enumerator.Current;
			}
 
			return null;
		}
 
		int IndexInGroup(object item, object group)
		{
			switch (group)
			{
				case IList list:
					return list.IndexOf(item);
				case IEnumerable enumerable:
					var enumerator = enumerable.GetEnumerator();
					var index = 0;
					while (enumerator.MoveNext())
					{
						if (enumerator.Current == item)
						{
							return index;
						}
					}
					return -1;
			}
 
			return -1;
		}
 
		bool ReloadRequired()
		{
			// If the UICollectionView has never been loaded, or doesn't yet have any sections, any insert/delete operations 
			// are gonna crash hard. We'll need to reload the data instead.
 
			return NotLoadedYet()
				|| _collectionView.NumberOfSections() == 0;
		}
 
		void Update(Action update)
		{
			if (_collectionView.Hidden)
			{
				return;
			}
 
			update();
		}
	}
}