Skip to content

Instantly share code, notes, and snippets.

@jsuarezruiz
Created August 24, 2020 14:11
Show Gist options
  • Save jsuarezruiz/341a2eea56dd9205729c317912b0bee5 to your computer and use it in GitHub Desktop.
Save jsuarezruiz/341a2eea56dd9205729c317912b0bee5 to your computer and use it in GitHub Desktop.
issue11853
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Threading;
using System.Threading.Tasks;
using Foundation;
using UIKit;
namespace Xamarin.Forms.Platform.iOS
{
internal class ObservableGroupedSource : IItemsViewSource
{
readonly UICollectionView _collectionView;
readonly UICollectionViewController _collectionViewController;
readonly IList _groupSource;
bool _disposed;
readonly SemaphoreSlim _batchUpdating = new SemaphoreSlim(1, 1);
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));
}
}
}
async void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if (Device.IsInvokeRequired)
{
await Device.InvokeOnMainThreadAsync(async () => await CollectionChanged(args));
}
else
{
await CollectionChanged(args);
}
}
async Task CollectionChanged(NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
await Add(args);
break;
case NotifyCollectionChangedAction.Remove:
await Remove(args);
break;
case NotifyCollectionChangedAction.Replace:
await Replace(args);
break;
case NotifyCollectionChangedAction.Move:
Move(args);
break;
case NotifyCollectionChangedAction.Reset:
await Reload();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
async Task Reload()
{
ResetGroupTracking();
await _batchUpdating.WaitAsync();
_collectionView.ReloadData();
_collectionView.CollectionViewLayout.InvalidateLayout();
_batchUpdating.Release();
}
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;
}
async Task Add(NotifyCollectionChangedEventArgs args)
{
if (ReloadRequired())
{
await 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
await BatchUpdateAsync(() => _collectionView.InsertSections(CreateIndexSetFrom(startIndex, count)));
}
async Task 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
await Reload();
return;
}
if (ReloadRequired())
{
await 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
await BatchUpdateAsync(() => _collectionView.DeleteSections(CreateIndexSetFrom(startIndex, count)));
}
async Task 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
_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
await 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
_collectionView.MoveSection(args.OldStartingIndex, args.NewStartingIndex);
return;
}
var start = Math.Min(args.OldStartingIndex, args.NewStartingIndex);
var end = Math.Max(args.OldStartingIndex, args.NewStartingIndex) + count;
_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, or has no actual
// cells (just supplementary views like Header/Footer), any insert/delete operations are gonna crash
// hard. We'll need to reload the data instead.
return NotLoadedYet()
|| _collectionView.NumberOfSections() == 0
|| _collectionView.VisibleCells.Length == 0;
}
Task BatchUpdateAsync(Action update)
{
_collectionView.PerformBatchUpdates(async () =>
{
if (_batchUpdating.CurrentCount > 0)
{
await _batchUpdating.WaitAsync();
}
update();
},
(_) =>
{
if (_batchUpdating.CurrentCount == 0)
{
_batchUpdating.Release();
}
});
return Task.CompletedTask;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment