Invalid update: invalid number of items on UICollectionView

36,352

Solution 1

It's a bug with using insertItemsAtIndexPaths on an empty UICollectionView. Just do this:

if (self.birthdays.count == 1) {
    [self.collectionView reloadData];
} else {
    [self.collectionView insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:   (self.birthdays.count -1) inSection:0]]];
}

(Can't believe this is still broken in iOS8.)

Solution 2

It is a bug in UICollectionView even in iOS 11, but not exist in UITableView, very easy to reproduce: Assume the following SWIFT code is working:

addItemInDataSource()
collectionView!.insertItems(at: [IndexPath(item: position, section: 0)])

Change it to:

collectionView!.reloadData()    // <-- This new line will make UICollection crash...

addItemInDataSource()
collectionView!.insertItems(at: [IndexPath(item: position, section: 0)])

It will crash...

UICollectionView will lose track of the original number of items in some condition, just add a dummy line before insertItems command will avoid this kind of crash:

collectionView!.reloadData()

collectionView!.numberOfItems(inSection: 0) //<-- This code is no used, but it will let UICollectionView synchronize number of items, so it will not crash in following code.
addItemInDataSource()
collectionView!.insertItems(at: [IndexPath(item: position, section: 0)])

Hope it could help in your situation.

Solution 3

I had the same error when I've inserted elements in a collectionView with only one section.

I've done the Timothy fix, but it was not enough. In fact, the collectionView had already refreshed its data when I tried to insert new elements. Using collectionView numberOfItemsInSection solves the problem.

[self.collectionView performBatchUpdates:^{
        NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
        for (NSUInteger i = [self.collectionView numberOfItemsInSection:0]; i < self.elements.count ; i++)
        {
            [arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
        }

        [self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
    } completion:nil];

Solution 4

In my case, numberOfItemsInSection was being called twice from self.performBatchUpdates (once for the BEFORE number, once for the AFTER number) because I was trying to insert something into the collectionView before the view controller had finished loading.

The solution is to add guard isViewLoaded else { return } before calling self.performBatchUpdates.

Share:
36,352
Tommy Alexander
Author by

Tommy Alexander

I have more than 15 years experience in IT doing pretty much everything at some point. Started out as a Service Desk tech, did PC work, SyS Admin, SAP/Oracle, Cisco networking, virtualization, pretty much all things infrastructure. Have done more development work lately writing some business app's in PHP/JQuery/HTML5 on top of MySQL server side and IndexedDB client side, pretty much using the LAMP stack. Have written an app or two in iOS, working on a certification now in iOS development from University of Washington. Oh yea, I have an MBA so I can also talk pretty well to the business guys.....

Updated on December 11, 2020

Comments

  • Tommy Alexander
    Tommy Alexander over 3 years

    I am stumped on this. Here is my scenario. In my Appdelegate, I am creating

    1. An instance of a view controller that will be presented modally to collect two pieces of data from the user
    2. A tableView controller
    3. A navigation controller that I init with the tableview controller as its rootview
      controller
    4. A collectionviewController, note that I am registering the class for the cell here as well.
    5. A UITabBarController that I add the navigation controller to as the first view and the
      collectionview to as the second view.. The TabBarCOntorller is set as the rootview of the window.

    Here is the code:

     - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:   
    (NSDictionary *)launchOptions
    {
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.evc = [[WTAEntryControllerViewController alloc] init];
    WTATableView *tv = [[WTATableView alloc] initWithNibName:@"WTATableView" bundle:nil];
    UINavigationController *nav = [[UINavigationController alloc]    
    initWithRootViewController:tv];
    nav.tabBarItem.title = NSLocalizedString(@"Birthday List", nil);
    nav.tabBarItem.image = [UIImage imageNamed:@"birthdaycake"];
    WTACollectionViewController *cv = [[WTACollectionViewController alloc] 
    initWithCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init]];
    [cv.collectionView registerClass:[UICollectionViewCell class] 
    forCellWithReuseIdentifier:CellIdentifier];
    cv.collectionView.backgroundColor = [UIColor whiteColor];
    NSLog(@"cv instance %@", cv);
    UITabBarController *tabController = [[UITabBarController alloc] init];
    tabController.viewControllers = @[nav, cv];
    self.window.rootViewController = tabController;
    [self.window makeKeyAndVisible];
    return YES
    }
    

    The tableview has a navbar button that presents the model view controller. The modal view controller has a button that takes the values from a textField and a datepicker and posts that to the default notification center with the values in the UserInfo dictionary. The tableView contorller subscribes to the notification, puts the UserInfo dictionary in a MutableArray and updates the table. That works fine.

    The problem is with the CollectionVIew. The collectionVIew receives the notification and calls a target method to handle the notification. I am trying to take the UserInfo dictionary from the notification and add a new cell to the collectionView.

    BUT...

    When the collectionview receives the notification I get the error:

    '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).'

    Here is the code for the CollectionView:

    #import "WTACollectionViewController.h"
    #import "UIColor+WTAExtensions.h"
    #import "WTAAppDelegate.h"
    #import "WTAEntryControllerViewController.h"
    
    @interface WTACollectionViewController () <UICollectionViewDelegateFlowLayout>
    @property (strong, nonatomic) NSMutableArray *birthdays;
    @end
    
    @implementation WTACollectionViewController
    
    -(id)initWithCollectionViewLayout:(UICollectionViewLayout *)layout{
    
    NSLog(@"Calling init on Collection");
    if(!(self = [super initWithCollectionViewLayout:layout])){
        return nil;
    }
    
    self.birthdays = [NSMutableArray array];
    
    NSLog(@"Count of Birthdays at init %ld", (long)[self.birthdays count] );
    self.title = NSLocalizedString(@"Birthday Grid", nil);
    self.tabBarItem.image = [UIImage imageNamed:@"package"];
    NSLog(@"cv instance getting notif %@", self);
    [[NSNotificationCenter defaultCenter]
     addObserver:self
     selector:@selector(handleNotification:)
     name:@"BirthdayAdded"
     object:nil];
     return self;
    
    }
    
    
    -(void)handleNotification:(NSNotification *)notification
    {
    [self.birthdays addObject:[notification userInfo]];
    NSLog(@"Count of birthdays when the notification is received: %ld", (long)         
    [self.birthdays count]);
    [self.collectionView insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:   
    (self.birthdays.count -1) inSection:0]]];
    }
    
    - (void)viewDidLoad
    {
    [super viewDidLoad];
    
    NSLog(@"Calling View Did Load on Collection");
    }
    
    - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
    
    return 1;
    
    }
    
    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:   
    (NSInteger)section{
    
    NSLog(@"Count of birthdays in  the number of items in section: %ld", (long)  
    [self.birthdays count]);
    return [self.birthdays count];
    }
    
    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView    
    cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    
    NSLog(@"Calling the cell for index path method");
    
    NSDictionary *dict = [[NSMutableDictionary alloc] init];
    dict = self.birthdays[indexPath.item];
    
    UICollectionViewCell *cell = [collectionView   
    dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath];
    NSAssert(cell != nil, @"Expected a Cell");
    
    cell.contentView.backgroundColor = [UIColor randomColor];
    return cell;
    
    }
    
    #define GAP (1.0f)
    
    -(CGSize)collectionView:(UICollectionView *)collectionView layout:   
    (UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath   
    *)indexPath {
    
    
    CGFloat edgeLength = self.collectionView.frame.size.width / 4.0f - GAP;
    return (CGSize) {.width = edgeLength, .height = edgeLength};
    }
    
    -(CGFloat)collectionView:(UICollectionView *)collectionView layout:   
    (UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:    
    (NSInteger)section{
    
    return GAP;
    }
    
    -(CGFloat)collectionView:(UICollectionView *)collectionView layout:   
    (UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:  
    (NSInteger)section{
    
    return GAP;
    }
    @end
    

    I would so much appreciate anyone who can point anything out here.....

  • Tommy Alexander
    Tommy Alexander over 10 years
    This was a life saver! Thanks!
  • Max
    Max over 9 years
    Is that fixed in iOS 8?
  • Johnny
    Johnny over 9 years
    It works for me on iOS 8.1, Inserting items in an empty collectionview.
  • Nick Vikeras
    Nick Vikeras over 7 years
    Wow I spent 30+ hours tracking this down. Do you have a link to the bug report with apple? I'd like to document it in my code base.
  • Alexander Khitev
    Alexander Khitev over 6 years
    interestingly, does Apple knows this?
  • Michael Su
    Michael Su over 6 years
    I did not report it to Apple.
  • Tà Truhoada
    Tà Truhoada about 6 years
    What is "isViewLoaded"
  • p-sun
    p-sun about 6 years
    @TàTruhoada It's a bool on the UIViewController that becomes true after the UI elements are displayed on screen. developer.apple.com/documentation/uikit/uiviewcontroller/…
  • Admin
    Admin about 6 years
    I still have this bug (May 2018), your solution fixed it. Thank you. I am inserting dynamically a NativeAd as a cell in my UICollectionView when it is ready and loaded, but sometimes it crashes. The "dummy line" seems to work.
  • StartPlayer
    StartPlayer over 5 years
    my crash was happening as I was setting dataSource in ViewDidLoad. after I reading this I moved it to view did appear and not had the crash again
  • marcelosalloum
    marcelosalloum almost 5 years
    @p-sun: according to Apple, isViewLoaded is: a Boolean value indicating whether the view is currently loaded into memory. This means the view is loaded into memory BEFORE becoming visible. If you need to check if the viewController is visible, you should do viewController.viewIfLoaded?.window != nil
  • Majid Bashir
    Majid Bashir over 4 years
    in which method do i need to add ? this line of code ?
  • Timothy Moose
    Timothy Moose over 4 years
    @MajidBashir wherever it makes sense for your view controller.
  • sebastienhamel
    sebastienhamel about 4 years
    This fix works for the same bug in NSCollectionView in macOS 10.15.