UICollectionView - too many update animations on one view

南楼画角 提交于 2019-12-05 14:18:25

Cool! The latest version of RxUI has a similar class for UITableView, ReactiveTableViewSource. I also had some tricky issues with NSInternalInconsistencyException:

  1. If any of your updates are a Reset, you need to forget about doing everything else
  2. If the app has added and removed the same item in the same run, you need to detect that and debounce it (i.e. don't even tell UIKit about it). This gets even trickier when you realize that Add / Remove can change a range of indices, not just a single index.
Oliver Weichhold

Update: Now almost a year later after I wrote this answer, I would strongly recommend using the ReactiveUI CollectionView/TableView binding functionality mentioned by Paul Betts. Which is in a much more mature state now.


The solution turned out to be a bit harder than expected. Thanks to RX, throttling the rate of per single item inserts or deletes was easy to solve in UICollectionViewDataSourceFlatReadOnly . The next step involved batching those changes together inside UIDataBoundCollectionView. PerformBatchUpdate didn't help here but issuing a single InsertItems call with all the inserted IndexPaths did solve the problem.

Due to the way UICollectionView validates its internal consistency (ie. it calls GetItemsCount after each and every InsertItem or DeleteItems etc), I had to hand over ItemCount management to UIDataBoundCollectionView (that one was hard to swallow but there was no choice).

Performance is stellar by the way.

Here's the updated source for anyone interested:

ICollectionViewDataSource

public interface ICollectionViewDataSource
{
  /// <summary>
  /// Gets the bound item at the specified index
  /// </summary>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  object GetItemAt(NSIndexPath indexPath);

  /// <summary>
  /// Gets the actual item count.
  /// </summary>
  /// <value>The item count.</value>
  int ActualItemCount { get; }

  /// <summary>
  /// Gets or sets the item count reported to UIKit
  /// </summary>
  /// <value>The item count.</value>
  int ItemCount { get; set; }

  /// <summary>
  /// Observable providing change monitoring
  /// </summary>
  /// <value>The collection changed observable.</value>
  IObservable<NotifyCollectionChangedEventArgs[]> CollectionChangedObservable { get; }
}

UIDataBoundCollectionView

[Register("UIDataBoundCollectionView")]
public class UIDataBoundCollectionView : UICollectionView,
  IEnableLogger
{
  public UIDataBoundCollectionView (NSObjectFlag t) : base(t)
  {
  }

  public UIDataBoundCollectionView (IntPtr handle) : base(handle)
  {
  }

  public UIDataBoundCollectionView (RectangleF frame, UICollectionViewLayout layout) : base(frame, layout)
  {
  }

  public UIDataBoundCollectionView (NSCoder coder) : base(coder)
  {
  }

  protected override void Dispose(bool disposing)
  {
    base.Dispose(disposing);

    if(collectionChangedSubscription != null)
    {
      collectionChangedSubscription.Dispose();
      collectionChangedSubscription = null;
    }
  }

  IDisposable collectionChangedSubscription;

  public override NSObject WeakDataSource
  {
    get
    {
      return base.WeakDataSource;
    }

    set
    {
      if(collectionChangedSubscription != null)
      {
        collectionChangedSubscription.Dispose();
        collectionChangedSubscription = null;
      }

      base.WeakDataSource = value;

      collectionChangedSubscription = ICVS.CollectionChangedObservable
        .Subscribe(OnDataSourceCollectionChanged);
    }
  }

  ICollectionViewDataSource ICVS
  {
    get { return (ICollectionViewDataSource) WeakDataSource; }
  }

  void OnDataSourceCollectionChanged(NotifyCollectionChangedEventArgs[] changes)
  {
    List<NSIndexPath> indexPaths = new List<NSIndexPath>();
    int index = 0;

    for(;index<changes.Length;index++)
    {
      var e = changes[index];

      switch(e.Action)
      {
        case NotifyCollectionChangedAction.Add:
          indexPaths.AddRange(IndexPathHelper.FromRange(e.NewStartingIndex, e.NewItems.Count));
          ICVS.ItemCount++;

          // attempt to batch subsequent changes of the same type
          if(index < changes.Length - 1)
          {
            for(int i=index + 1; i<changes.Length; i++)
            {
              if(changes[i].Action == e.Action)
              {
                indexPaths.AddRange(IndexPathHelper.FromRange(changes[i].NewStartingIndex, changes[i].NewItems.Count));
                index++;
                ICVS.ItemCount++;
              }
            }
          }

          InsertItems(indexPaths.ToArray());
          indexPaths.Clear();
          break;

        case NotifyCollectionChangedAction.Remove:
          indexPaths.AddRange(IndexPathHelper.FromRange(e.OldStartingIndex, e.OldItems.Count));
          ICVS.ItemCount--;

          // attempt to batch subsequent changes of the same type
          if(index < changes.Length - 1)
          {
            for(int i=index + 1; i<changes.Length; i++)
            {
              if(changes[i].Action == e.Action)
              {
                indexPaths.AddRange(IndexPathHelper.FromRange(changes[i].OldStartingIndex, changes[i].OldItems.Count));
                index++;
                ICVS.ItemCount--;
              }
            }
          }

          DeleteItems(indexPaths.ToArray());
          indexPaths.Clear();
          break;

        case NotifyCollectionChangedAction.Replace:
        case NotifyCollectionChangedAction.Move:
          PerformBatchUpdates(() =>
          {
            for(int i=0; i<e.OldItems.Count; i++)
              MoveItem(NSIndexPath.FromItemSection(e.OldStartingIndex + i, 0), NSIndexPath.FromItemSection(e.NewStartingIndex + i, 0));
          }, null);
          break;

        case NotifyCollectionChangedAction.Reset:
          ICVS.ItemCount = ICVS.ActualItemCount;
          ReloadData();
          break;
      }
    }
  }
}

UICollectionViewDataSourceFlatReadOnly

public class UICollectionViewDataSourceFlatReadOnly : UICollectionViewDataSource,
  ICollectionViewDataSource
{
  /// <summary>
  /// Initializes a new instance of the <see cref="UICollectionViewDataSourceFlat"/> class.
  /// </summary>
  /// <param name="table">The table.</param>
  /// <param name="items">The items.</param>
  /// <param name="cellProvider">The cell provider</param>
  public UICollectionViewDataSourceFlatReadOnly(IReadOnlyList<object> items, ICollectionViewCellProvider cellProvider)
  {
    this.items = items;
    this.cellProvider = cellProvider;

    // wire up proxying collection changes if supported by source
    var ncc = items as INotifyCollectionChanged;
    if(ncc != null)
    {
      collectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
        h => ncc.CollectionChanged += h, h => ncc.CollectionChanged -= h)
        .SubscribeOn(TaskPoolScheduler.Default)
        .Select(x => x.EventArgs)
        .Buffer(TimeSpan.FromMilliseconds(100), 20)
        .Where(x => x.Count > 0)
        .Select(x => x.ToArray())
        .ObserveOn(RxApp.MainThreadScheduler)
        .StartWith(new[] { reset});   // ensure initial update
    }

    else
      collectionChangedObservable = Observable.Return(reset);
  }

  #region Properties
  private IReadOnlyList<object> items;
  private readonly ICollectionViewCellProvider cellProvider;
  IObservable<NotifyCollectionChangedEventArgs[]> collectionChangedObservable;
  static readonly NotifyCollectionChangedEventArgs[] reset = new[] { new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) };
  #endregion

  #region Overrides of UICollectionViewDataSource

  public override int NumberOfSections(UICollectionView collectionView)
  {
    return 1;
  }

  public override int GetItemsCount(UICollectionView collectionView, int section)
  {
    return ItemCount;
  }

  /// <summary>
  /// Gets the cell.
  /// </summary>
  /// <param name="tableView">The table view.</param>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  public override UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
  {
    // reuse or create new cell
    var cell = (UICollectionViewCell) collectionView.DequeueReusableCell(cellProvider.Identifier, indexPath);

    // get the associated collection item
    var item = GetItemAt(indexPath);

    // update the cell
    if(item != null)
      cellProvider.UpdateCell(cell, item, collectionView.GetIndexPathsForSelectedItems().Contains(indexPath));

    // done
    return cell;
  }

  #endregion

  #region Implementation of ICollectionViewDataSource

  /// <summary>
  /// Gets the item at.
  /// </summary>
  /// <param name="indexPath">The index path.</param>
  /// <returns></returns>
  public object GetItemAt(NSIndexPath indexPath)
  {
    return items[indexPath.Item];
  }

  public int ActualItemCount
  {
    get
    {
      return items.Count;
    }
  }

  public int ItemCount { get; set; }

  public IObservable<NotifyCollectionChangedEventArgs[]> CollectionChangedObservable
  {
    get
    {
      return collectionChangedObservable;
    }
  }

  #endregion
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!