16
votes

I'm attempting to setup a scrollview with infinite (horizontal) scrolling.

Scrolling forward is easy - I have implemented scrollViewDidScroll, and when the contentOffset gets near the end I make the scrollview contentsize bigger and add more data into the space (i'll have to deal with the crippling effect this will have later!)

My problem is scrolling back - the plan is to see when I get near the beginning of the scroll view, then when I do make the contentsize bigger, move the existing content along, add the new data to the beginning and then - importantly adjust the contentOffset so the data under the view port stays the same.

This works perfectly if I scroll slowly (or enable paging) but if I go fast (not even very fast!) it goes mad! Heres the code:

- (void) scrollViewDidScroll:(UIScrollView *)scrollView {

    float pageNumber = scrollView.contentOffset.x / 320;
    float pageCount = scrollView.contentSize.width / 320;

    if (pageNumber > pageCount-4) {
        //Add 10 new pages to end
        mainScrollView.contentSize = CGSizeMake(mainScrollView.contentSize.width + 3200, mainScrollView.contentSize.height);
        //add new data here at (320*pageCount, 0);
    }

    //*** the problem is here - I use updatingScrollingContent to make sure its only called once (for accurate testing!)
    if (pageNumber < 4 && !updatingScrollingContent) {

        updatingScrollingContent = YES;

        mainScrollView.contentSize = CGSizeMake(mainScrollView.contentSize.width + 3200, mainScrollView.contentSize.height);
        mainScrollView.contentOffset = CGPointMake(mainScrollView.contentOffset.x + 3200, 0);
        for (UIView *view in [mainContainerView subviews]) {
            view.frame = CGRectMake(view.frame.origin.x+3200, view.frame.origin.y, view.frame.size.width, view.frame.size.height);
        }
        //add new data here at (0, 0);      
    }

    //** MY CHECK!
    NSLog(@"%f", mainScrollView.contentOffset.x);
}

As the scrolling happens the log reads: 1286.500000 1285.500000 1284.500000 1283.500000 1282.500000 1281.500000 1280.500000

Then, when pageNumber<4 (we're getting near the beginning): 4479.500000 4479.500000

Great! - but the numbers should continue to go down in the 4,000s but the next log entries read: 1278.000000 1277.000000 1276.500000 1275.500000 etc....

Continiuing from where it left off!

Just for the record, if scrolled slowly the log reads: 1294.500000 1290.000000 1284.500000 1280.500000 4476.000000 4476.000000 4473.000000 4470.000000 4467.500000 4464.000000 4460.500000 4457.500000 etc....

Any ideas????

Thanks

Ben.

10

10 Answers

7
votes

It could be that whatever is setting those numbers in there, is not greatly impressed by you setting the contentOffset under its hands. So it just goes on setting what it thinks should be the contentOffset for the next instant - without verifying if the contentOffset has changed in the meantime.

I would subclass UIScrollView and put the magic in the setContentOffset method. In my experience all content-offset changing passes through that method, even the content-offset changing induced by the internal scrolling. Just do [super setContentOffset:..] at some point to pass the message on to the real UIScrollView.

Maybe if you put your shifting action in there it will work better. You could at least detect the 3000-off setting of contentOffset, and fix it before passing the message on. If you would also override the contentOffset method, you could try and see if you can make a virtual infinite content size, and reduce that to real proportions "under the hood".

20
votes

I found a really great example app by Apple to implement Infinite Scrolling using a similar idea in the OP. Very simple and most importantly no tearing.

http://developer.apple.com/library/ios/#samplecode/StreetScroller/Introduction/Intro.html

They implemented the "content recentering" every time layoutSubviews was called on the UIScrollView.

The only adjustment I made was to recycle the "tiling effect" instead of throwing away old tiles and allocing new ones.

6
votes

When I had faced this problem I had 3 images, which needed to be able to scroll infinitely in either direction. The optimal solution would probably be to load 3 images and modifying the content panel when user moves to rightmost/ leftmost image but I have found an easier solution. (Did some editing on the code, might contain typos)

Instead of 3 images, I have setup a scroll view with 5 images, like:

3 | 1 | 2 | 3 | 1

and calculated the content view accordingly, whereas the content size is fixed. Here is the code:

for ( NSUInteger i = 1; i <= NO_OF_IMAGES_IN_SCROLLVIEW + 2 ; i ++) {

    UIImage *image;
    if ( i  % 3 == 1){

        image = [UIImage imageNamed:[NSString stringWithFormat:@"img1.png"]];
    }

    else if (i % 3 == 2 ) {

        image = [UIImage imageNamed:[NSString stringWithFormat:@"img2.png"]];

    }

    else {

        image = [UIImage imageNamed:[NSString stringWithFormat:@"img3.png"]];
    }

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i-1)*_scrollView.frame.size.width, 0, _scrollView.frame.size.width, _scrollView.frame.size.height)];
    imageView.contentMode=UIViewContentModeScaleToFill;
    [imageView setImage:image];
    imageView.tag=i+1;
    [self.scrollView addSubview:imageView];
}

[self.scrollView setContentOffset:CGPointMake(self.scrollView.frame.size.width, 0)];
[scrMain setContentSize:CGSizeMake(self.scrollView.frame.size.width * ( NO_OF_IMAGES_IN_SCROLLVIEW + 2 ), self.scrollView.frame.size.height)];

Now that the images are added, the only thing is to create the illusion of infinite scrolling. To do that I have "teleported" the user into the three main image, whenever he tries to scroll to one of the outer two images. It is important to do that not-animated, so that the user won't be able to feel it:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    if (scrollView.contentOffset.x < scrollView.frame.size.width ){

        [scrollView scrollRectToVisible:CGRectMake(scrollView.contentOffset.x + 3 * scrollView.frame.size.width, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:NO];
    }
}

    else if ( scrollView.contentOffset.x > 4 * scrollView.frame.size.width  ){

        [scrollView scrollRectToVisible:CGRectMake(scrollView.contentOffset.x - 3 * scrollView.frame.size.width, 0, scrollView.frame.size.width, scrollView.frame.size.height) animated:NO];
    }
}
5
votes

(hope I'm posting this correctly - I'm new here!?)

mvds was spot on - thanks - subclassing UIScrollView works perfectly - I'm yet to implement the shifting and loading of new data, but I have a UIScrollView that scrolls round in an endless loop!

Heres the code:

#import "BRScrollView.h"

@implementation BRScrollView

- (id)awakeFromNib:(NSCoder *)decoder {
    offsetAdjustment = 0;
    [super initWithCoder:decoder];
    return self;
}

- (void)setContentOffset:(CGPoint)contentOffset {

    float realOffset = contentOffset.x + offsetAdjustment;

    //This happens when contentOffset has updated correctly - there is no need for the adjustment any more
    if (realOffset < expectedContentOffset-2000 || realOffset > expectedContentOffset+2000) {
        offsetAdjustment = 0;
        realOffset = contentOffset.x;
    }

    float pageNumber = realOffset / 320;
    float pageCount = self.contentSize.width / 320;

    if (pageNumber > pageCount-4) {
        offsetAdjustment -= 3200;
        realOffset -= 3200;
    }

    if (pageNumber < 4) {
        offsetAdjustment += 3200;
        realOffset += 3200; 
    }

    //Save expected offset for next call, and pass the real offset on
    expectedContentOffset = realOffset;     
    [super setContentOffset:CGPointMake(realOffset, 0)];


}

- (void)dealloc {
    [super dealloc];
}


@end

Notes:

If you actually want an infinite loop, you'd need to adjust the numbers - this codes notices when you get near the edge and moves you to just beyond the middle

My contentSize is 6720 (21 pages)

I'm only interest in scrolling horizontally, so only save the x values and hard code the y to 0!

Ben

5
votes

I have made a sample project to solve your problem.

you can download the code from

https://code.google.com/p/iphone-infinite-horizontal-scroller/

this scrolls in both directions endlessly and loads images on the go.

1
votes

I have developed this kind of uiscrollview, so you can check my project at Infinite UIScrollView in both direction

1
votes

I've made a subclass of UIScrollView that just does what you want. It just scrolls forever in any directions, even in both directions simultaneously. It supports smooth scrolling and paging scrolling

https://github.com/vasvf/NPInfiniteScrollView

0
votes

Have a look at this too (the video will give you a quick idea of what it does): http://dev.doukasd.com/2011/04/infinite-scrolling-dial-control-for-ios/

It's not horizontal but it should be useful. I tried what you were suggesting, but setting the content offset never looked right while the scroll view was animating, the animation would always break up.

0
votes

You can see this example

numbercolors=[[NSMutableArray alloc] init];

//total count of array is  49 

 numbercolors = [NSMutableArray arrayWithObjects:@"25",@"26",@"27",@"28",@"29",@"31",@"32",@"33",@"34",@"35","0",@"1",@"2",@"3",@"4",@"5",@"6",@"7",@"8",@"9",@"10",@"11",@"12",@"13",@"14",@"15",@"16",@"17",@"18",@"19",@"20",@"21",@"22",@"23",@"24",@"25",@"26",@"27",@"28",@"29",@"30",@"31",@"32",@"33",@"34",@"35", @"0",@"1",@"2",@"3",nil];

  int x=2500;

 for (NSInteger index = 0; index < [numbercolors count]; index++)
 {
  UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];

  button.frame = CGRectMake(x ,0,29.0,77.0);

  button.tag = index;

  [button setTitle:[numbercolors objectAtIndex:index] forState:UIControlStateNormal];

  [button addTarget:self action:@selector(didTapButton:) 

  forControlEvents:UIControlEventTouchUpInside];

  [coloringScroll addSubview:button];

   x=x+70+29;
 } 
  [coloringScroll setContentSize:CGSizeMake(5000+ (29+70)*[numbercolors count], 1)];

  [coloringScroll setContentOffset:CGPointMake(2500+(29+70)*11, 0)];


- (void)scrollViewDidScroll:(UIScrollView *)scrollView

{

if (scrollView.contentOffset.x > 2500+(29+70)*4 + ((29+70)*36))
{

 [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x-((29+70)*36),  0)];

}

 if (scrollView.contentOffset.x < 2500+(29+70)*4)

{
 [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x+((29+70)*36), 

 0)];
}

}
0
votes

So one issue is that setting the contentOffset while in the scrollViewDidScroll: delegate method will cause the delegate method to fire again while you are inside it. So what I do is remove the delegate > setContentOffset > set the delegate again.

Here's my code:

#import "ViewController.h"

@interface ViewController () <UIScrollViewDelegate>
@property (weak, nonatomic) IBOutlet UIScrollView *scrollView;
@property (strong, nonatomic) NSMutableArray *labels;
@property (weak, nonatomic) UILabel *originLabel;

- (void)addScrollViewLabels;
- (void)adjustOrigins:(float)deltaX;

@end


@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    // just some starting size
    CGSize size = self.scrollView.frame.size;
    CGSize contentSize = CGSizeMake(size.width * 4, size.height);
    [self.scrollView setContentSize:contentSize];

    // just some starting offset
    CGPoint contentOffset = CGPointMake((contentSize.width / 2), 0);
    [self.scrollView setContentOffset:contentOffset];

    [self addScrollViewLabels];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGSize contentSize = scrollView.contentSize;
    CGSize size = scrollView.frame.size;
    CGPoint contentOffset = scrollView.contentOffset;

    const float kContentOffsetBuffer = 100;
    const float kContentSizeGrowth = (4 * size.width);

    BOOL shouldGrowContentSize = (contentOffset.x > (contentSize.width - size.width - kContentOffsetBuffer))
                                || (contentOffset.x < (kContentOffsetBuffer));
    if (shouldGrowContentSize) {
        // the contentOffset has reached a point where we need to grow the contentSize
        CGSize adjustedContentSize = CGSizeMake(contentSize.width + kContentSizeGrowth, contentSize.height);
        [self.scrollView setContentSize:adjustedContentSize];

        if(contentOffset.x < (kContentOffsetBuffer)) {
            // the growth needs to happen on the left side which means that we need to adjust the contentOffset to compensate for the growth.
            // this is not necessary when growth happens to the right since the contentOffset is the same.
            CGPoint adjustedContentOffset = CGPointMake(contentOffset.x + kContentSizeGrowth, contentOffset.y);
            [self.scrollView setDelegate:nil];
            [self.scrollView setContentOffset:adjustedContentOffset];
            [self.scrollView setDelegate:self];
            [self adjustOrigins:kContentSizeGrowth];
        }

        [self addScrollViewLabels];
    }
}


- (void)addScrollViewLabels {
    const float kOriginY = 100;

    if (!self.labels) {
        self.labels = [NSMutableArray array];
        float originX = [self.scrollView contentOffset].x;
        UILabel *label0 = [[UILabel alloc] initWithFrame:CGRectMake(originX, kOriginY, 100, 30)];
        label0.text = @"0";
        [self.scrollView addSubview:label0];
        [self.labels addObject:label0];
        self.originLabel = label0;
    }

    CGSize contentSize = [self.scrollView contentSize];
    const float kIncrementAmount = 75;
    NSInteger indexOfOriginLabel = [self.labels indexOfObject:self.originLabel];

    // add labels to the right
    UILabel *lastLabel = [self.labels lastObject];
    float lastOriginX = lastLabel.frame.origin.x;
    for (float x = (lastOriginX + kIncrementAmount); (x < (contentSize.width)) ; x += kIncrementAmount) {
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(x, kOriginY, 100, 30)];
        NSInteger indexFromOrigin = ([self.labels count] - indexOfOriginLabel);
        label.text = [@(indexFromOrigin) description];
        [self.scrollView addSubview:label];
        [self.labels addObject:label];
        [label setNeedsDisplay];
    }

    // add labels to the left
    UILabel *firstLabel = [self.labels firstObject];
    float firstOriginX = firstLabel.frame.origin.x;
    for (float x = (firstOriginX - kIncrementAmount); (x >= 0) ; x -= kIncrementAmount) {
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(x, kOriginY, 100, 30)];
        NSInteger indexFromOrigin = -(indexOfOriginLabel + 1);
        label.text = [@(indexFromOrigin) description];
        [self.scrollView addSubview:label];
        [self.labels insertObject:label atIndex:0];
        indexOfOriginLabel++;
        [label setNeedsDisplay];
    }
}

- (void)adjustOrigins:(float)deltaX {
    for (UILabel *label in self.labels) {
        CGRect frame = label.frame;
        frame.origin.x += deltaX;
        [label setFrame:frame];
        [label setNeedsDisplay];
    }
}


@end