7

I'm stucked with a strange crash and trying to fix it all day long. I have a custom UICollectionViewLayout that basically adds gravity and collision behavious to the cells.

The implementation works great! The problem happens when I try to delete one cell using: [self.collectionView performBatchUpdates:].

It gives me the following error:

2013-12-12 21:15:35.269 APPNAME[97890:70b] *** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit_Sim/UIKit-2935.58/UICollectionViewData.m:357

2013-12-12 20:55:49.739 APPNAME[97438:70b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UICollectionView recieved layout attributes for a cell with an index path that does not exist: <NSIndexPath: 0x975d290> {length = 2, path = 0 - 4}'

My model is being handled correctly and I can see it removing the item from it!

The indexPaths of the item to delete is being passed correctly between objects. The only time the collectionView update doen't crash is when I delete the last cell on it, otherwise, the crash happens.

Here's the code I'm using to delete the cell.

- (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion
{
    UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove];

    [self.gravityBehaviour removeItem:attributes];
    [self.itemBehaviour removeItem:attributes];
    [self.collisionBehaviour removeItem:attributes];

    [self.collectionView performBatchUpdates:^{
        [self.fetchedBeacons removeObjectAtIndex:itemToRemove.row];
        [self.collectionView deleteItemsAtIndexPaths:@[itemToRemove]];
    } completion:nil];   
}

The CollectionView delegates that handles the cell attibutes are the basic ones below.

- (CGSize)collectionViewContentSize
{
    return self.collectionView.bounds.size;
}

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return [self.dynamicAnimator itemsInRect:rect];
}

- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return [self.dynamicAnimator layoutAttributesForCellAtIndexPath:indexPath];
}

Things I already tried with no success: - Invalidating the layout - Reloading the data - Removing the behaviours from the UIDynamicAnimator and adding them again after the update

Any insights?

A source code with the problem is available on this repository. Please check it out. Code Repository

Best. George.

  • Try adding some code into the data delegate method that returns a cell and monitor the requests on the console. Likewise add logging to the methods return number of sections, number of cells. Perhaps the cv is requesting the latter before it even starts the batch update (in which case you need to move you beacon delete out of the block to before. Really, I sympathize - batch update was working just fine for me, I made a small change to my cells (added views), and wham, it started crashing all over and I had to make every change use reload :-( – David H Dec 13 '13 at 14:57
  • Thanks David. Here is the output of the load, delete and crash sequence. Seems the counts are ok. [Link to output log](https://dl.dropboxusercontent.com/u/7373916/output.txt) – George Villasboas Dec 13 '13 at 18:14
  • Can you add logs before and after performBatchUpdates, and within the block before, middle, and after each code line. Also to make me happy add log the number of items in self.fetchedBeacons after the delete (make that the middle log message), then update log. I know this is a PITA, but what I did with CVs is write a really simple one view app that used labels in the cells, to play with the various modes of doing things. If that works and the complex code doesn't its CV screwing up (most likely). It seems CV changes behavior on how complex the cells are, which really makes it hard for us! – David H Dec 13 '13 at 18:26
  • I searched on "Assertion failure in -[UICollectionViewData validateLayoutInRect:]" and got a huge number of hits. One possible solution: http://stackoverflow.com/a/19378624/1633251 - another good one http://stackoverflow.com/a/18411860/1633251 . Hmmm also, are you verifying that the item is visible? If not perhaps just reloading data would work... – David H Dec 13 '13 at 18:37
  • @DavidH Thank you David. I had tried the suggested links. Reloading data and invalidating the layout seems useless. Seems that the dynamicAnimator takes full control of that. Also I'm using a custom layout, so header sizes never gets called here. I isolated the code in a working example. If you have the time and wanna dig down on this bug, please check it out. [Code Repository](https://github.com/ghvillasboas/CollectionViewDynamics) Best!!! – George Villasboas Dec 13 '13 at 20:25
  • You say your counts are okay, but if you perform the delete on your collection view directly -- not using perform batch updates, you see there is a mismatch. I get: "The number of items contained in an existing section after the update (5) must be equal to the number of items contained in that section before the update (5), plus or minus the number of items inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).'" when I run your code and move the delete out of the perform batch call. – Matt Long Dec 17 '13 at 17:10
  • @MattLong You only get this error message if you delete a roll and still then dont update the model. If you do both, this error wont happen. Best. – George Villasboas Dec 19 '13 at 12:59
  • If you didn't update your github project, I sure hope you will as this is an amazing demo! – David H Jan 02 '14 at 23:17

4 Answers4

12

After some weeks struggling with this bug, some helpful insights from our friends @david-h and @erwin and some calls and e-mails from Apple's WWDR, I was able to figure this issue out. Just wanna share the solution with the community.

  1. The problem. UIKit Dynamics has some helper methods to work with Collection Views and these methods don't actually do some internal stuff that I think it should do automatically, like automatically update the dynamically animated cells when some action using performBatchUpdates: is performed. So you have to do it manually according to WWDR, so we iterate over the updated itens updating their NSIndexPaths so the the dynamic animator can give us proper updates on the cells.

  2. The bug. Even doing this indexPath update on a cell insertion/deletion, I had lots of random crashes and strange behaviours with the animation. So, I followed a tip given by @erwin that consists on re-instantiate the UIDynamicAnimator after a performBatchUpdates: and that fixes up all the problems with this kind of situation.

So the code.

- (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion
{
    UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove];

    if (attributes) {
        [self.collisionBehaviour removeItem:attributes];
        [self.gravityBehaviour removeItem:attributes];
        [self.itemBehaviour removeItem:attributes];

        // Fix the problem explained on 1.
        // Update all the indexPaths of the remaining cells
        NSArray *remainingAttributes = self.collisionBehaviour.items;
        for (UICollectionViewLayoutAttributes *attributes in remainingAttributes) {
            if (attributes.indexPath.row > itemToRemove.row)
                attributes.indexPath = [NSIndexPath indexPathForRow:(attributes.indexPath.row - 1) inSection:attributes.indexPath.section];
        }

        [self.collectionView performBatchUpdates:^{
            completion();
            [self.collectionView deleteItemsAtIndexPaths:@[itemToRemove]];
        } completion:nil];

        // Fix the bug explained on 2.
        // Re-instantiate the Dynamic Animator
        self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];
        [self.dynamicAnimator addBehavior:self.collisionBehaviour];
        [self.dynamicAnimator addBehavior:self.gravityBehaviour];
        [self.dynamicAnimator addBehavior:self.itemBehaviour];
    }
}

I opened a Radar explaining the issue expecting Apple to fix this on a future update. If anyone whats to duplicate it, It's available on OpenRadar.

Thank you everyone.

4

I've been struggling with a similar situation.

In my case I am using UIAttachmentBehaviors, so each UICollectionViewLayoutAttributes item gets its own behavior. So instead of removing items from behaviors, I am removing the appropriate behavior from the dynamic animator.

For me, deleting a middle UICollectionViewCell seems to work (no crash), but the app then crashes if I try to delete the last cell.

Close inspection of the animator's behaviors (using debug logs) shows that the index paths of the remaining items are indeed off by one past the item that was deleted. Manually resetting them does not by itself fix the issue.

The problem seems to be a mismatch between the number of cells in the collection view and the number of items returned by the dynamic animator's -itemsInRect: (all my cells are always visible).

I can prevent a crash by removing all behaviors before I delete a cell. But of course this results in an undesired movement of my cells when the attachments go away.

What I really needed was a way to reset the items in the dynamic animator without discarding them completely and re-creating them.

So, finally, I came up with a mechanism based on storing the behaviors off to the side, re-instatiating a dynamic animator, and re-adding the behaviors.

It seems to work well, and can probably be further optimized.

- (void)detachItemAtIndexPath:(NSIndexPath *)indexPath completion:(void (^)(void))completion {

for (UIAttachmentBehavior *behavior in dynamicAnimator.behaviors) {
    UICollectionViewLayoutAttributes *thisItem = [[behavior items] firstObject];
    if (thisItem.indexPath.row == indexPath.row) {
        [dynamicAnimator removeBehavior:behavior];
    }
    if (thisItem.indexPath.row > indexPath.row) {
        thisItem.indexPath = [NSIndexPath indexPathForRow:thisItem.indexPath.row-1 inSection:0];
    }
}

NSArray *tmp = [NSArray arrayWithArray:dynamicAnimator.behaviors];

self.dynamicAnimator = [[UIDynamicAnimator alloc] initWithCollectionViewLayout:self];

for (UIAttachmentBehavior *behavior in tmp) {
    [dynamicAnimator addBehavior:behavior];
}

    // custom algorithm to place cells
for (UIAttachmentBehavior *behavior in dynamicAnimator.behaviors) {
    [self setAnchorPoint:behavior];
}

[self.collectionView performBatchUpdates:^{
    [self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
} completion:^(BOOL finished) {
            completion();
}];

}

Erwin
  • 555
  • 6
  • 8
2

First, amazing project! I love how the boxes bounce. There always seems to be one missing - row 0. Anyway, anyone reading this who has a interest in layout should see it! Love the animation.

In ...Layout.m:

Changed this method to just reload:

- (void)removeItemAtIndexPath:(NSIndexPath *)itemToRemove completion:(void (^)(void))completion
{
    //assert([NSThread isMainThread]);

    UICollectionViewLayoutAttributes *attributes = [self.dynamicAnimator layoutAttributesForCellAtIndexPath:itemToRemove];
    [self.collisionBehaviour removeItem:attributes];
    [self.gravityBehaviour removeItem:attributes];
    [self.itemBehaviour removeItem:attributes];

    completion();

    [self.collectionView reloadData];
}

I added these log messages:

- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    NSLog(@"ASKED FOR LAYOUT ELEMENTS IN RECT");
    NSArray *foo = [self.dynamicAnimator itemsInRect:rect];
    NSLog(@"...LAYOUT ELEMENTS %@", foo);
    return foo;
}

Ran the program and deleted a middle item. Look at the index paths in the console. When you delete an item, you have to reset the index paths, as they now do not properly reflect the cells new indices.

CollectionViewDynamics[95862:70b] ASKED FOR LAYOUT ELEMENTS IN RECT
CollectionViewDynamics[95862:70b] ...LAYOUT ELEMENTS (
    "<UICollectionViewLayoutAttributes: 0x109405530> index path: (<NSIndexPath: 0xc000000000008016> {length = 2, path = 0 - 1}); frame = (105.41 -102.09; 100.18 100.18); transform = [0.99999838000043739, -0.0017999990280001574, 0.0017999990280001574, 0.99999838000043739, 0, 0]; ",
    "<UICollectionViewLayoutAttributes: 0x10939c2b0> index path: (<NSIndexPath: 0xc000000000018016> {length = 2, path = 0 - 3}); frame = (1 -100.5; 100 100); ",
    "<UICollectionViewLayoutAttributes: 0x10912b200> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (3 468; 100 100); "
)
CollectionViewDynamics[95862:70b] *** Assertion failure in -[UICollectionViewData validateLayoutInRect:], /SourceCache/UIKit_Sim/UIKit-2935.80.1/UICollectionViewData.m:357

Fixing that should get you going (I hope). NSLog has been my best friend for years! YMMV

David H
  • 40,852
  • 12
  • 92
  • 138
  • Hi @david-h! Thanks a lot. Glad you liked the animation. The final look is even more interesting. Lets keep in touch so I can show you when its ready. Still, no success here. Updating the indexpaths is something that reloading the CV should handle and doing it manually introduced lots other strange bugs. Since I'm a bit tired of filling up Radars/Bugreports and never get feedback, I used one of my accounts support request. Lets see if a Apple Engineer can figure this out. I'll be more then glad to post the final answer (if there is one). Thanks and take care! – George Villasboas Dec 16 '13 at 18:20
0

solved similar problem by one string

self.<#custom_layout_class_name#>.dynamicAnimator = nil;

Have to cast it every time renewing datasource

Mikhail Baynov
  • 121
  • 1
  • 3