File: iOS\CollectionView\ObservableItemsSource.cs
Web Access
Project: src\src\Compatibility\Core\src\Compatibility.csproj (Microsoft.Maui.Controls.Compatibility)
using System;
using System.Collections;
using System.Collections.Specialized;
using Foundation;
using ObjCRuntime;
using UIKit;
 
namespace Microsoft.Maui.Controls.Compatibility.Platform.iOS
{
	internal class ObservableItemsSource : IItemsViewSource
	{
		readonly UICollectionViewController _collectionViewController;
		protected readonly UICollectionView CollectionView;
		readonly bool _grouped;
		readonly int _section;
		readonly IEnumerable _itemsSource;
		bool _disposed;
 
		public ObservableItemsSource(IEnumerable itemSource, UICollectionViewController collectionViewController, int group = -1)
		{
			_collectionViewController = collectionViewController;
			CollectionView = _collectionViewController.CollectionView;
 
			_section = group < 0 ? 0 : group;
			_grouped = group >= 0;
 
			_itemsSource = itemSource;
 
			Count = ItemsCount();
 
			((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
		}
 
		internal event NotifyCollectionChangedEventHandler CollectionViewUpdating;
		internal event NotifyCollectionChangedEventHandler CollectionViewUpdated;
 
		public int Count { get; private set; }
 
		public int Section => _section;
 
		public object this[int index] => ElementAt(index);
 
		public void Dispose()
		{
			Dispose(true);
		}
 
		protected virtual void Dispose(bool disposing)
		{
			if (!_disposed)
			{
				if (disposing)
				{
					((INotifyCollectionChanged)_itemsSource).CollectionChanged -= CollectionChanged;
				}
 
				_disposed = true;
			}
		}
 
		public int ItemCountInGroup(nint group)
		{
			return Count;
		}
 
		public object Group(NSIndexPath indexPath)
		{
			return null;
		}
 
		public NSIndexPath GetIndexForItem(object item)
		{
			for (int n = 0; n < Count; n++)
			{
				if (this[n] == item)
				{
					return NSIndexPath.Create(_section, n);
				}
			}
 
			return NSIndexPath.Create(-1, -1);
		}
 
		public int GroupCount => 1;
 
		public int ItemCount => Count;
 
		public object this[NSIndexPath indexPath]
		{
			get
			{
				if (indexPath.Section != _section)
				{
					throw new ArgumentOutOfRangeException(nameof(indexPath));
				}
 
				return this[(int)indexPath.Item];
			}
		}
 
		void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
		{
			CollectionView.BeginInvokeOnMainThread(() => CollectionChanged(args));
		}
 
		void CollectionChanged(NotifyCollectionChangedEventArgs args)
		{
			// Force UICollectionView to get the internal accounting straight 
			if (!CollectionView.Hidden)
				CollectionView.NumberOfItemsInSection(_section);
 
			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()
		{
			var args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
 
			Count = ItemsCount();
 
			OnCollectionViewUpdating(args);
 
			CollectionView.ReloadData();
			CollectionView.CollectionViewLayout.InvalidateLayout();
 
			OnCollectionViewUpdated(args);
		}
 
		protected virtual NSIndexPath[] CreateIndexesFrom(int startIndex, int count)
		{
			return IndexPathHelpers.GenerateIndexPathRange(_section, startIndex, count);
		}
 
		void Add(NotifyCollectionChangedEventArgs args)
		{
			var count = args.NewItems.Count;
			Count += count;
			var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]);
 
			// Queue up the updates to the UICollectionView
			Update(() => CollectionView.InsertItems(CreateIndexesFrom(startIndex, count)), args);
		}
 
		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 ReloadData()
				Reload();
				return;
			}
 
			// If 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;
			Count -= count;
 
			Update(() => CollectionView.DeleteItems(CreateIndexesFrom(startIndex, count)), args);
		}
 
		void Replace(NotifyCollectionChangedEventArgs args)
		{
			var newCount = args.NewItems.Count;
 
			if (newCount == args.OldItems.Count)
			{
				var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.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.ReloadItems(CreateIndexesFrom(startIndex, newCount)), args);
				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;
 
			if (count == 1)
			{
				// For a single item, we can use MoveItem and get the animation
				var oldPath = NSIndexPath.Create(_section, args.OldStartingIndex);
				var newPath = NSIndexPath.Create(_section, args.NewStartingIndex);
 
				Update(() => CollectionView.MoveItem(oldPath, newPath), args);
				return;
			}
 
			var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
			var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
 
			Update(() => CollectionView.ReloadItems(CreateIndexesFrom(start, end)), args);
		}
 
		internal int ItemsCount()
		{
			if (_itemsSource is IList list)
				return list.Count;
 
			int count = 0;
			foreach (var item in _itemsSource)
				count++;
			return count;
		}
 
		internal object ElementAt(int index)
		{
			if (_itemsSource is IList list)
				return list[index];
 
			int count = 0;
			foreach (var item in _itemsSource)
			{
				if (count == index)
					return item;
				count++;
			}
 
			return -1;
		}
 
		void Update(Action update, NotifyCollectionChangedEventArgs args)
		{
			if (CollectionView.Hidden)
			{
				return;
			}
 
			OnCollectionViewUpdating(args);
			update();
			OnCollectionViewUpdated(args);
		}
 
		void OnCollectionViewUpdating(NotifyCollectionChangedEventArgs args)
		{
			CollectionViewUpdating?.Invoke(this, args);
		}
 
		void OnCollectionViewUpdated(NotifyCollectionChangedEventArgs args)
		{
			CollectionView.BeginInvokeOnMainThread(() =>
			{
				CollectionViewUpdated?.Invoke(this, args);
			});
		}
	}
}