更新:已解决!请参阅下面的答案以获取解决方案。
我的应用在 UICollectionView 中显示许多图像。而且我目前遇到了插入项目索引路径的问题,当新项目插入得太快以至于集合视图无法处理时。以下是例外情况:
NSInternalInconsistency异常原因:更新动画过多 在一个视图上 - 一次限制为 31 个飞行中
事实证明,这是由于我的模型缓冲了多达 20 个新图像并立即将它们推送到数据源,但不是在集合视图批处理更新块中。没有批量更新不是由于我的懒惰,而是因为我的数据源之间的抽象层实际上是 .Net 可观察集合(下面的代码(。
我想知道的是,开发人员应该如何防止在飞行中达到 31 个动画的硬编码限制?我的意思是,当它发生时,你是烤面包。那么苹果在想什么呢?
阅读代码的Monotouch开发人员请注意:
崩溃实际上是由 UICollectionViewDataSourceFlatReadOnly 压倒性的 UIDataBoundCollectionView 和 CollectionChanged 事件引起的,它代表基础可观察集合代理到控件。这会导致集合视图被非批处理的 InsertItems 调用所困扰。(是的,保罗,这是一个反应式集合(。
UIDataBoundCollectionView
/// <summary>
/// UITableView subclass that supports automatic updating in response
/// to DataSource changes if the DataSource supports INotifiyCollectionChanged
/// </summary>
[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
/// <summary>
/// Binds a table to an flat (non-grouped) items collection
/// Supports dynamically changing collections through INotifyCollectionChanged
/// </summary>
public class UICollectionViewDataSourceFlatReadOnly : UICollectionViewDataSource,
ICollectionViewDataSource,
INotifyCollectionChanged
{
/// <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)
{
// wire event handler
ncc.CollectionChanged += OnItemsChanged;
}
}
#region Properties
private IReadOnlyList<object> 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;
}
/// <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 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
:
- 如果您的任何更新是重置的,则需要忘记执行其他所有操作
- 如果应用程序在同一次运行中添加和删除了相同的项目,则需要检测到并对其进行去抖动(即甚至不要告诉 UIKit 它(。当您意识到添加/删除可以更改一系列索引而不仅仅是单个索引时,这变得更加棘手。
更新:在我写完这个答案将近一年后,我强烈建议使用 Paul Betts 提到的 ReactiveUI CollectionView/TableView 绑定功能。现在处于更加成熟的状态。
事实证明,解决方案比预期的要困难一些。多亏了 RX,限制每个项目插入或删除的速率在 UICollectionViewDataSourceFlatReadOnly 中很容易解决。下一步涉及在UIDataBoundCollectionView中将这些更改一起批处理。PerformBatchUpdate在这里没有帮助,但是发出一个带有所有插入的IndexPaths的InsertItems调用确实解决了这个问题。
由于UICollectionView验证其内部一致性的方式(即,它在每次InsertItem或DeleteItems等之后调用GetItemsCount(,我不得不将ItemCount管理移交给UIDataBoundCollectionView(那个很难接受,但别无选择(。
顺便说一下,性能是一流的。
以下是任何感兴趣的人的更新来源:
ICollectionView数据源
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
}