Parallax scrolling album covers with UICollectionView

Parallax is all the rage right now and with iOS 7 came a whole lot more of it. In the new Music app, in iTunes Radio, there is a particular parallax effect that I really like. The scrolling album cover stacks. This seemed like a fun challenge to implement and today I’m going to show you how to create this effect using, of course, UICollectionView. Here is the result.

Before I dive into any code I want to explain my approach a little bit first. We’re going to create a UICollectionViewCell and place a UIImageView centered inside. This image will be fixed and have an equal amount of padding on each side. Then we’ll create several more image views, each one slightly inset from the previous and place them behind the fixed image view. These images will be fluid and move left and right based on where the cell is currently scrolled, occupying the padded space.

Here you can see an outline of the cell I’ve described. The blue outline is our fixed image and the green outline is the bounds of our cell. Our fluid images are outlined in gray.

Next let’s determine how these fluid images will move. To do this we need to know where each cell is relative to collection view’s bounds. Let’s create a common scale to represent this information. Let’s say:

-1 will mean the cell is scrolled leftmost in the view.
 0 will mean the cell is perfectly centered in the view.
 1 will mean the cell is scrolled rightmost in the view.

This is called normalization and we can do it with a simple linear equation. Here it is in code:

- (CGFloat)parallaxPositionForCell:(UICollectionViewCell *)cell {
 
    CGRect frame = [cell frame];
    CGPoint point = [[cell superview] convertPoint:frame.origin toView:collectionView];
 
    const CGFloat minX = CGRectGetMinX([collectionView bounds]) - frame.size.width;
    const CGFloat maxX = CGRectGetMaxX([collectionView bounds]);
 
    const CGFloat minPos = -1.0f;
    const CGFloat maxPos = 1.0f;
 
    return (maxPos - minPos) / (maxX - minX) * (point.x - minX) + minPos;
}

Now we need a way to deliver this information to each cell as the collectionView scrolls.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
 
    for (id cell in [collectionView visibleCells]) {
        CGFloat position = [self parallaxPositionForCell:cell];
        [cell setParallaxPosition:position]; // We will implement this next.
    }
}

Next we need to implement setParallaxPosition: in our custom cell class. This method will be responsible for moving our fluid images around based on the position value. Since we’re working with a common scale here we should define what our values mean for the cell.

-1 will mean the images are completely offset to the right.

0 will mean the images are perfectly centered.

1 will mean the images are completely offset to the left.

Now, in our custom cell class we’ll add this implementation:

- (void)setParallaxPosition:(CGFloat)position {
 
    CGRect bounds = [self bounds];
 
    // We only use the height dimension for our image view. So the padding
    // on either side will be the difference in width divided by 2.
    const CGFloat padding = (bounds.size.width - bounds.size.height) / 2.0;
 
    const CGFloat minOffsetX  = -padding;
    const CGFloat maxOffsetX  = padding;
 
    const CGFloat minPosition = 1.0;
    const CGFloat maxPosition = -1.0;
 
    // Compute the total offset using a linear equation
    CGFloat offsetX = (maxOffsetX - minOffsetX) / (maxPosition - minPosition) * (position - minPosition) + minOffsetX;
 
    // Divide the total offset among the images that will be moved
    offsetX /= ([imageViews count] - 1);
 
    // Apply the offsetX to each image relative to the first one
    CGRect fixedRect = [[imageViews objectAtIndex:0] frame];
    for (NSInteger i = 1; i < [imageViews count]; i++) {
 
        UIImageView *imageView = [imageViews objectAtIndex:i];
        CGRect imageRect = [imageView frame];
        CGFloat imageWidth = imageRect.size.width;
        imageRect.origin.x = CGRectGetMidX(fixedRect) - 0.5 * imageWidth + (offsetX * i);
        [imageView setFrame:imageRect];
    }
}

What we are doing here is very similar to what we did previously. We’re taking our position in the -1 to 1 range and converting it to an offsetX amount. Then we iterate through the fluid image views and apply that offset to each frame relative to the fixed image frame.

You can view the full source on Github.