Deleting cell at edge of UICollectionView - cells

2019-02-10 02:52发布

Consider an standard, vertically scrolling flow layout populated with enough cells to cause scrolling. When scrolled to the bottom, if you delete an item such that the content size of the collection view must shrink to accommodate the new number of items (i.e. delete the last item on the bottom row), the row of cells that scroll in from the top are hidden. At the end of the deletion animation, the top row appears without animation - it's a very unpleasant effect.

In slow motion:

Cells not appearing

It's really simple to reproduce:

  1. Create a new single view project and change the default ViewController to be a subclass of UICollectionViewController

  2. Add a UICollectionViewController to the storyboard that uses a standard flow layout, and change its class to ViewController. Give the cell prototype the identifier "Cell" and a size of 200x200.

  3. Add the following code to ViewController.m:


@interface ViewController ()
@property(nonatomic, assign) NSInteger numberOfItems;
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    self.numberOfItems = 19;
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return self.numberOfItems;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    return [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
}

- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
    self.numberOfItems--;
    [collectionView deleteItemsAtIndexPaths:@[indexPath]];
}

@end



Additional Info

I've seen other manifestations of this problem when dealing with collection views, it's just that the above example seems the simplest to demonstrate the issue. UICollectionView seems to go into some kind of paralysed state of panic during the default animations, and refuses to unhide certain cells until after the animation completes. It even prevents manual calls to cell.hidden = NO on hidden cells from having an effect (hidden is still YES afterwards). Dropping down to the underlying layer and setting hidden there works, provided you can get a reference to the cell you want to unhide, which is non-trivial when dealing with cells that haven't been displayed yet.

-initialLayoutAttributesForAppearingItemAtIndexPath is being called for every item visible at the time of the call to deleteItemsAtIndexPaths:, but not for the ones that are scrolled into view. It is possible work around the issue by calling reloadData inside a batch update block immediately afterwards, which appears to make the collection view realise that the top row is about to appear:

[collectionView deleteItemsAtIndexPaths:@[indexPath]];
[collectionView performBatchUpdates:^{
    [collectionView reloadData];
} completion:nil];

But unfortunately this is not an option for me. I am trying to implement some custom animation timing by manipulating the cell layers & animations, and calling reloadData really throws things out of whack by causing unnecessary layout callbacks.



Update: A bit of investigation

I added log statements to a lot of layout methods and looked through some stack frames to try and find out what's going wrong. Crucially, I'm checking when layoutSubviews is called, when the collection view asks for layout attributes from the layout object (layoutAttributesForElementsInRect:) and when applyLayoutAttributes: is called on the cells.

I would expect to see a sequence of methods like this:

// user taps cell (to delete it)
-deleteItemsAtIndexPaths:
-layoutAttributesForElementsInRect:
-finalLayoutAttributes...:                // Called for the item being deleted
-finalLayoutAttributes...:                // \__ Called for each index path visible
-initialLayoutAttributes...:              // /   when deletion started
-applyLayoutAttributes:                   // Called for the item being deleted, to apply final layout attributes
// collection view begins scrolling up
-layoutSubviews:                          // Called multiple times as the 
-layoutAttributesForElementsInRect:       // collection view scrolls
// ... for any new set of
// ... attributes returned:
-collectionView:cellForItemAtIndexPath:
-applyLayoutAttributes:                   // Sets the standard attributes for the new cell
// collection view finishes scrolling

Most of this is happening; layout is correctly triggered as the view scrolls, and the collection view properly queries the layout for the attributes of cells to be displayed. However, collectionView:cellForItemAtIndexPath: and the corresponding applyLayoutAttributes: methods are not being called until after the deletion, when layout is invoked one last time causing the hidden cells to be assigned their layout attributes (sets hidden = NO).

So it seems that despite receiving all the correct responses from the layout object, the collection view has some kind of flag set to not update the cells during the update. There is a private method on UICollectionView called from within layoutSubviews that seems responsible for refreshing the cells' appearance: _updateVisibleCellsNow:. This is from where the data source eventually gets asked for a new cell before applying the cells starting attributes, and it seems this is the point of failure, as it is not being called when it should be.


Additionally, this does seem to be related to the update animation, or at least cells are not updated for the duration of the insertion/deletion. For example the following works without glitches:

- (void)addCell
{
    NSIndexPath *indexPathToInsert = [NSIndexPath indexPathForItem:self.numberOfItems
                                                         inSection:0];
    self.numberOfItems++;
    [self.collectionView insertItemsAtIndexPaths:@[indexPathToInsert]];
    [self.collectionView scrollToItemAtIndexPath:indexPathToInsert
                                atScrollPosition:UICollectionViewScrollPositionCenteredVertically
                                        animated:YES];
}

If the above method is called to insert a cell while the inserted cell is outside the current visible bounds, the item is inserted without animation and the collection view scrolls to it, properly dequeuing and displaying cells on the way.

Problem occurs in iOS 7 & iOS 8 beta 5.

1条回答
甜甜的少女心
2楼-- · 2019-02-10 03:17

Adjust your content insets so that they go beyond the bounds of the device's screen size slightly.

collectionView.contentInsets = UIEdgeInsetsMake(-5,0,0,0); //Adjust this value until it looks ok
查看更多
登录 后发表回答