I'm using UICollectionView in one of my projects and when I try to call reloadData on the collection view, the cell order is always changed the same.
I create the cells something like that:
- (UICollectionViewCell *)collectionView:(UICollectionView *)aCollectionView
cellForItemAtIndexPath:(NSIndexPath *)aIndexPath {
PPhotoCVCell *cell =
[self._collectionView dequeueReusableCellWithReuseIdentifier:#"PPhotoCVCell"
forIndexPath:aIndexPath];
if (cell.photo == nil) {
PPhoto *photo = self._photos[aIndexPath.row];
cell.photo = photo;
}
cell.enableEditing = self._editing;
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}
The cell has two subviews of the class UIImageView, one for the image and another for an overlay if the cell is selected by the user.
When I'm in editing mode, the user can select cells. When some cells are selected and the user quits the editing mode, I set the alpha value of the overlay image view with an animation to 0.0 in the overwritten setSelected: method.
The problem is when I call the reloadData method on the collection view, the overlay image views in the selected cells hide without animation, but in other cells overlay image views appear without animation and disappear with the correct animation.
I detected that the image view for the photo is also changing when I call reloadData.
Is there any solution for this problem?
I ran into this same issue where each time I performed reloadData, the collection view would reverse it's order. Since I didn't see an answer here after 5 years, I'm posting my solution in case anyone else runs into this.
You actually have 2 options:
If you only need to reload a single cell, use the following instead of reloadData:
NSArray *thisItem = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:thisRow inSection:0]];
[self.collectionView reloadItemsAtIndexPaths:thisItem];
If you need to reload all data, use the following:
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
There's probably a reasonable explanation for this behavior somewhere, but I can't find one.
It sounds like your cells are being recycled. In your cell's prepareForReuse, make sure you're removing the overlay. Then add the overlay as appropriate in collectionView: cellForItemAtIndexPath:.
Related
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:
It's really simple to reproduce:
Create a new single view project and change the default ViewController to be a subclass of UICollectionViewController
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.
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.
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
I have an app with a list of articles that a user can read. Once the user taps an article, I want to present it to the user, and then animate moving the cell representing an article to a different collection view section, representing read articles:
Unread | Read
0 0 0 0 | X X
The code below causes all cells to reload, which looks like all of them flashing with background color. Is there a way for me to animate moving once cell from one section of a collection view to another?
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
if([collectionView isEqual:self.largeCollectionView])
{
//position the tapped image at the end and animate reload
UIImage* image = largeImages[indexPath.row];
[largeImages removeObjectAtIndex:indexPath.row];
[largeImages addObject:image];
}
[collectionView performBatchUpdates:^{
[collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:^(BOOL finished) {
}];
}
Have you looked at -moveItemAtIndexPath:toIndexPath:? It sounds like that provides exactly what you are looking for.
You appear to be changing the data backing your collection view and then relying on -reloadSections: to update the UI. That unsurprisingly reloads every cell in the section. Since you know exactly what changes you have been performed you could at least use -deleteItemsAtIndexPaths: and -insertItemsAtIndexPaths: to indicate exactly where you removed and re-inserted this image.
I am using UICollectionView to display images.
Here is my code:
- (WaterFallCollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
WaterFallCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:collectionViewCellIdentifier forIndexPath:indexPath];
NSURL *itemTBURL = [NSURL URLWithString:self.items[indexPath.item][#"image"][#"thumbnailLink"]];
[cell setNeedsLayout];
cell.imageView.imageURL = itemTBURL;
return cell;
}
My problem is, for example, it has totally 10 images in collectionView, first I scroll down to last one, which the first image in collectionView will be invisible, when I scroll up to the first one, it has the refreshing effect and the program will jump into the method I showed. It looks like when the cell turns from invisible to visible state, iOS will force to call the method. I want to store the previously displayed cell, so when you scroll up, it won't have the refresh effect. What should I do? I know Twitter, Pinterest, Facebook they looks like wont' refresh the previous cells when you scroll up. I know it is feasible, just don't know how to do that. Thanks in advance.
What is "refresh effect"? What do you mean?
Everytime you scroll to new UICollectionViewCell, UICollectionView call cellForItemAtIndexPath: to get this cell.
Every cell is reusable. So, you can initialize cell for further reuse it in "- (void) prepareForReuse()" method of cell.
As far as I understand, every time you set imageView.imageURL - image start loading and after that, you show it, right? I think, you should cache this images after first loading.
In any case, there is no way to skip calling cellForItemAtIndexPath.
I have an UICollectionViewController and my custom cells, and in my cellForRowAtIndexPath: method I set the cells based on indexPath.row.
But I am getting wrong results, this cell appears even after first position, and if you scroll back and forth, it pops up in random places. How do i fix that?
Here is the code:
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
DVGCollectionViewCell *cell;
cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"Cell" forIndexPath:indexPath];
if (indexPath.row == 0)
{
cell.imageView.image = [UIImage imageNamed:#"something1.png"];
cell.buyLabel.text = #"170";
cell.textLabel.text = #"11.2011";
}
return cell;
}
Cell in both UITableView and UICollectionView are recycled, that means that when one goes off screen it is put in an NSSet until you need it again. When it's need it's removed from the set ad added again at UICollectionView views hierarchy. If you do not clean the value inside the cell or set them again, the cell will show the same data when it was created.
This is made for performance reason creating cell takes more time instead of value them again.
If your problem is in layout check the layout flow object, which size did you set?
I have found the problem, once the cell contents was set it was never cleaned. So I added cleaning every cell properties as additional clause and it works fine.
You can perform any clean up necessary to prepare the view for use again if you override prepareForReuse in your custom cell implementation.
One of the answers in this SO post helped: override prepareForReuse and reset the cell to its default state. Don't forget to call super.prepareForReuse.
I am just starting to implement a multiselect UICollectionView. Would the below be considered "safe" code (since i assume theres some reason it is called BackgroundView instead of AccessoryView or such) ? I had the idea to save some effort, i intend to keep track of the selected items at the indexpath for further use via an array.
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
//....
cell.selectedBackgroundView = someView_With_A_Checkmark_Image;
[cell bringSubviewToFront:cell.selectedBackgroundView];
//...
return cell;
}
Is it safe?? Ya of course it wont cause any error. If your backgroundView is above the contentView of the cell, then what is the significance of contentView??.
Collection view cell structure
If you select an item in the collection view, collectionView will switch the BackgroundView and Selected background view. So what you can do is give valid views as background view and selected background view upon configuring your custom cell or change any properties of the cell in didSelectItem to differentiate selection. That is better.
Then one more no need to keep track of selection using a separate array. [self.collectionView indexPathsForSelectedItems] will give you selected items path at any point of time