Ryan Ballantyne Ryan Ballantyne - 3 months ago 33
iOS Question

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?

Answer

Just a guess: at the time you are inserting the first image, the collection view may not yet have loaded its data. However, in the exception message, the collection view claims to "know" the number of items before the insertion (1). Therefore, it could have lazily loaded its data in insertItemsAtIndexPaths: and taken the result as "before" state. Also, you don't need to reload data after an insertion.

Long story short, move the

[cv reloadData];

up to get

if ([imageViews count] == 0)  {
    [self.noImagesLabel removeFromSuperview];
    [self.imageDisplayCell.contentView addSubview:cv];
    [cv reloadData];
}
Comments