UICollectionView animate items after reloadItemsAtIndexPaths is called (fade animation).
Is there a way to avoid this animation?
iOS 6
It's worth noting that if you're targeting iOS 7 and above, you can use the new UIView method performWithoutAnimation:. I suspect that under the hood this is doing much the same as the other answers here (temporarily disabling UIView animations / Core Animation actions), but the syntax is nice and clean.
So for this question in particular...
Objective-C:
[UIView performWithoutAnimation:^{
[self.collectionView reloadItemsAtIndexPaths:indexPaths];
}];
Swift:
UIView.performWithoutAnimation {
self.collectionView.reloadItemsAtIndexPaths(indexPaths)
}
Of course this principle can be applied for any situation that you want to ensure a change is not animated.
You could also try this:
UICollectionView *collectionView;
...
[UIView setAnimationsEnabled:NO];
[collectionView performBatchUpdates:^{
[collectionView reloadItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
[UIView setAnimationsEnabled:YES];
}];
Edit:
I have also found that if you wrap performBatchUpdates in a UIView animation block, the UIView animation is used instead of the default animation, so you can just set the animation duration to 0, like so:
[UIView animateWithDuration:0 animations:^{
[collectionView performBatchUpdates:^{
[collectionView reloadItemsAtIndexPaths:indexPaths];
} completion:nil];
}];
This is extra cool if you want to use iOS 7 springy animations during inserts and deletes!
UICollectionView animate items after reloadItemsAtIndexPaths is called
(fade animation).
Is there a way to avoid this animation?
iOS 6
I assume you're using a FlowLayout. Since you're trying to get rid of the fade animation, try this:
import UIKit
class NoFadeFlowLayout: UICollectionViewFlowLayout {
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)
attrs?.alpha = 1.0
return attrs
}
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath)
attrs?.alpha = 1.0
return attrs
}
}
This is a very old question, so you're probably not targeting iOS 6 anymore. I was personally working on tvOS 11 and had the same question, so this is here for anyone who comes along with the same problem.
I wrote a category on UICollectionView to do just that. The trick is to disable all animations while reloading:
if (!animated) {
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
}
[self reloadItemsAtIndexPaths:indexPaths];
if (!animated) {
[CATransaction commit];
}
extension UICollectionView {
func reloadWithoutAnimation(){
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
self.reloadData()
CATransaction.commit()
}
}
Here is a Swift 3 version to performBatchUpdates without animation to a UICollectionView. I found this to work better for me than collectionView.reloadData() because it reduced cell swapping when records were inserted.
func appendCollectionView(numberOfItems count: Int){
// calculate indexes for the items to be added
let firstIndex = dataItems.count - count
let lastIndex = dataItems.count - 1
var indexPaths = [IndexPath]()
for index in firstIndex...lastIndex {
let indexPath = IndexPath(item: index, section: 0)
indexPaths.append(indexPath)
}
UIView.performWithoutAnimation {
self.collectionView.performBatchUpdates({ () -> Void in
self.collectionView.insertItems(at: indexPaths)
}, completion: { (finished) -> Void in
})
}
}
- (void)reloadCollectionViewAnimated:(BOOL)animated {
if (animated) {
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:^(BOOL finished) {
}];
} else {
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
[CATransaction commit];
}
}
Just to add my $0.02, I tried both versions of the selected answer, and the original way worked better for my purposes. I am working on an infinite scrolling calendar view that allows for a user to enter the calendar at a given week and then swipe back and forth and select individual days for filtering a list.
In my implementation, in order to keep things performant on older devices the array of dates that represent the calendar view has to be kept relatively small which means holding about 5 weeks worth of dates, with the user in the middle at the 3rd week. The issue with using the second approach is, there's a second step where you have to scroll the collection view back to the middle without an animation, which makes for a very jagged appearance for some reason with the blocked base animation.
My Code:
[UIView setAnimationsEnabled:NO];
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:indexPathDeleteArray];
[self.collectionView insertItemsAtIndexPaths:indexPathAddArray];
} completion:NULL];
[UIView setAnimationsEnabled:YES];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:14 inSection:0];
[self.collectionView scrollToItemAtIndexPath:newIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
func reloadRowsWithoutAnimation(at indexPaths: [IndexPath]) {
let contentOffset = collectionView.contentOffset
UIView.setAnimationsEnabled(false)
collectionView.performBatchUpdates {
collectionView.reloadItems(at: indexPaths)
}
UIView.setAnimationsEnabled(true)
collectionView.setContentOffset(contentOffset, animated: false)
}
Instead of using reloadData() try the following to reload all visible cells without animation.
self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems)
Related
UICollectionView animate items after reloadItemsAtIndexPaths is called (fade animation).
Is there a way to avoid this animation?
iOS 6
It's worth noting that if you're targeting iOS 7 and above, you can use the new UIView method performWithoutAnimation:. I suspect that under the hood this is doing much the same as the other answers here (temporarily disabling UIView animations / Core Animation actions), but the syntax is nice and clean.
So for this question in particular...
Objective-C:
[UIView performWithoutAnimation:^{
[self.collectionView reloadItemsAtIndexPaths:indexPaths];
}];
Swift:
UIView.performWithoutAnimation {
self.collectionView.reloadItemsAtIndexPaths(indexPaths)
}
Of course this principle can be applied for any situation that you want to ensure a change is not animated.
You could also try this:
UICollectionView *collectionView;
...
[UIView setAnimationsEnabled:NO];
[collectionView performBatchUpdates:^{
[collectionView reloadItemsAtIndexPaths:indexPaths];
} completion:^(BOOL finished) {
[UIView setAnimationsEnabled:YES];
}];
Edit:
I have also found that if you wrap performBatchUpdates in a UIView animation block, the UIView animation is used instead of the default animation, so you can just set the animation duration to 0, like so:
[UIView animateWithDuration:0 animations:^{
[collectionView performBatchUpdates:^{
[collectionView reloadItemsAtIndexPaths:indexPaths];
} completion:nil];
}];
This is extra cool if you want to use iOS 7 springy animations during inserts and deletes!
UICollectionView animate items after reloadItemsAtIndexPaths is called
(fade animation).
Is there a way to avoid this animation?
iOS 6
I assume you're using a FlowLayout. Since you're trying to get rid of the fade animation, try this:
import UIKit
class NoFadeFlowLayout: UICollectionViewFlowLayout {
override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.initialLayoutAttributesForAppearingItem(at: itemIndexPath)
attrs?.alpha = 1.0
return attrs
}
override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attrs = super.finalLayoutAttributesForDisappearingItem(at: itemIndexPath)
attrs?.alpha = 1.0
return attrs
}
}
This is a very old question, so you're probably not targeting iOS 6 anymore. I was personally working on tvOS 11 and had the same question, so this is here for anyone who comes along with the same problem.
I wrote a category on UICollectionView to do just that. The trick is to disable all animations while reloading:
if (!animated) {
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
}
[self reloadItemsAtIndexPaths:indexPaths];
if (!animated) {
[CATransaction commit];
}
extension UICollectionView {
func reloadWithoutAnimation(){
CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
self.reloadData()
CATransaction.commit()
}
}
Here is a Swift 3 version to performBatchUpdates without animation to a UICollectionView. I found this to work better for me than collectionView.reloadData() because it reduced cell swapping when records were inserted.
func appendCollectionView(numberOfItems count: Int){
// calculate indexes for the items to be added
let firstIndex = dataItems.count - count
let lastIndex = dataItems.count - 1
var indexPaths = [IndexPath]()
for index in firstIndex...lastIndex {
let indexPath = IndexPath(item: index, section: 0)
indexPaths.append(indexPath)
}
UIView.performWithoutAnimation {
self.collectionView.performBatchUpdates({ () -> Void in
self.collectionView.insertItems(at: indexPaths)
}, completion: { (finished) -> Void in
})
}
}
- (void)reloadCollectionViewAnimated:(BOOL)animated {
if (animated) {
[self.collectionView performBatchUpdates:^{
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
} completion:^(BOOL finished) {
}];
} else {
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
[self.collectionView reloadSections:[NSIndexSet indexSetWithIndex:0]];
[CATransaction commit];
}
}
Just to add my $0.02, I tried both versions of the selected answer, and the original way worked better for my purposes. I am working on an infinite scrolling calendar view that allows for a user to enter the calendar at a given week and then swipe back and forth and select individual days for filtering a list.
In my implementation, in order to keep things performant on older devices the array of dates that represent the calendar view has to be kept relatively small which means holding about 5 weeks worth of dates, with the user in the middle at the 3rd week. The issue with using the second approach is, there's a second step where you have to scroll the collection view back to the middle without an animation, which makes for a very jagged appearance for some reason with the blocked base animation.
My Code:
[UIView setAnimationsEnabled:NO];
[self.collectionView performBatchUpdates:^{
[self.collectionView deleteItemsAtIndexPaths:indexPathDeleteArray];
[self.collectionView insertItemsAtIndexPaths:indexPathAddArray];
} completion:NULL];
[UIView setAnimationsEnabled:YES];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForItem:14 inSection:0];
[self.collectionView scrollToItemAtIndexPath:newIndexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:NO];
func reloadRowsWithoutAnimation(at indexPaths: [IndexPath]) {
let contentOffset = collectionView.contentOffset
UIView.setAnimationsEnabled(false)
collectionView.performBatchUpdates {
collectionView.reloadItems(at: indexPaths)
}
UIView.setAnimationsEnabled(true)
collectionView.setContentOffset(contentOffset, animated: false)
}
Instead of using reloadData() try the following to reload all visible cells without animation.
self.collectionView.reloadItems(at: self.collectionView.indexPathsForVisibleItems)
I'm trying to call collectionView...scrollToItemAtIndexPath to scroll my UICollectionView to the ith cell, where i is the index of the element. My collection view has only one section, and when I go the other away around, I always call dataArray[indexPath.item] to get the data for the element at indexPath. However, I'm having trouble getting an indexPath from an integer.
[NSIndexPath indexPathForRow:] works only for UITableView
[NSIndexPath indexPathWithIndex:] doesn't return an NSIndexPath with a section
[NSIndexPath indexPathForItem:inSection:0] returns a path of 0 length that, when used to get the cell with [collectionView cellAtIndexPath:] returns nil.
Any idea what I can do to get a valid index path that I can then use to scroll my UICollectionView? Assume that I have only the NSInteger index of the data array. Thanks.
UPDATED WITH CODE:
NSIndexPath* pathToCell = [NSIndexPath indexPathForItem:curIndex inSection:0];
CustomCollectionViewCell* updatedCell = (CustomCollectionViewCell*)[self collectionView:self.collection cellForItemAtIndexPath:pathToCell];
selectedContent = updatedCell.cellContentView;
// scroll to cell and recenter cover on it
[self.collection scrollToItemAtIndexPath:pathToCell atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];
Then I use the location of the cell to recenter a "cover" view on top of it:
// Animate
CGPoint coverCenter = [childView convertPoint:selectedContent.center toView:parentView];
coverSelectDummy.center = coverCenter;
coverSelectDummy.imageView.image = selectedContent.imageView.image;
coverSelectDummy.imageLabel.text = selectedContent.imageLabel.text;
coverSelectDummy.alpha = 1.0;
coverSelectDummy.transform = CGAffineTransformIdentity;
coverSelectDummy.hidden = NO;
[self.view bringSubviewToFront:coverSelectDummy];
[UIView transitionWithView:coverSelectDummy duration:0.1f options: UIViewAnimationOptionCurveLinear animations:^(void)
{
coverSelectDummy.alpha = 1.0;
coverSelectDummy.transform = CGAffineTransformMakeScale(1.2, 1.2);
}
completion:^(BOOL finished)
{
// .... processing ....
}];
cellForItemAtIndexPath: returns nil if the cell isn't visible (documentation). So if you want to scroll to an item that isn't currently on-screen, that won't work. If I understand correctly you just want to scroll somewhere, so I don't think you even need to worry about that method anyway.
You should be creating an NSIndexPath object using
indexPathForItem:inSection:, and then scroll to that index path using the scrollToItemAtIndexPath:atScrollPosition:animated:
So, assuming the collection view is a property of the View Controller in question and you have a scroll position variable in play:
NSInteger itemToScrollTo = 10;
NSIndexPath *indexPathToScrollTo = [NSIndexPath indexPathForItem:itemToScrollTo inSection:0];
[self.collectionView scrollToItemAtIndexPath:indexPathToScrollTo atScrollPosition:scrollPosition animated:YES];
I have a UITableView which is populated from array and a drop down list.
If I select any row of drop down list the array will be inserted with new values and tableview should get reloaded.
How to animate the tableview with new array contents?
Animation like I want to show the row one by one. i tried this method
- (void)reloadRowsAtIndexPaths:(NSArray )indexPaths withRowAnimation:(UITableViewRowAnimation)animation {
NSIndexPath rowToReload = [NSIndexPath indexPathForRow:[names count] inSection:0];
NSArray* rowsToReload = [NSArray arrayWithObjects:rowToReload, nil];
[tableDetails reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationNone];
}
To add to the correct answer, if you want to reload all sections in your UITableView you will need to do the following:
ObjC
NSRange range = NSMakeRange(0, [self numberOfSectionsInTableView:self.tableView]);
NSIndexSet *sections = [NSIndexSet indexSetWithIndexesInRange:range];
[self.tableView reloadSections:sections withRowAnimation:UITableViewRowAnimationAutomatic];
Swift
let range = NSMakeRange(0, self.tableView.numberOfSections)
let sections = NSIndexSet(indexesIn: range)
self.tableView.reloadSections(sections as IndexSet, with: .automatic)
This will reload everything in the table view with an animation. Like a boss.
Swift 5:
UIView.transition(with: tableView, duration: 1.0, options: .transitionCrossDissolve, animations: {self.tableView.reloadData()}, completion: nil)
Swift 3
UIView.transition(with: myTableView, duration: 1.0, options: .transitionCrossDissolve, animations: {self.myTableView.reloadData()}, completion: nil)
Swift 2
UIView.transitionWithView(myTableView, duration: 1.0, options: .TransitionCrossDissolve, animations: {self.myTableView.reloadData()}, completion: nil)
I went with this in Swift
use this method,
[_tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationFade];
In Swift 3 you can simply add the following extension to UITableView:
extension UITableView {
func reloadData(with animation: UITableView.RowAnimation) {
reloadSections(IndexSet(integersIn: 0..<numberOfSections), with: animation)
}
}
You first tell the tableView to expect updates with beginUpdates. Then update the relevant sections (if your tableView has only one section then just pass zero for the section number). Here's where you specify the animation - you can play around with it to get the effect that you want. After that you call endUpdates on the tableView. The typedef for UITableViewRowAnimation specifies:
typedef enum {
UITableViewRowAnimationFade,
UITableViewRowAnimationRight,
UITableViewRowAnimationLeft,
UITableViewRowAnimationTop,
UITableViewRowAnimationBottom,
UITableViewRowAnimationNone,
UITableViewRowAnimationMiddle,
UITableViewRowAnimationAutomatic = 100
} UITableViewRowAnimation;
Play around to see which one you want. Even selecting UITableViewRowAnimationNone can have a nice effect sometimes. Table update code below:
[self.tableView beginUpdates];
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
you can use reloadSections:withRowAnimation: functions. Check UITableView Class Reference.
Check this reloadData with Animation which already answered your question.
Below method is supported by tableView for animation :
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation NS_AVAILABLE_IOS(3_0);
Usage :
//Reload tableView with animation
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationRight];
Different types of animation available are :
typedef NS_ENUM(NSInteger, UITableViewRowAnimation) {
UITableViewRowAnimationFade,
UITableViewRowAnimationRight, // slide in from right (or out to right)
UITableViewRowAnimationLeft,
UITableViewRowAnimationTop,
UITableViewRowAnimationBottom,
UITableViewRowAnimationNone, // available in iOS 3.0
UITableViewRowAnimationMiddle, // available in iOS 3.2. attempts to keep cell centered in the space it will/did occupy
UITableViewRowAnimationAutomatic = 100 // available in iOS 5.0. chooses an appropriate animation style for you };
Hope this might help!
I have tried this and my Table is Animated with smooth animation
//Put this code where you want to reload your table view
dispatch_async(dispatch_get_main_queue(), ^{
[UIView transitionWithView:<"TableName">
duration:0.1f
options:UIViewAnimationOptionTransitionFlipFromRight
animations:^(void) {
[<"TableName"> reloadData];
} completion:NULL];
});
Try Animating while Transition Change of a view while reloading UITableView
[UIView transitionWithView:_yourTableView
duration:0.40f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^(void) {[_yourTableView reloadData];}
completion:nil];
Swift 5:
let section = 0
tableView.reloadSections([section], with: .automatic)
in will dislplaycell paste this code it will animate add the new cell to tableview
let rotationTransform = CATransform3DTranslate(CATransform3DIdentity, 0, 50, 0)
cell.layer.transform = rotationTransform
cell.alpha = 0.0
UIView.animate(withDuration: 0.05 * indexPath.row) {
cell.layer.transform = CATransform3DIdentity
cell.alpha = 1.0
}
In my UICollectionView, one of the cells contains an animation will go out of the cell's bounds, and now it is covered by other cells nearby. Is it possible to make this special cell on top of others, so it can present animation perfectly?
Thanks.
I had this same need
Specifically, I want to present a modal view controller based on the cell within the collection view that was selected. You can see this sort of behavior in iBooks when you are looking at the library. Select a "book" from the collection, and it "pops out" and opens to the book.
Simply scaling the cell "as is" may have it overlaid by other cells, which is not desirable.
To get this to work, you simply remember the cell's superview (it may not be the UICollectionView directly!) and its original center point. Then you remove it from its superview, add it to your main view (optionally bringing it to the front), perform your animation. In the completion block, reset your transform to Identity (no transform), remove it from your main view, and add it back to its original parent and center point.
I have example code below, but there are a few caveats:
I use IVARS for animation state information because I use the information in multiple methods in my actual implementation
I'm also using QuartzCore 3D transformations due to additional transformations my code does that is outside the scope of this answer
I added an "animating" property to my cell to have it alter its appearance during the animation (so it will scale nicer)
Finally, I switched my code to do a standard modal presentation at the end in this example, rather than calling a non-standard presentation controller which I did in my actual app for the actual launching of the next VC in the scene
There are probably other tweaks you will need to do as well, but the basic technique I outlined above will definitely work.
// Assume: UIView *cell == cell we are animating
// Assume: UIViewController *vc == View controlelr we will launch
// Assume: IVAR CGPoint _animationCellOrigCenter
// Assume: IVAR UIView *_animationCellParent
// Assume: IVAR UIView *_animationCell;
// Save "before" state before animating:
_animationCellOrigCenter = cell.center;
_animationCellParent = cell.superview;
_animationCell = cell;
// Where is the cell currently in the main view?
CGPoint cellViewLoc = [self.view convertPoint:_animationCellOrigCenter fromView:_animationCellParent];
// OK. Hoist the cell into the main view:
[cell removeFromSuperview];
cell.center = cellViewLoc;
[self.view addSubview:cell];
[self.view bringSubviewToFront:cell];
CGSize cellSize = cell.bounds.size;
CGSize newSize = CGSizeMake(600,600); // YMMV
[cell.superview bringSubviewToFront:cell];
// Get ready to do some animating:
CGSize viewSize = collectionView.bounds.size;
CGPoint destination = CGPointMake(viewSize.width / 2, viewSize.height / 2);
CGFloat scaleX = newSize.width / cellSize.width;
CGFloat scaleY = newSize.height / cellSize.height;
cell.animating = YES;
[UIView animateWithDuration:.2
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
animations:^{
cell.layer.transform = CATransform3DMakeScale(scaleX, scaleY, 1);
cell.center = destination;
}
completion:^(BOOL finished) {
[self presentViewController:vc
animated:YES
completion:^{
// Put everything back the way we found it!
[_animationCell removeFromSuperview];
[_animationCellParent addSubview:_animationCell];
_animationCell.center = _animationCellOrigCenter;
_animationCell.layer.transform = CATransform3DIdentity;
_animationCell.animating = NO;
}
];
}
];
This is fairly straightforward. Just add the cell to the collectionview's scrollview before you expand it on top of the others, and then remove it when you're no longer wanting it on top. Below I've done this and then added an animation on each cell itself to have it expand on top the others. I've left that animation out for simplicity.
override func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let cell = collectionView.cellForItemAtIndexPath(indexPath)
self.collectionView?.addSubview(cell!)
return true
}
override func collectionView(collectionView: UICollectionView, shouldDeselectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
let cell = collectionView.cellForItemAtIndexPath(indexPath)
if (cell?.superview != nil) {
cell?.removeFromSuperview()
}
return true
}
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?
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.
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.
This will work:
collectionView.collectionViewLayout.invalidateLayout()
collectionView.layoutIfNeeded()
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.
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])
}
}
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.