36

I have tried to set the duration (with the normal methods that I would try) for setting the animation duration for UICollectionView's selectItemAtIndexPath:animated:scrollPosition: method (or the scrollToItemAtIndexPath:atScrollPosition:animated: method). I've tried [UIView setAnimationDuration], and I've tried wrapping it in a CATransaction. I've been unsuccessful up to this point at changing that animation duration (although I admit that I could have made a mistake in this logic).

Thoughts?

UPDATE:

I have tried a good number of approaches here. The closest solution is to do what we would normally do for UIScrollView animation (by setting the animated: argument to NO and wrapping it in a UIView animation block). This works perfectly fine for the scrollview. However, this screws with the UICollectionView creation process for some reason.

I have included an example below using two approaches. Each approach assumes that you have 4 sections with 4 items in each section. In addition, the animation assumes you are moving from 0,0 to 3,3.

Using Default Animation

Part of the issue here certainly seems tied to UICollectionView. If you take the following approach (using the default animation option) - all works fine:

[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:3 inSection:3]
                                atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                                        animated:YES];

When this is being executed, each cell in-between the current visible cells and the destination cell is created. I have included logging on the collectionView:cellForItemAtIndexPath: method:

2013-05-18 09:33:24.366 DEF-CV-Testing[75463:c07] Transition
Cell Created for Index Path: <NSIndexPath 0x8913f40> 2 indexes [0, 1]
Cell Created for Index Path: <NSIndexPath 0x75112e0> 2 indexes [0, 2]
Cell Created for Index Path: <NSIndexPath 0xfe1a6c0> 2 indexes [0, 3]
Cell Created for Index Path: <NSIndexPath 0x89159e0> 2 indexes [1, 0]
Cell Created for Index Path: <NSIndexPath 0x8a10e70> 2 indexes [1, 1]
Cell Created for Index Path: <NSIndexPath 0x7510d90> 2 indexes [1, 2]
Cell Created for Index Path: <NSIndexPath 0x75112a0> 2 indexes [1, 3]
Cell Created for Index Path: <NSIndexPath 0x8915a00> 2 indexes [2, 0]
Cell Created for Index Path: <NSIndexPath 0x75111c0> 2 indexes [2, 1]
Cell Created for Index Path: <NSIndexPath 0xfe17f30> 2 indexes [2, 2]
Cell Created for Index Path: <NSIndexPath 0xfe190c0> 2 indexes [2, 3]
Cell Created for Index Path: <NSIndexPath 0xfe16920> 2 indexes [3, 0]
Cell Created for Index Path: <NSIndexPath 0x75112a0> 2 indexes [3, 1]
Cell Created for Index Path: <NSIndexPath 0xfe1a4f0> 2 indexes [3, 2]
Cell Created for Index Path: <NSIndexPath 0x75142d0> 2 indexes [3, 3]

Using Custom Animation

When wrapping the scrollToItemAtIndexPath: method in a UIView animation block, items are not created correctly. See code sample here:

[UIView animateWithDuration:5.0 delay:0.0 options:0 animations:^{
    [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:3 inSection:3]
                                atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                                        animated:NO];
} completion:^(BOOL finished) {
    NSLog(@"Completed");
}];

The currently visible cells disappear and only the destination one is created. I have included the same logging on the collectionView:cellForItemAtIndexPath: method:

Transition
Cell Created for Index Path: <NSIndexPath 0x71702f0> 2 indexes [3, 3]
Completed
Venk
  • 5,949
  • 9
  • 41
  • 52
dtuckernet
  • 7,817
  • 5
  • 39
  • 54
  • Have you tried to set animated to NO and then wrap it inside a `UIView animateWithDuration`? This is how I do it for one of my scrollview animations. I do `[UIView animtateWithDuration ... [scrollView scrollToRect..` – Kal May 17 '13 at 19:51
  • 1
    Yes. Unfortunately this screws up the cell creation. It scrolls - but only shows the first and last UICollectionViewsCell's in the animation. – dtuckernet May 17 '13 at 22:21
  • Just out of curiosity, did you also try to brute force the animation within your block itself. `[UIView animateWithDuration:.... animations:^{ [self.colectionView scrollToItemAtIndexPath:[0, 1]; self.collectionView.scrollToItemAtIndexpath[0,2];....self.collectionView.scrollToItemAtIndexPath[3,3];` – Kal May 20 '13 at 01:30
  • Unfortunately no. In that case the last one wins. The ones before that one have no effect. – dtuckernet May 20 '13 at 10:42
  • 3
    Seems that the only way do to it without any timers etc. and is to call `objc_msgSend(self.collectionView, @selector(_setContentOffsetAnimationDuration:), 10.0)`, where `_setContentOffsetAnimationDuration:` seems to be a part of the private API... – akashivskyy May 20 '13 at 17:48
  • 1
    While this is not an answer it may at least shed some light on your second scenario: what you observe is optimization at work. Why *should* iOS create the intermediate cells if it's not going to show them anyway (animate:NO). So you simply get the final cell created and switched to that - remember the view doesn't know you're animating it "from the outside" ;) – Shirkrin May 23 '13 at 11:54
  • I totally understand what you are saying here - but the problem is then there is no way to animate this correctly with the currently provided public API. This has to be included with UICollectionView - so I have followed with Apple and provided a bug report. – dtuckernet May 24 '13 at 13:04

7 Answers7

8

I had similar problems with UITableView which I solved by the following code:

[CATransaction begin];
[CATransaction setCompletionBlock:onCompletion];
[CATransaction setAnimationDuration:duration];

[self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:3 inSection:3]
                            atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally
                                    animated:YES];

[CATransaction commit];

Obviously, you can't call it with animated:NO because the animation code inside the method is important. Using a CATransaction to wrap the animation worked for me.

Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • 11
    Unfortunately, no - this doesn't work. In this case, the animation does take place - but at the pre-defined duration (not the duration that is set in the `setAnimationDuration:` method. – dtuckernet May 23 '13 at 11:07
  • You can just use `- [UIView animateWithDuration: animations: completion:]` to get the same behavior with less code. – orkoden Dec 14 '15 at 13:11
  • @orkoden Actually no. `UIView` animations and core animations are not the same thing. But I suppose `UIView` animations could work where core animation don't work. – Sulthan Dec 14 '15 at 14:12
1

For me this works. Obviously it needs some adjustments (to have more accurate time and to make it scrolling reverse, from bottom to top. By now it supports only vertical scrolling from top to bottom)

Call something like this:

[self scrollToIndexPath:[NSIndexPath indexPathForItem:40 inSection:0] withAnimationDuration:3.0f];

and here is the code

-(void)scrollToIndexPath:(NSIndexPath *)path withAnimationDuration:(float)duration
{
    NSIndexPath *firstCell = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
    UICollectionViewLayoutAttributes *attributesForFirstCell = [self.collectionView layoutAttributesForItemAtIndexPath:firstCell];
    CGRect firstCellRect = attributesForFirstCell.frame;
    CGPoint startPoint = firstCellRect.origin;

    NSIndexPath *destinationCell = path;
    UICollectionViewLayoutAttributes *attributesForDestCell = [self.collectionView layoutAttributesForItemAtIndexPath:destinationCell];
    CGRect destCellRect = attributesForDestCell.frame;
    CGPoint destPoint = destCellRect.origin;

    self.destination = destPoint;

    float delta = destPoint.y - startPoint.y;

    float cycle = (delta / (50*duration));
    self.pass = (int)cycle + 1;

    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:0.02 target:self selector:@selector(scrollView) userInfo:nil repeats:YES];
}



- (void) scrollView {

    float w = self.pass;

    CGPoint scrollPoint = self.collectionView.contentOffset;
    scrollPoint.y = scrollPoint.y + w;

    if (scrollPoint.y >= self.destination.y)
        [self.myTimer invalidate];
    [self.collectionView setContentOffset: scrollPoint animated: NO];
}
LombaX
  • 17,265
  • 5
  • 52
  • 77
  • 1
    I'd recommend using a [CADisplayLink](https://developer.apple.com/library/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/) for this rather than an NSTimer. CADisplayLink fires automatically once / system frame. – bcattle Jun 25 '15 at 22:31
0

What if you put some code in scrollViewDidScroll: ?

I have a similar problem that I solved by calling selectItemAtIndexPath:animated:scrollPosition and then using scrollViewDidScroll: to run what I would usually run in a completion block.

Of course you'll probably have to keep track of a BOOL or two to decide wether you need to execute anything in scrollViewDidScroll:.

Bart Vandendriessche
  • 1,372
  • 13
  • 14
0

You can do this cleanly with the AnimationEngine library. It wraps a CADisplayLink in block-based animation syntax. I posted an answer here with a code example that animates contentOffset. You just need to do the conversion from indexPath to contentOffset.

Community
  • 1
  • 1
bcattle
  • 12,115
  • 6
  • 62
  • 82
0

I'm able to get it working by wrapping it inside UIView.animate with animated false:

UIView.animate(withDuration: 0.7, delay: 0.0, options: .curveEaseInOut, animations: {
            self.collectionView.scrollToItem(at: IndexPath(row: 0, section: 0), at: .centeredHorizontally, animated: false)
        }, completion: nil)
Strong84
  • 1,869
  • 2
  • 19
  • 24
-1

I found a way to scroll manually and completely bypass the self.collectionView scrollToItemAtIndexPath ..

I also tried to use the completion block to loop through the different sections, but this resulted in a jerky experience.

Note that this example scrolls vertically -- you should be able to change this to scroll horizontally. You're also probably going to have to adjust the offsets so that the scrolling stops at the desired location. Again, I'm surprised that the API does not offer a way to control the speed.

-(void)scrollMethod {

    // Find the point where scrolling should stop.
    UICollectionViewLayoutAttributes *attr = [self.collectionView layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:3 inSection:3]];

    CGRect cellRect = attr.frame;

    self.source = self.collectionView.frame.origin;
    self.destination = cellRect.origin;
    self.myTimer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(scrollOn) userInfo:nil repeats:YES];

}

-(void)scrollOn {
    self.collectionView.contentOffset = self.source;
    // If we have crossed where we need to be, quit the timer
    if ( self.source.y > self.destination.y)
        [self.myTimer invalidate];

    self.source = CGPointMake(self.source.x, self.source.y+1);
}
Kal
  • 24,724
  • 7
  • 65
  • 65
  • 4
    @Lifely It might help if you actually suggested something useful instead of downvoting and making snide comments. The above solution actually worked and I'm still alive :-) – Kal Sep 09 '14 at 01:20
  • @Lifely - Actually a timer can be a best practice for certain types of scrolling logic. See John Resig's (creator of jQuery) blog: http://ejohn.org/blog/learning-from-twitter – Shaheen Ghiassy Aug 04 '15 at 00:33
-3

The Actual solution is you need to do this scrollToItemAtIndexPath in main queue. so that only you can get the actual working of it.

Example code :

 dispatch_async(dispatch_get_main_queue(), ^{

    [collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:NO];

});

shankar
  • 76
  • 12