Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView insertItemsAtIndexPaths: throws exception

UICollectionView: I'm doing it wrong. I just don't know how.

My Setup

I'm running this on an iPhone 4S with iOS 6.0.1.

My Goal

I have a table view in which one section is devoted to images: Screen shot of the table view section

When the user taps the "Add Image..." cell, they are prompted to either choose an image from their photo library or take a new one with the camera. That part of the app seems to be working fine.

When the user first adds an image, the "No Images" label will be removed from the second table cell, and a UICollectionView, created programmatically, is added in its place. That part also seems to be working fine.

The collection view is supposed to display the images they have added, and it's here where I'm running into trouble. (I know that I'm going to have to jump through some hoops to get the table view cell to enlarge itself as the number of images grows. I'm not that far yet.)

When I attempt to insert an item into the collection view, it throws an exception. More on that later.

My Code

I've got the UITableViewController in charge of the table view also acting as the collection view's delegate and datasource. Here is the relevant code (I have omitted the bits of the controller that I consider unrelated to this problem, including lines in methods like -viewDidLoad. I've also omitted most of the image acquisition code since I don't think it's relevant):

#define ATImageThumbnailMaxDimension 100

@interface ATAddEditActivityViewController ()
{
    UICollectionView* imageDisplayView;
    NSMutableArray* imageViews;
}

@property (weak, nonatomic) IBOutlet UITableViewCell *imageDisplayCell;
@property (weak, nonatomic) IBOutlet UILabel *noImagesLabel;
@end

@implementation ATAddEditActivityViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UICollectionViewFlowLayout* flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;

    imageDisplayView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, 0, 290, 120) collectionViewLayout:flowLayout];  // The frame rect still needs tweaking
    imageDisplayView.delegate = self;
    imageDisplayView.dataSource = self;
    imageDisplayView.backgroundColor = [UIColor yellowColor];  // Just so I can see the actual extent of the view
    imageDisplayView.opaque = YES;
    [imageDisplayView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:@"Cell"];

    imageViews = [NSMutableArray array];
}

#pragma mark - UIImagePickerDelegate
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{
    /* ...code defining imageToSave omitted... */

    [self addImage:imageToSave toCollectionView:imageDisplayView];


    [self dismissModalViewControllerAnimated:YES];
}

#pragma mark - UICollectionViewDelegate
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return YES;
}

#pragma mark - UICollectionViewDatasource
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    [[cell  contentView] addSubview:imageViews[indexPath.row]];
    return cell;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return [imageViews count];
}

#pragma mark - UICollectionViewDelegateFlowLayout
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return ((UIImageView*)imageViews[indexPath.item]).bounds.size;
}

#pragma mark - Image Handling
- (void)addImage:(UIImage*)image toCollectionView:(UICollectionView*)cv
{
    if ([imageViews count] == 0)  {
        [self.noImagesLabel removeFromSuperview];
        [self.imageDisplayCell.contentView addSubview:cv];
    }

    UIImageView* imageView = [[UIImageView alloc] initWithImage:image];
    /* ...code that sets the bounds of the image view omitted... */

    [imageViews addObject:imageView];
    [cv insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
    [cv reloadData];
}

@end

To summarize:

  • The collection view is instantiated and configured in the -viewDidLoad method
  • The UIImagePickerDelegate method that receives the chosen image calls -addImage:toCollectionView
  • ...which creates a new image view to hold the image and adds it to the datasource array and collection view. This is the line that produces the exception.
  • The UICollectionView datasource methods rely on the imageViews array.

The Exception

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

If I'm parsing this right, what this is telling me is that the (brand new!) collection view thinks it was created with a single item. So, I added a log to -addImage:toCollectionView to test this theory:

NSLog(@"%d", [cv numberOfItemsInSection:0]);

With that line in there, the exception never gets thrown! The call to -numberOfItemsInSection: must force the collection view to consult its datasource and realize that it has no items. Or something. I'm conjecturing here. But, well, whatever: the collection view still doesn't display any items at this point, so I'm still doing something wrong and I don't know what.

In Conclusion

  1. I get an odd exception when I attempt to add an item to a newly-minted-and-inserted collection view...except when I call -numberOfItemsInSection: before attempting insertion.
  2. Even if I manage to get past the exception with a shady workaround, the items still do not show up in the collection view.

Sorry for the novel of a question. Any ideas?

like image 470
Ryan Ballantyne Avatar asked Dec 26 '22 14:12

Ryan Ballantyne


1 Answers

Unfortunately the accepted answer is incorrect (although it's on the right track); the problem is that you were calling reloadData & insertItems when you should have just been inserting the item. So instead of:

[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];
[cv reloadData];

Just do:

[imageViews addObject:imageView];
[cv insertItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:[imageViews count]-1 inSection:0]]];

Not only will this give you a nice animation, it prevents you from using the tableview inefficiently (not a big deal in a 1-cell collection view, but a huge problem for larger data sets), and avoids crashes like the one you were seeing, where two methods were both trying to modify the collection view (and one of them -- reloadData -- does not play well with others).

As an aside, reloadData is not very UICollectionView-friendly; if you do have a sizable &/or complex collection, and an insertion happens shortly before or after a call to reloadData, the insertion might finish before the reloadData finishes -- which will reliably cause an "invalid number of items" crash (same goes for deletions). Calling reloadSections instead of just reloadData seems to help avoid that problem.

like image 128
sounder_michael Avatar answered Feb 23 '23 18:02

sounder_michael