21

I'm working on a project where I'm using a UICollectionView to create an 'image ticker' where I'm advertising a series of logos. The collectionView is one item high and twelve items long, and shows two to three items at a time (depending on size of the logos visible).

I would like to make a slow automatic scrolling animation from the first item to the last, and then repeat.

Has anyone been able to make this work? I can get the scrolling working using

[myCollection scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:(myImages.count -1) inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:YES];

But this is way too fast!

[UIView animateWithDuration:10 delay:2 options:(UIViewAnimationOptionAutoreverse + UIViewAnimationOptionRepeat) animations:^{
    [myCollection scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:(myImages.count -1) inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
} completion:nil];

This yields the desired scrolling speed, but only the last few cells are visible in the series. I suspect they (and even the starting visible cells) are being dequeued immediately.

Any thoughts?

Alex Poole
  • 161,851
  • 8
  • 150
  • 257
Chris
  • 743
  • 2
  • 8
  • 22
  • I tried your code, actually the thing is scrollToItemAtIndexPath drives you to the particular indexPath you are passing i.e [myImages.count -1] , so image at that indexPath is visible – Akash KR May 05 '16 at 11:52

5 Answers5

17

You can try this approach:

@property (nonatomic, assign) CGPoint scrollingPoint, endPoint;
@property (nonatomic, strong) NSTimer *scrollingTimer;
@synthesize scrollingPoint, endPoint;
@synthesize scrollingTimer;

- (void)scrollSlowly {
    // Set the point where the scrolling stops.
    self.endPoint = CGPointMake(0, 300);
    // Assuming that you are starting at {0, 0} and scrolling along the x-axis.
    self.scrollingPoint = CGPointMake(0, 0);
    // Change the timer interval for speed regulation. 
    self.scrollingTimer = [NSTimer scheduledTimerWithTimeInterval:0.015 target:self selector:@selector(scrollSlowlyToPoint) userInfo:nil repeats:YES];
}

- (void)scrollSlowlyToPoint {
    self.collectionView.contentOffset = self.scrollingPoint;
    // Here you have to respond to user interactions or else the scrolling will not stop until it reaches the endPoint.
    if (CGPointEqualToPoint(self.scrollingPoint, self.endPoint)) {
        [self.scrollingTimer invalidate];
    }
    // Going one pixel to the right.
    self.scrollingPoint = CGPointMake(self.scrollingPoint.x, self.scrollingPoint.y+1);
}
Masa
  • 3,071
  • 1
  • 14
  • 12
  • 1
    This make the UI thread block, if you have any other controls which requires focus change (like in tvos) this doesn't work. – puru020 Dec 26 '15 at 21:13
  • How do you get it to auto reverse and repeat though in an animation block without messing it up? – mdimarca Jun 24 '18 at 17:35
14

I use a "1 pixel offset" trick. When scrolling programmatically, just set the end contentOffset.x to 1 pixel more/less than it should. This should be unnoticeable. And in completion block set it to actual value. That way you can avoid the premature cell dequeuing problem and get smooth scrolling animation ;)

Here is an example of scrolling to the right page (e.g. from page 1 to 2). Notice that in the animations: block I actually scrolls one pixel less (pageWidth * nextPage - 1). I then restore the correct value in the completion: block.

CGFloat pageWidth = self.collectionView.frame.size.width;
int currentPage = self.collectionView.contentOffset.x / pageWidth;
int nextPage = currentPage + 1;

[UIView animateWithDuration:1
                      delay:0
                    options:UIViewAnimationOptionCurveEaseOut
                 animations:^{
                     [self.collectionView setContentOffset:CGPointMake(pageWidth * nextPage - 1, 0)];
                 } completion:^(BOOL finished) {
                     [self.collectionView setContentOffset:CGPointMake(pageWidth * nextPage, 0)];
                 }];
Hlung
  • 12,632
  • 6
  • 66
  • 87
10

You could also have a "slow" scroll to the end of you UICollectionView without having to "jump" from indexpath to indexpath. I created this quickly for a collection view on a TVOS app:

func autoScroll () {
    let co = collectionView.contentOffset.x
    let no = co + 1

    UIView.animateWithDuration(0.001, delay: 0, options: .CurveEaseInOut, animations: { [weak self]() -> Void in
        self?.collectionView.contentOffset = CGPoint(x: no, y: 0)
        }) { [weak self](finished) -> Void in
            self?.autoScroll()
    }
}

I just call the autoScroll() in the viewDidLoad() and the rest takes care of it itself. The speed of the scrolling is decided by the animation time of the UIView. You could (I haven't tried) add an NSTimer with 0 seconds instead so you can invalidate it on userscroll.

Paul Peelen
  • 9,588
  • 15
  • 82
  • 162
  • I am also trying do to something similar, but want to create a infinite loop (going to the first after the last one). How did you do that? – puru020 Dec 24 '15 at 20:54
  • @puru020 I have that problem as well. If you have a collectionview with same size items than it might be easier. Basically what you do is you add the first few items to the end of the collectionview items, and the last of the range of items to the beginning. When the the last items are comming on screen you can reposition the `contentOffset` to the beginning of the `UICollectionView` without the user noticing; hence: infinite scroll. This might help: http://stackoverflow.com/questions/15549233/view-with-continuous-scroll-both-horizontal-and-vertical – Paul Peelen Feb 19 '16 at 09:07
  • @PaulPeelenYou don't need to set capture list in Swift UIView block as this is executed in a later stage. It will not create a retain cycle. – Tal Zion Jun 07 '16 at 23:06
  • Using this code you need to make sure you do something to stop scrolling when the view disappears (when you navigate to another view but this view remains alive), because in our app this gets called continuously (like thousands of times per second) maxing out the CPU core in our app. – fattjake Jan 18 '18 at 17:52
  • 1
    This is an infinite loop which is called 1000 times per second. Please note that the screen refreshes only 30 times per second. – Yuval Tal Jan 25 '19 at 19:28
5

For anyone else finding this, I've updated Masa's suggestion to work on Swift and I've introduced a little bit of easing towards the end so it acts more like the original scrollItemsToIndexPath animated call. I have hundreds of items in my view so a steady pace wasn't an option for me.

var scrollPoint: CGPoint?
var endPoint: CGPoint?
var scrollTimer: NSTimer?
var scrollingUp = false

func scrollToIndexPath(path: NSIndexPath) {
    let atts = self.collectionView!.layoutAttributesForItemAtIndexPath(path)
    self.endPoint = CGPointMake(0, atts!.frame.origin.y - self.collectionView!.contentInset.top)
    self.scrollPoint = self.collectionView!.contentOffset
    self.scrollingUp = self.collectionView!.contentOffset.y > self.endPoint!.y

    self.scrollTimer?.invalidate()
    self.scrollTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: "scrollTimerTriggered:", userInfo: nil, repeats: true)
}

func scrollTimerTriggered(timer: NSTimer) {
    let dif = fabs(self.scrollPoint!.y - self.endPoint!.y) / 1000.0
    let modifier: CGFloat = self.scrollingUp ? -30 : 30

    self.scrollPoint = CGPointMake(self.scrollPoint!.x, self.scrollPoint!.y + (modifier * dif))
    self.collectionView?.contentOffset = self.scrollPoint!

    if self.scrollingUp && self.collectionView!.contentOffset.y <= self.endPoint!.y {
        self.collectionView!.contentOffset = self.endPoint!
        timer.invalidate()
    } else if !self.scrollingUp && self.collectionView!.contentOffset.y >= self.endPoint!.y {
        self.collectionView!.contentOffset = self.endPoint!
        timer.invalidate()
    }
}

Tweaking the values of dif and modifier adjusts the duration/level of ease for most situations.

Allan Weir
  • 618
  • 5
  • 12
0

In .h file, declare this timer,

NSTimer *Timer;

Place this timer code,

  [CollectionView reloadData];
  Timer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(AutoScroll) userInfo:nil repeats:YES];

then,

#pragma mark- Auto scroll image collectionview
-(void)AutoScroll
{
    if (i<[Array count])
    {
        NSIndexPath *IndexPath = [NSIndexPath indexPathForRow:i inSection:0];
        [self.ImageCollectionView scrollToItemAtIndexPath:IndexPath
            atScrollPosition:UICollectionViewScrollPositionRight animated:NO];
        i++;
        if (i==[Array count])
        {
            i=0;
        }
    }
}

Hope it'll be useful.

Ramdhas
  • 1,723
  • 1
  • 17
  • 26