UICollectionView horizontal scrolling, deleting last item, animation not working

前端 未结 6 403
我在风中等你
我在风中等你 2021-02-02 00:53

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 t

相关标签:
6条回答
  • 2021-02-02 01:05

    And here's yet another solution (targetIndexPath is the indexPath of the cell to be removed). This code can simply be placed in a removeCellAtIndexPath:(NSIndexPath*)targetIndexPath method and you're done (assuming my adaptations from my code to public code are correct, otherwise ask me and I'll try to help).

    // (Here it's assumed that self.collectionView.collectionViewLayout has been assigned a UICollectionViewFlowLayout before; note the word "FLOW" in that class name) 
    UICollectionViewFlowLayout* layout = (UICollectionViewFlowLayout*) self.collectionView.collectionViewLayout;
    
    // Find index path of the last cell in collection view:
    NSIndexPath* lastIndexPath = [self lastItemIndexPath];
    
    // Extend content area temporarily to fill out space left by the last row after deletion, if last row is visible:
    BOOL lastRowWasVisible = NO;
    if ([self.collectionView icn_cellAtIndexPathIsVisible:lastIndexPath]) {
    
        lastRowWasVisible = YES;
    
        // Adapt section spacing to temporarily fill out the space potentially left after removing last cell:
        CGFloat cellWithLineSpacingHeight = 79.0f + 8.0f; // Height of my cell + one line spacing
        layout.sectionInset = UIEdgeInsetsMake(0.0f, 0.0f, cellWithLineSpacingHeight, 0.0f);
    }
    
    // Remove the cell:
    [self.collectionView performBatchUpdates:^{
    
        [self.collectionView deleteItemsAtIndexPaths:[NSArray arrayWithObject:targetIndexPath]];
    
    } completion:^(BOOL finished) {
    
        // Only scroll if we had the last row visible:
        if (lastRowWasVisible) {
    
            NSIndexPath* lastItemIndexPath = [self lastItemIndexPath];
    
            // Run a custom scroll animation for two reasons; 1. that way we can reset the insets when animation is finished, and 2. using the "animated:YES" option lags here for some reason:
            [UIView animateWithDuration:0.3f animations:^{
                [self.collectionView scrollToItemAtIndexPath:prevItemIndexPath atScrollPosition:UICollectionViewScrollPositionBottom animated:NO];
            } completion:^(BOOL finished) {
    
                // Reset the space placeholder once having scrolled away from it:
                layout.sectionInset = UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f);
            }];
        }
    }];
    

    The icn_cellAtIndexPathIsVisible: method is just a category on UICollectionView:

    - (BOOL) icn_cellAtIndexPathIsVisible:(NSIndexPath*)indexPath {
    
        BOOL __block wasVisible = NO;
        [self.indexPathsForVisibleItems enumerateObjectsUsingBlock:^(NSIndexPath* ip, NSUInteger idx, BOOL *stop) {
    
            if ([ip isEqual:indexPath]) {
    
                wasVisible = YES;
                *stop = YES;
            }
        }];
    
        return wasVisible;
    }
    

    Update: This only works with one section.

    0 讨论(0)
  • 2021-02-02 01:23

    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.

    0 讨论(0)
  • 2021-02-02 01:25

    I was facing a similar issue with a single line horizontal scrolling through images collection view. The issue disappeared when i removed my code for setting the collection view's contentSize it seems to handle this automagically.

    Not a particularly verbose answer, but i hope it helps.

    0 讨论(0)
  • 2021-02-02 01:27

    This will work:

    collectionView.collectionViewLayout.invalidateLayout()
    
    collectionView.layoutIfNeeded()
    
    0 讨论(0)
  • 2021-02-02 01:30

    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.

    0 讨论(0)
  • 2021-02-02 01:32

    A cheap solution is to add another 0px cell as the last cell and never remove it.

    func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
        if indexPath.row < collectibles.count - 1 { // dont delete the last one. just because the animation isn't working
            collectibles.removeAtIndex(indexPath.row)
            collectionView.deleteItemsAtIndexPaths([indexPath])
        }
    }
    
    0 讨论(0)
提交回复
热议问题