代码之家  ›  专栏  ›  技术社区  ›  Oliver Weichhold

UICollectionView-一个视图上的更新动画太多

  •  4
  • Oliver Weichhold  · 技术社区  · 11 年前

    更新: 解决了的!有关解决方案,请参阅下面的答案。

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

    NSInternalConsistencyException原因:更新动画过多 在一个视图上-每次飞行限制为31

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

    我想知道的是,开发人员应该如何防止达到飞行中31个动画的硬编码限制?我的意思是,当它发生时,你就完蛋了。那么苹果在想什么呢?

    阅读代码的Monotuch开发人员注意:

    崩溃实际上是由UICollectionViewDataSourceFlatReadOnly用CollectionChanged事件压倒UIDataBoundCollectionView引起的,UIDataBound CollectionView代表基础可观察集合代理到控件。这导致collectionview被非批处理 插入项目 电话。(是的,Paul,这是一个ReactiveCollection)。

    UI数据边界集合视图

    /// <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;
        }
      }
    }
    

    UI集合视图DataSource平面只读

    /// <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);
      }
    }
    
    2 回复  |  直到 11 年前
        1
  •  3
  •   Ana Betts    11 年前

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

    1. 如果你的任何更新都是重置,你需要忘记做其他事情
    2. 如果应用程序已添加和删除 相同的 在同一次运行中,您需要检测到它并取消它的抖动(即,甚至不要告诉UIKit这件事)。当您意识到Add/Remove可以更改 范围 指数,而不仅仅是一个单一的指数。
        2
  •  2
  •   Community leo1    7 年前

    更新: 现在,在我写下这个答案将近一年后,我强烈建议使用ReactiveUI CollectionView/TableView绑定功能 mentioned by Paul Betts 。现在它的状态要成熟得多。


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

    由于UICollectionView验证其内部一致性的方式(即在每个InsertItem或DeleteItems之后调用GetItemsCount等),我不得不将ItemCount管理移交给UIDataBoundCollectionView(这很难接受,但别无选择)。

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

    以下是感兴趣的人的最新消息来源:

    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; }
    }
    

    UI数据边界集合视图

    [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;
          }
        }
      }
    }
    

    UI集合视图DataSource平面只读

    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
    }