Creating Circular and Infinite UIScrollViews

Fri, Nov 5

Editor’s Note: This post was written by Jacob Haskins, Director of Mobile Development at Accella. Thanks to Jon Stroz of Accella for reaching out to me to share this idea.

When creating paging functionality for iPhone apps, there may be times that an infinite page loop would be desired. For example, if you have a small gallery of photos you are displaying, you may want to swipe through the set and have it start back at the beginning once you reach the end. The user would be able to continue swiping as much as they wanted in one direction to continue to view the content. Here are two strategies for achieving this result:

Duplicate End Caps

The first option is best suited for smaller loops. Suppose you have ten photos to display. When the user is on photo one and swipes left, it will take the user to photo ten. When the user is on photo ten and swipes right, it will take the user to photo one. The logic we will follow here is to add photos in order, but place an duplicate of photo ten to the left of photo one and a duplicate of photo one to the right of photo ten.

Now when the user scrolls to the end, we reposition the content offset of our UIScrollView. By having a duplicate photo at the end and repositioning the offset without using animation, we create a seamless experience for the user.

- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender 
{
    // The key is repositioning without animation
    if (scrollView.contentOffset.x == 0) {
        // user is scrolling to the left from image 1 to image 10.
        // reposition offset to show image 10 that is on the right in the scroll view
        [scrollView scrollRectToVisible:CGRectMake(3520,0,320,480) animated:NO];
    }
    else if (scrollView.contentOffset.x == 3840) {
        // user is scrolling to the right from image 10 to image 1.
        // reposition offset to show image 1 that is on the left in the scroll view
        [scrollView scrollRectToVisible:CGRectMake(320,0,320,480) animated:NO];
    }
}
3 Pages Only

There may be times when you want an infinite page loop, but don’t want to load in a lot of content. For example, You may have a lot of content to display inside the UIScrollView. Loading large amounts of data there would not be the ideal approach to the situation.

The logic that we can use there is to keep the UIScrollView at just three pages. Data would load on each page and the user would always be looking at the data in the middle page. When the user scrolled to a new page, the content for each page would be reset and the offset would go back the user is back to viewing the middle page. That way even though you may have a large amount of data to scroll through, it’s not all loaded at once. Only three pages are ever loaded at one time.

 
- (void)viewDidLoad 
{
  [super viewDidLoad];
 
  documentTitles = [[NSMutableArray alloc] init];
 
  // create our array of documents
  for (int i = 0; i < 10; i++) {
    [documentTitles addObject:[NSString stringWithFormat:@"Document %i",i+1]];
  }
 
  // create placeholders for each of our documents
  pageOneDoc = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
  pageTwoDoc = [[UILabel alloc] initWithFrame:CGRectMake(320, 0, 320, 44)];
  pageThreeDoc = [[UILabel alloc] initWithFrame:CGRectMake(640, 0, 320, 44)];
 
  pageOneDoc.textAlignment = UITextAlignmentCenter;
  pageTwoDoc.textAlignment = UITextAlignmentCenter;
  pageThreeDoc.textAlignment = UITextAlignmentCenter;
 
  // load all three pages into our scroll view
  [self loadPageWithId:9 onPage:0];
  [self loadPageWithId:0 onPage:1];
  [self loadPageWithId:1 onPage:2];
 
  [scrollView addSubview:pageOneDoc];
  [scrollView addSubview:pageTwoDoc];
  [scrollView addSubview:pageThreeDoc];
 
  // adjust content size for three pages of data and reposition to center page
  scrollView.contentSize = CGSizeMake(960, 416);
  [scrollView scrollRectToVisible:CGRectMake(320,0,320,416) animated:NO];
}
 
- (void)loadPageWithId:(int)index onPage:(int)page 
{
  // load data for page
  switch (page) {
    case 0:
      pageOneDoc.text = [documentTitles objectAtIndex:index];
      break;
    case 1:
      pageTwoDoc.text = [documentTitles objectAtIndex:index];
      break;
    case 2:
      pageThreeDoc.text = [documentTitles objectAtIndex:index];
      break;
  }
}
 
- (void)scrollViewDidEndDecelerating:(UIScrollView *)sender 
{
  // All data for the documents are stored in an array (documentTitles).
  // We keep track of the index that we are scrolling to so that we
  // know what data to load for each page.
  if(scrollView.contentOffset.x > scrollView.frame.size.width) 
  {
    // We are moving forward. Load the current doc data on the first page.
    [self loadPageWithId:currIndex onPage:0];
 
    // Add one to the currentIndex or reset to 0 if we have reached the end.
    currIndex = (currIndex $gt;= [documentTitles count]-1) ? 0 : currIndex + 1;
    [self loadPageWithId:currIndex onPage:1];
 
    // Load content on the last page. This is either from the next item in the array
    // or the first if we have reached the end.
    nextIndex = (currIndex $gt;= [documentTitles count]-1) ? 0 : currIndex + 1;
 
    [self loadPageWithId:nextIndex onPage:2];
  }
  if(scrollView.contentOffset.x $lt; scrollView.frame.size.width) {
    // We are moving backward. Load the current doc data on the last page.
    [self loadPageWithId:currIndex onPage:2];
 
    // Subtract one from the currentIndex or go to the end if we have reached the beginning.
    currIndex = (currIndex == 0) ? [documentTitles count]-1 : currIndex - 1;
    [self loadPageWithId:currIndex onPage:1];
 
    // Load content on the first page. This is either from the prev item in the array
    // or the last if we have reached the beginning.
    prevIndex = (currIndex == 0) ? [documentTitles count]-1 : currIndex - 1;
 
    [self loadPageWithId:prevIndex onPage:0];
  }     
 
  // Reset offset back to middle page
  [scrollView scrollRectToVisible:CGRectMake(320,0,320,416) animated:NO];
}
Download the Source Code

Xcode Project – Circular UI Scrollview

16 comments

Great tips, it’s a very common problem having to do this. Would you be interested in expanding this a little bit to adapt to ScrollViews where you can see more than one object at once? Check this super customized ScrollView I wrote: http://i.imgur.com/7xVOC.png . It’s a series of circular UIImageViews but your solution wouldn’t work on my case because I must show several elements at the same time.

A big problem I had on this one was paging. You can’t use paging on this situation so I had to code the behavior myself.

by Rafael Gaino on Nov 5, 2010. #

Hello Rafael,

Your image is nearly what I have been trying to figure out how to do. I have a scrollview with 8 images (95 x 77) that need to rotate across the screen back and forth. There is a center ‘selector’ that once the scrollview stops, is supposed to change the image it’s pointing to. I’ve created various methods and have only gotten so far with each of them – is there any chance you’d be willing to help point me in the right direction. It would be most appreciated.

Thank you,
Dae

by Dae Melchi on Oct 28, 2011. #

Hello Dae

I have similar problem that you mentioned. If you got the solution please let me know about how can I do this. Any help would be most appreciated.

thanx
Ravi

by ravi on Dec 30, 2011. #

Thanks very much for this tutorial, I found it very useful.

by Andy on Feb 2, 2011. #

Anyone have issues with asynchronous image loading on the views, flashing when scrolled?

by TC Follansbee on Jun 1, 2011. #

Yes. I am using this example to scroll through a set of 10 UIWebViews. And my problem is that the scrollviews contentoffset is set to the middle view before it has reloaded. Now when you scroll from page 2 to page 3 you will see page 2 flash once very fast and then you see page 3 again.

Any ideas?

by pposthoorn on Sep 18, 2011. #

I was getting a similar flicker using UIImageView and just sublayers.

It took two changes to get rid of the flicker.

1. wrap the entire scrollViewDidEndDecelerating with animations turned off
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions]; // disable animated changes
// … code goes here

// Reset offset back to middle page
[scrollView scrollRectToVisible:CGRectMake(320,0,320,416) animated:NO];

[CATransaction commit];

2. Instead of reloading every page, move the views/layers you want to keep (by updating the frame property), and reload the one of the three that has scrolled off screen with the item that is now in your off screen view.

by David Blake on Oct 17, 2011. #

Thanks dude, Your code helped me alot

by sanchit on Aug 25, 2011. #

I’m grateful for this tutorial, and I have a question. I’m using the first example, but when the scrollview opens, the initial image showing is the left-most image (the duplicate of image 10). If the user tries to go left right away, nothing happens. You have to go right, and then go left.

Here’s my question: how do I set my initial picture to the second image (our original number 1)?

Thanks!

by MIchael on Jan 26, 2012. #

How can i make the same 3 Pages seamless scrollview, with having the webviews in place of label.??

by Bhanu Birani on Feb 3, 2012. #

For Duplicate End Caps method I’m suggesting next approach to avoid artefacts during fast scrolling. First remove whole code from scrollViewDidEndDecelerating. Second add next code.

– (void)scrollViewDidScroll:(UIScrollView *)sender
{
//Please, dont forget about optimization
//remove next 3 lines of code to viewDidLoad!
CGFloat periodOffset = 320*4;
CGFloat offsetActivatingMoveToBeginning = 320*5;
CGFloat offsetActivatingMoveToEnd = 320*1;

static CGFloat offsetX;
offsetX = self.scrollView.contentOffset.x;
if (offsetX > offsetActivatingMoveToBeginning) {
self.scrollView.contentOffset = CGPointMake((offsetX – periodOffset),0.0f);
}
else if (offsetX < offsetActivatingMoveToEnd) {
self.scrollView.contentOffset = CGPointMake((offsetX + periodOffset),0.0f);
}
}
Excuse for my english. Hope, this will be helpful.

by alex on Mar 16, 2012. #

Thanks alex, this worked great.

by ljtopp on Feb 13, 2013. #

Nice solution, but I’m having trouble with my implemantion of it.
I use Custom UIViews instead of UIImages. If I want to use the Duplicate End Caps solution, that means I must add a “mirror” version of my last UIView at the beginning, and a “mirror” version of my first UIView at the end.
Because, when one actually uses the first UIView (e.a setting a toggle switch to ON), and then scrolls to the right, it’s important to see that toggle button also in the duplicated version of my first UIView while scrolling. When the duplicate does not show the same state of the UIView, it would be a weird user experience.

So my code is:

//Weather
weatherView = [[WeatherView alloc] initWithFrame:CGRectMake(960, 0, 320, 190)];
[contentView addSubview:weatherView];

//Weather_duplicate to put in the first place of my uiscroll
weatherView_duplicate = weatherView;
weatherView_duplicate.frame = CGRectMake(0, 0, 320, 190);
[contentView addSubview:weatherView_duplicate];

But only the last UIView is added to the contentView (which is my UIScrollView).

Can someone help me out here? What should I do to have a nice duplicate of my first and last UIView…?

Thanks in advance!

by Wim Van Buynder on Jun 23, 2012. #

Your weatherView_duplicate is not actually a duplicate (it is the same old weatherView). You have to create it with alloc init.

by Calin B. on Jul 30, 2012. #

This is almost exactly what I was looking for! Thanks so much for writing this up.

The only thing I’m trying to do differently is I don’t want to allow infinite scrolling. I want to take advantage of the three page approach though to minimize the memory footprint, instead of loading a new ‘page’ for each element a user wants to swipe through.

I’ve got everything working perfectly, except when the user scrolls really fast. Because there’s no looping, the contentOffset changes depending on if you’re viewing the first page, last page, or one in the middle. During super fast swiping, the logic that moves the scrollRectToVisible call seems to mess up, and you sometimes get unexpected results, i.e the images don’t seem to scroll in order.

Has anybody gotten the 3 Page approach working without infinite scrolling?

Thanks!

by Kevin F on Aug 14, 2012. #

I find these solutions work great, except when scrolling really fast.
I.E. when scrollViewDidEndDecelerating doesn’t get called before you reach the end pages. It just bounces like it is the end.
Has anyone been able to implement these solutions better for fast scrolling. It definitely works with fast scrolling, but it just doesn’t look too great.

by Liam Parker on Dec 14, 2012. #