Animate infinite scrolling of an image in a seamless loop

You say that your image is 2048 wide and your view is 1024 wide. I don’t know if this means you have duplicated the contents of a 1024-wide image to make a 2048-wide image.

Anyway, here’s what I suggest. We’ll need to store the cloud layer and its animation in instance variables:

@implementation ViewController {
    CALayer *cloudLayer;
    CABasicAnimation *cloudLayerAnimation;
}

Instead of setting the cloud layer’s content to the cloud image, we set its background color to a pattern color created from the image. That way, we can set the layer’s bounds to whatever we want and the image will be tiled to fill the bounds:

-(void)cloudScroll {
    UIImage *cloudsImage = [UIImage imageNamed:@"TitleClouds.png"];
    UIColor *cloudPattern = [UIColor colorWithPatternImage:cloudsImage];
    cloudLayer = [CALayer layer];
    cloudLayer.backgroundColor = cloudPattern.CGColor;

However, a CALayer’s coordinate system puts the origin at the lower left instead of the upper left, with the Y axis increasing up. This means that the pattern will be drawn upside-down. We can fix that by flipping the Y axis:

    cloudLayer.transform = CATransform3DMakeScale(1, -1, 1);

By default, a layer’s anchor point is at its center. This means that setting the layer’s position sets the position of its center. It will be easier to position the layer by setting the position of its upper-left corner. We can do that by moving its anchor point to its upper-left corner:

    cloudLayer.anchorPoint = CGPointMake(0, 1);

The width of the layer needs to be the width of the image plus the width of the containing view. That way, as we scroll the layer so that the right edge of the image comes into view, another copy of the image will be drawn to the right of the first copy.

    CGSize viewSize = self.cloudsImageView.bounds.size;
    cloudLayer.frame = CGRectMake(0, 0, cloudsImage.size.width + viewSize.width, viewSize.height);

Now we’re ready to add the layer to the view:

    [self.cloudsImageView.layer addSublayer:cloudLayer];

Now let’s set up the animation. Remember that we changed the layer’s anchor point, so we can control its position by setting the position of its upper-left corner. We want the layer’s upper-left corner to start at the view’s upper-left corner:

    CGPoint startPoint = CGPointZero;

and we want the layer’s upper-left corner to move left by the width of the image:

    CGPoint endPoint = CGPointMake(-cloudsImage.size.width, 0);

The rest of the animation setup is the same as your code. I changed the duration to 3 seconds for testing:

    cloudLayerAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
    cloudLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    cloudLayerAnimation.fromValue = [NSValue valueWithCGPoint:startPoint];
    cloudLayerAnimation.toValue = [NSValue valueWithCGPoint:endPoint];
    cloudLayerAnimation.repeatCount = HUGE_VALF;
    cloudLayerAnimation.duration = 3.0;

We’ll call another method to actually attach the animation to the layer:

    [self applyCloudLayerAnimation];
}

Here’s the method that applies the animation:

- (void)applyCloudLayerAnimation {
    [cloudLayer addAnimation:cloudLayerAnimation forKey:@"position"];
}

When the application enters the background (because the user switched to another app), the system removes the animation from the cloud layer. So we need to reattach it when we enter the foreground again. That’s why we have the applyCloudLayerAnimation method. We need to call that method when the app enters the foreground.

In viewDidAppear:, we can start observing the notification that tells us the app has entered the foreground:

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

We need to stop observing the notification when our view disappears, or when the view controller is deallocated:

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}

When the view controller actually receives the notification, we need to apply the animation again:

- (void)applicationWillEnterForeground:(NSNotification *)note {
    [self applyCloudLayerAnimation];
}

Here’s all the code together for easy copy and paste:

- (void)viewDidLoad {
    [self cloudScroll];
    [super viewDidLoad];
}

-(void)cloudScroll {
    UIImage *cloudsImage = [UIImage imageNamed:@"TitleClouds.png"];
    UIColor *cloudPattern = [UIColor colorWithPatternImage:cloudsImage];
    cloudLayer = [CALayer layer];
    cloudLayer.backgroundColor = cloudPattern.CGColor;

    cloudLayer.transform = CATransform3DMakeScale(1, -1, 1);

    cloudLayer.anchorPoint = CGPointMake(0, 1);

    CGSize viewSize = self.cloudsImageView.bounds.size;
    cloudLayer.frame = CGRectMake(0, 0, cloudsImage.size.width + viewSize.width, viewSize.height);

    [self.cloudsImageView.layer addSublayer:cloudLayer];

    CGPoint startPoint = CGPointZero;
    CGPoint endPoint = CGPointMake(-cloudsImage.size.width, 0);
    cloudLayerAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
    cloudLayerAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    cloudLayerAnimation.fromValue = [NSValue valueWithCGPoint:startPoint];
    cloudLayerAnimation.toValue = [NSValue valueWithCGPoint:endPoint];
    cloudLayerAnimation.repeatCount = HUGE_VALF;
    cloudLayerAnimation.duration = 3.0;
    [self applyCloudLayerAnimation];
}

- (void)applyCloudLayerAnimation {
    [cloudLayer addAnimation:cloudLayerAnimation forKey:@"position"];
}

- (void)viewDidUnload {
    [self setCloudsImageView:nil];
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidBecomeActiveNotification object:nil];
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillEnterForegroundNotification object:nil];
}

- (void)applicationWillEnterForeground:(NSNotification *)note {
    [self applyCloudLayerAnimation];
}

Leave a Comment