insertItemsAtIndexPaths Update Error - objective-c

In UICollectionView, I am trying to use performBatchUpdates:completion to perform updates to my grid view. My data source array is self.results.
Here's my code:
dispatch_sync(dispatch_get_main_queue(), ^{
[self.collectionView performBatchUpdates:^{
int resultsSize = [self.results count];
[self.results addObjectsFromArray:newData];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
if (resultsSize == 0) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:0 inSection:0]];
}
else {
for (int i = 0; i < resultsSize; i++) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:resultsSize + i inSection:0]];
}
}
for (id obj in self.results)
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
} completion:nil];
Explanation of what I have / what I'm doing:
This code runs fine when the initial insertion into the collection view is done. However, when I add/insert more data into my collection view (by updating self.results and calling this), this gives the following error:
* 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 (8) must be equal to the number of
items contained in that section before the update (4), plus or minus
the number of items inserted or deleted from that section (32
inserted, 0 deleted) and plus or minus the number of items moved into
or out of that section (0 moved in, 0 moved out).'
I understand this means that the data source wasn't updated properly. However, when querying my self.results array, I see the new count of the data. I am doing that on the first line using addObjectsFromArray. I also stored the old results size in resultsSize. I use that variable to add the newly added index paths into arrayWithIndexPaths.
Now when adding/inserting items, I tried the following for-loops:
for (id obj in self.results) which is what I am using now. It works initially but further inserts crashes.
for (UIImage *image in newData) works as well initially but further inserts crash.
From the name of the function, I believe that insertItemsAtIndexPaths will insert all items at those index paths without a loop. However, without a loop, the app crashes when it initially tries to populate the data.
I also tried looping from resultsSize + 1 until the new self.results count (which contains the new data) and that also crashes at the initial update.
Any suggestions about what I'm doing wrong?
Thank you,

I see several things wrong here. First, I'm not sure why you're using dispatch_sync, I don't have much experience with GCD, and I couldn't make it work with that in there (it seemed to hang, and the UI was unresponsive). Perhaps someone else can help with that. Second, in the loop where you add index paths, you're looping over resultsSize, which as I understand it, is the size of the array before the update, which is not what you want -- you want to start the new indexes at resultsSize and loop up to resultsSize + newData.count. Lastly, when you call insertItemsAtIndexPaths, you want to do that once, not in a loop. I tried this, and it worked to update the collection view (I didn't try it from scratch with an empty collection view):
-(void)addNewCells {
[self.collectionView performBatchUpdates:^{
int resultsSize = [self.results count];
[self.results addObjectsFromArray:newData];
NSMutableArray *arrayWithIndexPaths = [NSMutableArray array];
for (int i = resultsSize; i < resultsSize + newData.count; i++) {
[arrayWithIndexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
[self.collectionView insertItemsAtIndexPaths:arrayWithIndexPaths];
}
completion:nil];
}

Related

NSMutableArray was mutated while being enumerated

I have an array in an old objective-C app that I am using to learn more "complicated" coding. It is back from the old days of OS X and was very much broken. I have gotten it to work (mostly)! However, the app has an NSMutableArray of images, 7 in total. I use a random number generator to insert the images on the screen, some code to allow them to fall, and then, using screen bounds, when they reach "0" on the Y axis they are removed from the array.
I initially just had:
if( currentFrame.origin.y+currentFrame.size.height <= 0 )
{
[flakesArray removeObject:myItem];
I have read when removing objects from an array it is best practice to iterate in reverse...so I have this bit of code:
for (NSInteger i = myArray.count - 1; i >= 0; i--)
{ //added for for statement
if( currentFrame.origin.y+currentFrame.size.height <= 0 )
{
[myArray removeObjectAtIndex:i];
}
Sadly both methods result in the same mutated while enumerated error. Am I missing something obvious?
If I add an NSLog statement I can get, I think, the index of the item that needs to be removed:
NSLog (#"Shazam! %ld", (long)i);
2017-01-07 14:39:42.086667 MyApp[45995:7500033] Shazam! 2
I have looked through a lot and tried several different methods including this one, which looks to be the most popular with the same error.
Thank you in advance! I will happily provide any additional information!
Adding more:
Sorry guys I am not explicitly calling NSFastEnumeration but I have this:
- (void) drawRectCocoa:(NSRect)rect
{
NSEnumerator* flakesEnum = [flakesArray objectEnumerator];
then
for( i = 0; i < numberToCreate; i++ )
{
[self newObject:self];
}
while( oneFlake = [flakesEnum nextObject] )
It is here where:
if( currentFrame.origin.y+currentFrame.size.height <= 0 )
{
NSLog (#"Shazam! %i", oneFlake);
[flakesArray removeObject:oneFlake];
}
Thank you all. I am learning a lot from this discussion!
There are two ways to go: (1) collect the objects to remove then remove them with removeObjectsInArray:.
NSMutableArray *removeThese = [NSMutableArray array];
for (id item in myArray) {
if (/* item satisfies some condition for removal */) {
[removeThese addObject:item];
}
}
// the following (and any other method that mutates the array) must be done
// *outside of* the loop that enumerates the array
[myArray removeObjectsInArray:removeThese];
Alternatively, reverseObjectEnumeration is tolerant of removes during iteration...
for (id item in [myArray reverseObjectEnumerator]) {
if (/* item satisfies some condition for removal */) {
[myArray removeObject: item];
}
}
As per the error, you may not mutate any NSMutableArray (or any NSMutable... collection) while it is being enumerated as part of any fast enumeration loop (for (... in ...) { ... }).
#danh's answer works as well, but involves allocating a new array of elements. There are two simpler and more efficient ways to filter an array:
[array filterUsingPredicate:[NSPredicate predicateWithBlock:^(id element, NSDictionary<NSString *,id> *bindings) {
// if element should stay, return YES; if it should be removed, return NO
}];
or
NSMutableIndexSet *indicesToRemove = [NSMutableIndexSet new];
for (NSUInteger i = 0; i < array.count; i += 1) {
if (/* array[i] should be removed */) {
[indicesToRemove addIndex:i];
}
}
[array removeObjectsAtIndexes:indicesToRemove];
filterUsingPredicate: will likely be slightly faster (since it uses fast enumeration itself), but depending on the specific application, removeObjectsAtIndexes: may be more flexible.
No matter what, if you're using your array inside a fast enumeration loop, you will have to perform the modification outside of the loop. You can use filterUsingPredicate: to replace the loop altogether, or you can keep the loop and keep track of the indices of the elements you want to remove for later.

UICollectionView Very Slow Custom Layout

I am using a UICollectionView with a custom layout that lays out cells in a grid format. There can be well over 50 rows and 50 columns. Scrolling occurs both vertically and horizontally. Currently, I am doing all of the layout setup in prepareLayout and storing it in arrays:
- (void)prepareLayout {
NSMutableArray *newLayoutInfo = [[NSMutableArray alloc] init];
NSMutableArray *newLinearLayoutInfor = [[NSMutableArray alloc] init];
NSInteger sectionCount = [self.collectionView numberOfSections];
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:0];
self.heightForRows = [delegate collectionViewHeightForAllRows];
self.totalWidthsForRows = [[NSMutableArray alloc] init];
for (int i = 0; i < sectionCount; i++) {
[self.totalWidthsForRows addObject:[NSNumber numberWithInt:0]];
}
for (NSInteger section = 0; section < sectionCount; section++) {
NSMutableArray *cellLayoutInfo = [[NSMutableArray alloc] init];
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
itemAttributes.frame = [self frameForCellAtIndexPath:indexPath];
[cellLayoutInfo addObject:itemAttributes];
[newLinearLayoutInfor addObject:itemAttributes];
}
[newLayoutInfo addObject:cellLayoutInfo];
}
self.layoutInfo = newLayoutInfo;
self.linearLayoutInfo = newLinearLayoutInfor;
}
Then in layoutAttributesForElementsInRect I have:
- (NSArray*)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *rows = [self.linearLayoutInfo filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UICollectionViewLayoutAttributes *evaluatedObject, NSDictionary *bindings) {
return CGRectIntersectsRect(rect, [evaluatedObject frame]);
}]];
This works okay, but it is laggy and jumpy when I have over 50 columns and 50 rows. The problem I now have is that I must set
-(BOOL)shouldInvalidateLayoutForBoundsChange {
return YES;
}
This makes it prepare the entire layout every time the bounds change, which, needless to say, has a huge impact on performance and you can barely scroll. The cells consist of just text with an opaque background, so there is no issue there.
I am sure I am not doing this right and that there must be a better way. Thanks for the help in advance.
In custom flow layout I do this and it seems to help:
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
return !(CGSizeEqualToSize(newBounds.size, self.collectionView.frame.size));
}
-(BOOL)shouldInvalidateLayoutForBoundsChange {
return YES;
}
Causes the layout to do prepareLayout() every time it scrolls, which means anything of heavy computing in prepare will lead to a laggy practice, so one possible direction to solve this is to check what's really taking much time. One possibility is what's inside
for (NSInteger section = 0; section < sectionCount; section++)
{
// generate attributes ...
}
in order to generate attributes for the layout. Every time it scrolls, every time this generalization reruns, so that it impacts on the scroll appear to be jumpy and clumsy. So in order to solve this issue, or at least sort out that this is not the really trouble, I suggest setting a flag in this layout algorithm, say, isScrolling, standing for the situation where the layout needs to prepare. Every time in prepareLayout() check the flag, if it is YES, then we'll know there's no need to do for loop to regenerate all the attributes, which alreay exsit ever since the first time the layout is initialised.
ok--I understand now. Here's what I recommend: create 3 collection views... one for the column headers (where each cell is column header), one for the row leaders (each cell = 1 row leader) and one collection view for your cells. Then when the scroll position of any collection view is changed by the user, update the scroll positions for the other 2 collection views as appropriate.

IBAction to display next object of array

I'm trying to do something I thought was going to be very simple but I can't figure it out. I'm filling an array with objects like this in TableViewController:
if (indexPath.row == 0) {
detailController.textArray = [[NSMutableArray alloc] initWithObjects:#"Text1", #"Text2", #"Text3", #"Text4", #"Text5", nil];
}
Then in the DetailController I added this action to make the label display the next object of the textArray:
- (IBAction)nexTextButtonPressed:(id)sender {
int i = 0;
if (i<[textArray count])
i++;
textLabel.text = [textArray objectAtIndex:i];
}
I connected the button in ib to the action with option Touch Down (tried some of the others too). Something isn't working, the button jumps to the second object of the array but then it doesn't work any more. What could be causing this and how to fix it?
int i = 0; is initialized inside the method, it will always be reinitialized to zero on every call which will not let you move forward from second object
Well every time you hit the button i is getting reset to 0 so when it hits the if statement yes it goes into the statement as the condition is correct, but then you are incrementing i by 1 so i becomes 1 then you retrieve the array index 1 every time. You aren't actually doing anything else. No loop to print through the array or anything.

IKImageBrowserView Moving items doesn't animate

I have an IKImageBrowser setup which appears to be working well. I have set it up to allow reordering and also set animation to YES (in my awakeFromNib), however whenever I select and try and reorder the images I get strange behaviour:
1) They don't reorder most of the time
2) If they do they don't animate
3) Sometimes the images land on each other, if I scroll away and back they are back where they started.
If I highlight an image and delete it, it animates and disappears as expected...
Is this a problem with Core Animation? Do I need to add a core animation layer to the object in interface builder? I followed this tutorial from Apple to get these results: http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/ImageKitProgrammingGuide/ImageBrowser/ImageBrowser.html#//apple_ref/doc/uid/TP40004907-CH5-SW1
Here's the code in question:
- (BOOL) imageBrowser:(IKImageBrowserView *) aBrowser moveItemsAtIndexes: (NSIndexSet *)indexes toIndex:(NSUInteger)destinationIndex{
int index;
NSMutableArray *temporaryArray;
temporaryArray = [[[NSMutableArray alloc] init] autorelease];
for(index=[indexes lastIndex]; index != NSNotFound;
index = [indexes indexLessThanIndex:index])
{
if (index < destinationIndex)
destinationIndex --;
id obj = [mImages objectAtIndex:index];
[temporaryArray addObject:obj];
[mImages removeObjectAtIndex:index];
}
// Insert at the new destination
int n = [temporaryArray count];
for(index=0; index < n; index++){
[mImages insertObject:[temporaryArray objectAtIndex:index]
atIndex:destinationIndex];
}
return YES;
}
Interestingly, this line throws a warning
for(index=[indexes lastIndex]; index != NSNotFound;
comparison is always true due to
limited range of data type
As mentioned above by NSGod, changing int index to NSUInteger index solved the problem

UITableView deleteRowsAtIndexPath when to update the model?

I'm implementing a method to remove a row from my tableview based on a domain object but I'm getting a error after call deleteRowsAtIndexPath:animated
Blockquote Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (2) must be equal to the number of rows contained in that section before the update (2), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted).'
So I thought "ok, I'm updating the model too soon". The code:
- (void)deleteItem:(Item *)item {
NSInteger index = [items indexOfObject:item]; // items is the model a NSArray
if (index != NSNotFound) {
NSMutableArray *itemEditable = [items mutableCopy];
[itemEditable removeObjectAtIndex:index];
self.items = itemEditable;
NSIndexPath *indexToDelete = [NSIndexPath indexPathForRow:index inSection:0];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexToDelete] withRowAnimation:UITableViewRowAnimationMiddle];
}
}
And then I change the code to update the model after I call deleteRowsAtIndexPath:animated
- (void)deleteItem:(Item *)item {
NSInteger index = [items indexOfObject:item]; // items is the model a NSArray
if (index != NSNotFound) {
NSIndexPath *indexToDelete = [NSIndexPath indexPathForRow:index inSection:0];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexToDelete] withRowAnimation:UITableViewRowAnimationMiddle];
NSMutableArray *itemEditable = [items mutableCopy];
[itemEditable removeObjectAtIndex:index];
self.items = itemEditable;
}
}
But it didn't solve, the only thing that changes is the number of rows in the error message from 2 to 3.
Blockquote Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (3) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted).'
Where should I update the model?
Added numberOfRowsInSection method
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [items count];
}
I've found the problem. The setter of items property is implemented to call reloadData and this was causing this confusion.
-(void) setItens:(NSMutableArray *)newItens{
if (itens != nil) {
[itens release];
itens = nil;
}
itens = [newItens copy];
// [tableView reloadData]; // WRONG don't do this
}
This is the second time I found what I was doing wrong just after publishing the question here. I hope this help someone one day. Thanks aBitObvious, I'm aBitShy now :)
I think after deleting the row you need to reload it just below the deleted line:
I mean,
- (void)deleteItem:(Item *)item
{
.......................your code..............
[tableView reloadData];
}
Try this, it should work.