insertItemsAtIndexPaths throws NSInternalInconsistencyException when called right after UICollectionView view was created.

Originator:anthony
Number:rdar://26484150 Date Originated:5/25/2016
Status:Open Resolved:
Product:iOS Product Version:9.3.2
Classification:Crash Reproducible:Always
 
Summary:

To insert or delete some items the collection view must know how many items it had before and after. That means insert\delete operations will trigger data source methods to get those numbers. Typically, the collection view knows how many items it has by the moment we insert\delete. Because it invokes data source methods starting from the moment when we lay it out. So usually it will query the data source only once on insert\delete: to get the updated numbers after the items were inserted\removed.

But, if we created a collection view from scratch and immediately tried to insert\delete data, the collection view would trigger the data source methods twice! First time to get the numbers before the change, and second time to see how they have changed. That means there's no way to change the number of items returned by the data source methods in between of those two queries, which leads to NSInternalInconsistencyException.

Calling reloadData before the update doesn't invoke data source methods immediately, but rather invalidates some collection view's internal state and queries for the data only when necessary. That's quite obviously made for optimization purposes.

The only reliable way to force the collection view to read the current number of items before the insertion/deletion is either:

1) [collectionView performBatchUpdates:^{  // Invokes initial numbedOfItemsInSection if needed.
      // Now update the model here.
      [collectionView insertItemsAtIndexPaths:_index_paths_];
    } completion:nil];
    or 
2) [collectionView numberOfItemsInSection:_section_];  // Force the initial data source call.
    [collectionView insertItemsAtIndexPaths:_index_paths_];

Steps to Reproduce:
Compile and run the following code:

@interface ViewController : UIViewController<UICollectionViewDataSource>
@end

@implementation ViewController {
  NSUInteger _numberOfItems;
}

- (void)viewDidLoad {
  [super viewDidLoad];
  UICollectionView *collectionView =
      [[UICollectionView alloc] initWithFrame:self.view.bounds
                                  collectionViewLayout:[UICollectionViewFlowLayout new]];
  collectionView.dataSource = self;

  NSArray<NSIndexPath *> *insertion = @[[NSIndexPath indexPathForItem:0 inSection:0]];
  _numberOfItems += insertion.count;
  [collectionView insertItemsAtIndexPaths:insertion];  // CRASH
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  return _numberOfItems;
}

@end

Expected Results:
UICollectionView performs the insertion and doesn't throw NSInternalInconsistencyException.
OR the official documentation describes this issue and provides some recommendations how to bypass it.

Actual Results:
2016-05-25 17:46:29.844 UICollectionView Tweaks[29712:7489886] *** Assertion failure in -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit_Sim/UIKit-3512.60.7/UICollectionView.m:4625
2016-05-25 17:46:29.847 UICollectionView Tweaks[29712:7489886] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0.  The number of items contained in an existing section after the update (1) must be equal to the number of items contained in that section before the update (1), plus or minus the number of items inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'
*** First throw call stack:
(
	0   CoreFoundation                      0x0000000106e78d85 __exceptionPreprocess + 165
	1   libobjc.A.dylib                     0x00000001068eadeb objc_exception_throw + 48
	2   CoreFoundation                      0x0000000106e78bea +[NSException raise:format:arguments:] + 106
	3   Foundation                          0x0000000106533d5a -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 198
	4   UIKit                               0x0000000107abe077 -[UICollectionView _endItemAnimationsWithInvalidationContext:tentativelyForReordering:] + 15363
	5   UIKit                               0x0000000107aba2ca -[UICollectionView _updateRowsAtIndexPaths:updateAction:] + 350
	6   UICollectionView Tweaks             0x00000001063e53f9 -[ViewController insertItemsIntoCollectionView] + 217
	7   UICollectionView Tweaks             0x00000001063e4efc -[ViewController viewDidLoad] + 92
	8   UIKit                               0x00000001073ca984 -[UIViewController loadViewIfRequired] + 1198
	9   UIKit                               0x00000001073cacd3 -[UIViewController view] + 27
	10  UIKit                               0x00000001072a0fb4 -[UIWindow addRootViewControllerViewIfPossible] + 61
	11  UIKit                               0x00000001072a169d -[UIWindow _setHidden:forced:] + 282
	12  UIKit                               0x00000001072b3180 -[UIWindow makeKeyAndVisible] + 42
	13  UICollectionView Tweaks             0x00000001063e56e3 -[AppDelegate application:didFinishLaunchingWithOptions:] + 483
	14  UIKit                               0x00000001072269ac -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] + 272
	15  UIKit                               0x0000000107227c0d -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3415
	16  UIKit                               0x000000010722e568 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1769
	17  UIKit                               0x000000010722b714 -[UIApplication workspaceDidEndTransaction:] + 188
	18  FrontBoardServices                  0x0000000109cca8c8 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 24
	19  FrontBoardServices                  0x0000000109cca741 -[FBSSerialQueue _performNext] + 178
	20  FrontBoardServices                  0x0000000109ccaaca -[FBSSerialQueue _performNextFromRunLoopSource] + 45
	21  CoreFoundation                      0x0000000106d9e301 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
	22  CoreFoundation                      0x0000000106d9422c __CFRunLoopDoSources0 + 556
	23  CoreFoundation                      0x0000000106d936e3 __CFRunLoopRun + 867
	24  CoreFoundation                      0x0000000106d930f8 CFRunLoopRunSpecific + 488
	25  UIKit                               0x000000010722af21 -[UIApplication _run] + 402
	26  UIKit                               0x000000010722ff09 UIApplicationMain + 171
	27  UICollectionView Tweaks             0x00000001063e54df main + 111
	28  libdyld.dylib                       0x000000010966a92d start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

Comments


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!