Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionView horizontal scrolling, deleting last item, animation not working

I have a UICollectionView. It scrolls horizontally, has only a single row of items, and behaves like a paging UIScrollView. I'm making something along the lines of the Safari tab picker, so you can still see the edge of each item. I only have one section.

If I delete an item that is not the last item, everything works as expected and a new item slides in from the right.

If I delete the last item, then the collection view's scroll position jumps to the N-1th item (doesn't smoothly animate), and then I see the Nth item (the one I deleted) fade out.

This behaviour isn't related to the custom layout I made, as it occurs even if I switch it to use a plain flow layout. I'm deleting the items using:

[self.tabCollectionView deleteItemsAtIndexPaths:@[[NSIndexPath indexPathForItem:index inSection:0]]];

Has anyone else experienced this? Is it a bug in UICollectionView, and is there a workaround?

like image 407
Amy Worrall Avatar asked Oct 26 '12 14:10

Amy Worrall


2 Answers

I managed to get my implementation working using the standard UICollectionViewFlowLayout. I had to create the animations manually.

First, I caused the deleted cell to fade out using a basic animation:

- (void)tappedCloseButtonOnCell:(ScreenCell *)cell {

    // We don't want to close our last screen.
    if ([self screenCount] == 1u) {
        return;
    }

    [UIView animateWithDuration:UINavigationControllerHideShowBarDuration
                     animations:^{
                         // Fade out the cell.
                         cell.alpha = 0.0f;
                     }
                     completion:^(BOOL finished) {

                         NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
                         UIViewController *screen = [self viewControllerAtIndex:indexPath.item];

                         [self removeScreen:screen animated:YES];
                     }];
}

Next, I caused the collection view to scroll to the previous cell. Once I've scrolled to the desired cell, I remove the deleted cell.

- (void)removeScreen:(UIViewController *)screen animated:(BOOL)animated {

    NSParameterAssert(screen);

    NSInteger index = [[self.viewControllerDictionaries valueForKeyPath:kViewControllerKey] indexOfObject:screen];

    if (index == NSNotFound) {
        return;
    }

    [screen willMoveToParentViewController:nil];

    if (animated) {

        dispatch_time_t popTime = DISPATCH_TIME_NOW;
        NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index
                                                 inSection:0];

        // Disables user interaction to make sure the user can't interact with
        // the collection view during the time between when the scroll animation
        // ends and the deleted cell is removed.
        [self.collectionView setUserInteractionEnabled:NO];

        // Scrolls to the previous item, if one exists. If we are at the first
        // item, we just let the next screen slide in from the right.
        if (index > 0) {
            popTime = dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC);
            NSIndexPath *targetIndexPath = [NSIndexPath indexPathForItem:index - 1
                                                  inSection:0];
            [self.collectionView scrollToItemAtIndexPath:targetIndexPath
                                        atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                                                animated:YES];
        }

        // Uses dispatch_after since -scrollToItemAtIndexPath:atScrollPosition:animated:
        // doesn't have a completion block.
        dispatch_after(popTime, dispatch_get_main_queue(), ^{

            [self.collectionView performBatchUpdates:^{
                [self.viewControllerDictionaries removeObjectAtIndex:index];
                [self.collectionView deleteItemsAtIndexPaths:@[indexPath]];
                [screen removeFromParentViewController];
                [self.collectionView setUserInteractionEnabled:YES];
            } completion:NULL];
        });

    } else {
        [self.viewControllerDictionaries removeObjectAtIndex:index];
        [self.collectionView reloadData];
        [screen removeFromParentViewController];
    }

    self.addPageButton.enabled = YES;
    [self postScreenChangeNotification];
}

The only part that is slightly questionable is the dispatch_after(). Unfortunately, -scrollToItemAtIndexPath:atScrollPosition:animated: does not have a completion block, so I had to simulate it. To avoid timing problems, I disabled user interaction. This prevents the user from interacting with the collection view before the cell is removed.

Another thing I had to watch for is I have to reset my cell's alpha back to 1 due to cell reuse.

I hope this helps you with your Safari-style tab picker. I know your implementation is different from mine, and I hope that my solution works for you too.

like image 57
rbrown Avatar answered Oct 16 '22 11:10

rbrown


I know this has an answer already but I implemented this in a slightly different way that doesn't require dispatching after a set interval.

In your delete method you would do a check to determine if the last item was being deleted. If it was call the following:

if(self.selection == self.assets.count-1 && self.selection != 0){
    isDeleting = YES;
    [collection scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:self.selection-1 inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}

Assuming selection is the selected item you are deleting. This will scroll to the item to the left of it. Note the if statement checking that this is not the only item. If it were the call would crash as there is no -1 row.

Then you can implement the following method which is called when the scroll animation is complete. I simply set isDeleting to no in the deleteObjectInCollection method and it all seems to work.

- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
    if(isDeleting){
        [self deleteObjectInCollection];
    }
}

I hope this helps.

like image 1
Steve Avatar answered Oct 16 '22 12:10

Steve