UICollectionView – 在一个视图上更新动画太多

更新:解决了! 请参阅下面的答案以获得解决方案。

我的应用程序在UICollectionView中显示了许多图像。 当新项目插入太快而集合视图无法处理时,我目前遇到insertItemsAtIndexPaths问题。 以下是例外情况:

NSInternalInconsistencyException原因:在一个视图上更新动画太多 – 一次飞行限制为31

事实certificate这是由于我的模型缓冲了多达20个新图像并立即将它们推送到数据源但不在集合视图批量更新块中。 没有批量更新不是由于我的懒惰造成的,而是因为我的数据源之间的抽象层实际上是一个.Net Observable集合(下面的代码)。

我想知道的是开发人员如何防止在飞行中达到31个动画的硬编码限制? 我的意思是当它发生时,你是敬酒。 那么Apple的想法是什么?

Monotouch开发人员阅读代码的注意事项:

崩溃实际上是由UICollectionViewDataSourceFlatReadOnly压倒UIDataBoundCollectionView并使用CollectionChanged事件引起的,它代表底层的observable集合代理控件。 这导致collectionview受到非批量InsertItems调用的打击。 (是保罗,它是一个ReactiveCollection)。

UIDataBoundCollectionView

///  /// UITableView subclass that supports automatic updating in response /// to DataSource changes if the DataSource supports INotifiyCollectionChanged ///  [Register("UIDataBoundCollectionView")] public class UIDataBoundCollectionView : UICollectionView, IEnableLogger { public override NSObject WeakDataSource { get { return base.WeakDataSource; } set { var ncc = base.WeakDataSource as INotifyCollectionChanged; if(ncc != null) { ncc.CollectionChanged -= OnDataSourceCollectionChanged; } base.WeakDataSource = value; ncc = base.WeakDataSource as INotifyCollectionChanged; if(ncc != null) { ncc.CollectionChanged += OnDataSourceCollectionChanged; } } } void OnDataSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { NSIndexPath[] indexPaths; switch(e.Action) { case NotifyCollectionChangedAction.Add: indexPaths = IndexPathHelper.FromRange(e.NewStartingIndex, e.NewItems.Count); InsertItems(indexPaths); break; case NotifyCollectionChangedAction.Remove: indexPaths = IndexPathHelper.FromRange(e.OldStartingIndex, e.OldItems.Count); DeleteItems(indexPaths); 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: ReloadData(); break; } } } 

UICollectionViewDataSourceFlatReadOnly

 ///  /// Binds a table to an flat (non-grouped) items collection /// Supports dynamically changing collections through INotifyCollectionChanged ///  public class UICollectionViewDataSourceFlatReadOnly : UICollectionViewDataSource, ICollectionViewDataSource, INotifyCollectionChanged { ///  /// Initializes a new instance of the  class. ///  /// The table. /// The items. /// The cell provider public UICollectionViewDataSourceFlatReadOnly(IReadOnlyList 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) { // wire event handler ncc.CollectionChanged += OnItemsChanged; } } #region Properties private IReadOnlyList items; private readonly ICollectionViewCellProvider cellProvider; #endregion #region Overrides of UICollectionViewDataSource public override int NumberOfSections(UICollectionView collectionView) { return 1; } public override int GetItemsCount(UICollectionView collectionView, int section) { return items.Count; } ///  /// Gets the cell. ///  /// The table view. /// The index path. ///  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 ///  /// Gets the item at. ///  /// The index path. ///  public object GetItemAt(NSIndexPath indexPath) { return items[indexPath.Item]; } public int ItemCount { get { return items.Count; } } #endregion #region INotifyCollectionChanged implementation // UIDataBoundCollectionView will subscribe to this event public event NotifyCollectionChangedEventHandler CollectionChanged; #endregion void OnItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { if(CollectionChanged != null) CollectionChanged(sender, e); } } 

凉! 最新版本的RxUI与UITableView, ReactiveTableViewSource类似。 我对NSInternalInconsistencyException也有一些棘手的问题:

  1. 如果您的任何更新都是重置,您需要忘记执行其他所有操作
  2. 如果应用程序在同一次运行中添加并删除了相同的项目,则需要检测并对其进行去抖(即甚至不告诉UIKit)。 当您意识到添加/删除可以更改索引范围而不仅仅是单个索引时,这会变得更加棘手。

更新:现在差不多一年后,在我写完这个答案之后,我强烈建议使用Paul Betts提到的ReactiveUI CollectionView / TableView绑定function。 现在处于一个更加成熟的状态。


事实certificate这个解决方案比预期的要困难一些。 由于RX,在UICollectionViewDataSourceFlatReadOnly中很容易解决每个项目插入或删除的速率限制。 下一步涉及在UIDataBoundCollectionView内将这些更改一起批处理。 PerformBatchUpdate在这里没有帮助,但发出一个InsertItems调用与所有插入的IndexPaths确实解决了这个问题。

由于UICollectionViewvalidation其内部一致性的方式(即它在每个InsertItem或DeleteItems之后调用GetItemsCount等),我不得不将ItemCount管理移交给UIDataBoundCollectionView(那个难以吞下但没有选择)。

顺便说一下,表现非常出色。

以下是感兴趣的人的更新来源:

ICollectionViewDataSource

 public interface ICollectionViewDataSource { ///  /// Gets the bound item at the specified index ///  /// The index path. ///  object GetItemAt(NSIndexPath indexPath); ///  /// Gets the actual item count. ///  /// The item count. int ActualItemCount { get; } ///  /// Gets or sets the item count reported to UIKit ///  /// The item count. int ItemCount { get; set; } ///  /// Observable providing change monitoring ///  /// The collection changed observable. IObservable 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 indexPaths = new List(); int index = 0; for(;index { for(int i=0; i 

UICollectionViewDataSourceFlatReadOnly

 public class UICollectionViewDataSourceFlatReadOnly : UICollectionViewDataSource, ICollectionViewDataSource { ///  /// Initializes a new instance of the  class. ///  /// The table. /// The items. /// The cell provider public UICollectionViewDataSourceFlatReadOnly(IReadOnlyList 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( 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 items; private readonly ICollectionViewCellProvider cellProvider; IObservable 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; } ///  /// Gets the cell. ///  /// The table view. /// The index path. ///  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 ///  /// Gets the item at. ///  /// The index path. ///  public object GetItemAt(NSIndexPath indexPath) { return items[indexPath.Item]; } public int ActualItemCount { get { return items.Count; } } public int ItemCount { get; set; } public IObservable CollectionChangedObservable { get { return collectionChangedObservable; } } #endregion }